Bun + Fastifyで Remote MCP Server を AWS にデプロイしてみた

こんにちは、永尾です。

GitHub が 2025 年 6 月 12 日に Remote GitHub MCP Server のパブリックプレビューを出しました。
さらにMicrosoftもDocsのRemote MCP Server を出しており、時代の波を感じています。

「AWSだとどういう構成になるだろう?」

と思ったので、いつも使い慣れている AWS で、Bun + Fastify + TypeScript で MCP Server を動かしてみました。
本記事はその技術メモです。


インフラのたたき台

メリットデメリット
API Gateway + Lambdaサーバーレスでお手軽・従量課金長時間のストリーミング / SSE に弱い
ALB + Fargateコネクション維持が得意・自由度高い常時起動コスト・証明書管理が必要

今回は SSE を多用する 想定だったため Fargate を選択しました。

また ACM で証明書を発行する手間を避けるため、Route 53 ではなく CloudFront をフロントに置き、TLS 終端も任せています。

graph LR;
    Client["Client"] --> WAF["WAF"];
    WAF --> CloudFront["CloudFront"];
    CloudFront --> ALB["Application Load Balancer"];
    ALB --> Fargate["ECS Fargate"];

ランタイム選定

普段使っている Python なのか、好きな Rust か、流行りの TypeScript かで悩みました。

ライブラリの更新頻度とエコシステムの成熟度から TypeScript を採用しました。

TypeScript を選んだからには、ネイティブで効率よく動かしたいと考えました。

Node, Bun, Deno など選択肢がありましたが、

npm との互換性の高さと、爆速起動が魅力的な Bun を採用しました

となると、次はWebフレームワークの選定です。

一般的な Express も検討しましたが、TypeScriptとの親和性が高く、パフォーマンスに優れた Fastify を採用することにしました。


いざ実装

セットアップ

bun init だけで typescript@types/buntsconfig.json などを自動で追加してくれます。

# Bun 本体
# <https://bun.sh/>
curl -fsSL <https://bun.sh/install> | bash

# プロジェクト初期化
bun init

# 依存追加
bun add fastify @modelcontextprotocol/sdk

SRE チーム説明エンドポイント

下記は サービスリライアビリティ部 (SRE) の業務内容を返すサンプル実装です。

単純な tools だけで実装しました。

*実際には Factory Pattern で実装しています。

記載はしていませんが、Server の Base Image は oven/bun:1.2-alpine を利用しました。


import Fastify from "fastify";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
  InitializeRequestSchema,
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// ===== 設定 =====
const CONFIG = {
  name: "simple-mcp-server",
  version: "1.0.0",
  port: 3000,
  protocolVersion: "2024-11-05",
} as const;

// ===== 型定義 =====
interface ToolResult {
  content: Array<{
    type: "text";
    text: string;
  }>;
}

// ===== ツール定義 =====
const TOOLS = [
  {
    name: "get_sre_responsibilities",
    description: "Get SRE team responsibilities and tasks",
    inputSchema: {
      type: "object",
      properties: {
        category: {
          type: "string",
          description: "SRE category (all, monitoring, incident, automation, capacity)",
          enum: ["all", "monitoring", "incident", "automation", "capacity"],
        },
      },
    },
  },
] as const;

// ===== SRE業務データ =====
const SRE_RESPONSIBILITIES = {
  all: {
    title: "サービスリライアビリティ部の全業務内容",
    items: [
      "📊 SLI/SLO策定・運用によるサービス品質向上",
      "☁️ インフラ設計・運用・管理(AWS・GCP・GitHub)",
      "🚨 インシデント対応・ポストモーテム文化の推進",
      "🔄 DevOps推進・CI/CDパイプライン整備",
      "🤝 プロダクト開発支援・信頼性向上",
      "💰 クラウド・SaaSコスト管理・最適化",
      "🛡️ SaaS Management・セキュリティ権限管理",
      "🔧 運用自動化・手作業削減による効率化",
      "🤖 AI API管理(OpenAI・Claude・Gemini)",
      "📈 SRE成熟度評価・継続的改善",
    ],
  },
  monitoring: {
    title: "SLI/SLO策定・運用によるサービス品質向上",
    items: [
      "各プロダクトに対するSRE成熟度評価の実施",
      "サービスの信頼性現状分析・可視化",
      "サービスレベル指標(SLI)の策定・定義",
      "サービスレベル目標(SLO)の設定・運用",
      "APIパフォーマンス監視(応答速度・成功率・エラー率)",
      "Datadogを活用した包括的監視システム運用",
      "リアルタイムダッシュボードでの継続的監視",
      "SLOベースでの開発・企画・SREチーム協働改善",
      "アラート設定・エスカレーション運用",
      "パフォーマンステストとベンチマーク実施",
    ],
  },
  incident: {
    title: "インシデント対応・ポストモーテム文化の推進",
    items: [
      "24/7インシデント対応体制の運用",
      "障害発生時の迅速な初動対応・エスカレーション",
      "インシデント管理フロー(IMS)の整備・運用",
      "Root Cause Analysis(RCA)による根本原因分析",
      "ポストモーテム実施・学習文化の推進",
      "再発防止策の策定・実装・追跡",
      "インシデント対応ノウハウの文書化・共有",
      "MTTR(平均復旧時間)短縮への取り組み",
      "チームワイドでの障害対応スキル向上",
      "Product SRE視点での予防的信頼性向上",
    ],
  },
  automation: {
    title: "DevOps推進・インフラ自動化・開発効率化",
    items: [
      "CI/CDパイプラインの設計・構築・改善",
      "Infrastructure as Code(IaC)導入・運用(Terraform)",
      "AWS・GCP・GitHubクラウドインフラ設計・開発・運用",
      "コンテナ化・Kubernetes運用(必要に応じて)",
      "デプロイメント自動化・リリース管理",
      "手作業プロセスの特定・自動化推進",
      "運用作業の効率化・標準化",
      "開発環境・テスト環境の自動プロビジョニング",
      "Python・Ruby・Go・TypeScriptでの自動化ツール開発",
      "GitOps・DevSecOpsプラクティスの推進",
    ],
  },
  capacity: {
    title: "コスト管理・プロダクト開発支援・戦略的パートナーシップ",
    items: [
      "クラウドリソース・SaaSコスト最適化戦略",
      "コスト管理フロー・予算管理プロセスの整備",
      "組織全体でのコスト意識向上・教育",
      "Advertising Flow・Commerce Flowプロダクト信頼性支援",
      "Sophia AI・iPalette・CREATIVE BLOOM技術支援",
      "新外部CVプロジェクトの技術コンサルティング",
      "プロダクト開発チームとの協働・技術戦略立案",
      "キャパシティプランニング・スケーラビリティ設計",
      "パフォーマンス最適化・リソース効率化",
      "技術負債解消・アーキテクチャ改善支援",
    ],
  },
} as const;

type SRECategory = keyof typeof SRE_RESPONSIBILITIES;

// ===== ツールハンドラー =====
class ToolHandler {
  static getSreResponsibilities(args: { category?: SRECategory }): ToolResult {
    const category = args.category || "all";
    const responsibility = SRE_RESPONSIBILITIES[category];

    if (!responsibility) {
      throw new Error(`Unknown SRE category: ${category}`);
    }

    return {
      content: [
        {
          type: "text",
          text: `## ${responsibility.title}\n\n${responsibility.items
            .map((item) => `- ${item}`)
            .join("\n")}\n\n*利用可能なカテゴリ: all, monitoring, incident, automation, capacity*`,
        },
      ],
    };
  }

  static handleTool(name: string, args: any): ToolResult {
    switch (name) {
      case "get_sre_responsibilities":
        return this.getSreResponsibilities(args);
      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  }
}

// ===== MCP Server ファクトリー =====
function createMCPServer(): Server {
  const mcpServer = new Server(
    {
      name: CONFIG.name,
      version: CONFIG.version,
    },
    {
      capabilities: {
        tools: {},
      },
    }
  );

  // Initialize ハンドラー
  mcpServer.setRequestHandler(InitializeRequestSchema, async () => {
    return {
      protocolVersion: CONFIG.protocolVersion,
      capabilities: {
        tools: {},
      },
      serverInfo: {
        name: CONFIG.name,
        version: CONFIG.version,
      },
    };
  });

  // Tools リスト ハンドラー
  mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
      tools: TOOLS,
    };
  });

  // Tool 実行ハンドラー
  mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    const result = ToolHandler.handleTool(name, args || {});
    
    return {
      content: result.content,
    };
  });

  return mcpServer;
}

// ===== セッション管理 =====
class SessionManager {
  private sessions = new Map<string, {
    server: Server;
    transport: SSEServerTransport;
  }>();

  storeSession(sessionId: string, server: Server, transport: SSEServerTransport) {
    this.sessions.set(sessionId, { server, transport });
  }

  getSession(sessionId: string) {
    return this.sessions.get(sessionId);
  }

  deleteSession(sessionId: string) {
    this.sessions.delete(sessionId);
  }

  getSessionCount(): number {
    return this.sessions.size;
  }
}

// ===== Fastify Web Server =====
function createWebServer() {
  const fastify = Fastify({
    logger: true,
    requestTimeout: 300000,
  });

  const sessionManager = new SessionManager();

  // ヘルスチェック
  fastify.get("/", async () => {
    return {
      message: "MCP Server is running",
      server: CONFIG.name,
      version: CONFIG.version,
      activeSessions: sessionManager.getSessionCount(),
      endpoints: {
        sse: "/sse",
        health: "/",
      },
    };
  });

  // SSE エンドポイント (GET)
  fastify.get("/sse", async (request, reply) => {
    console.log("SSE connection established");

    const query = request.query as { sessionId?: string };
    const sessionId = query.sessionId || `session_${Date.now()}`;

    // SSE ヘッダー設定
    reply.header("Content-Type", "text/event-stream");
    reply.header("Cache-Control", "no-cache");
    reply.header("Connection", "keep-alive");
    reply.header("Access-Control-Allow-Origin", "*");

    // MCP Server作成・接続
    const mcpServer = createMCPServer();
    const transport = new SSEServerTransport("/sse", reply.raw);
    
    sessionManager.storeSession(sessionId, mcpServer, transport);

    // Keep-alive送信
    const keepAlive = setInterval(() => {
      try {
        reply.raw.write(": keep-alive\n\n");
      } catch (error) {
        console.error("Keep-alive failed:", error);
        clearInterval(keepAlive);
      }
    }, 30000); // 30秒間隔

    try {
      await mcpServer.connect(transport);
      console.log(`MCP Server connected: ${sessionId}`);

      // 接続完了通知
      reply.raw.write(`data: ${JSON.stringify({
        type: "connected",
        sessionId,
        timestamp: new Date().toISOString(),
      })}\n\n`);

    } catch (error) {
      console.error("MCP connection failed:", error);
      clearInterval(keepAlive);
      sessionManager.deleteSession(sessionId);
      reply.status(500).send({ error: "Connection failed" });
      return;
    }

    // クリーンアップ処理
    const cleanup = () => {
      console.log(`SSE connection closed: ${sessionId}`);
      clearInterval(keepAlive);
      sessionManager.deleteSession(sessionId);
    };

    transport.onclose = cleanup;
    reply.raw.on("close", cleanup);
    reply.raw.on("error", cleanup);

    // 接続維持
    return new Promise((resolve) => {
      reply.raw.on("close", () => resolve(undefined));
    });
  });

  // CORS設定
  fastify.addHook("preHandler", async (request, reply) => {
    reply.header("Access-Control-Allow-Origin", "*");
    reply.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    reply.header("Access-Control-Allow-Headers", "Content-Type");
  });

  fastify.options("*", async (request, reply) => {
    reply.status(200).send();
  });

  return fastify;
}

// ===== サーバー起動 =====
async function startServer() {
  const fastify = createWebServer();

  try {
    await fastify.listen({ 
      port: CONFIG.port, 
      host: "0.0.0.0" 
    });
    
    console.log(`🚀 ${CONFIG.name} started!`);
    console.log(`📍 Health Check: http://localhost:${CONFIG.port}/`);
    console.log(`🔗 SSE Endpoint: http://localhost:${CONFIG.port}/sse`);
    console.log(`📝 Available Tool: ${TOOLS[0].name}`);
    console.log(`📋 SRE Categories: all, monitoring, incident, automation, capacity`);
    
  } catch (error) {
    console.error("❌ Server startup failed:", error);
    process.exit(1);
  }
}

実際にやってみた

普段使っている VSCode の Copilot Agent から「SREについて教えて」と聞いてみました。

無事、SRE チームの活動が返ってきました。


感想と振り返り

  • ALB + Fargate はストリーミングに強い反面、アイドル時コストがある
    • 小規模用途なら API Gateway + Lambda でもイケそう
  • Bun + Fastify はセットアップが驚くほど簡単
    • 型安全でレスポンスも速く、「Express を素振りしていた頃には戻れないかも」と感じた
  • 本番運用を視野に入れるなら OAuth 2.1 フロー実装や、ALB/WAF のルール整備、組織的セキュリティレビューは必須

さいごに

技術の進化は本当に早いものです。SRE としてサービスの信頼性を守りながら、新しいテクノロジーを取り込むには「まず触ってみる」姿勢が欠かせません。

そのためにも今後も新しいネタがあれば積極的に触って、どんどん投稿していきます!