API GatewayとLambdaでリモートMCPサーバーができた！

はい、できました🎉🎉🎉

API GatewayとLambdaでMCPサーバーを構築できました🎉🎉🎉

概要図はこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/4239e05f-3509-4a0e-923c-c57d5fd4d44d.png)

## MCPのTransports仕様について

MCPの2025-03-26版仕様ではTransportsとして`stdio`と`Streamable HTTP`があります。

- **stdio**: 標準入出力を使用し、MCPクライアントがMCPサーバーをサブプロセスとして起動。標準入出力を使用するため、MCPクライアントとMCPサーバーは同一の環境（パソコン）
- **Streamable HTTP**: HTTP POSTとGETで構成された通信仕様。HTTPでの通信のためMCPクライアントとMCPサーバーが別の環境（パソコン）として実現か可能に

一つ前の2024-11-05版仕様ではHTTP+SSEという仕様がありましたが、Streamable HTTPに置き換わりました。（後方互換としてオプションとしては残っています）

今回はStreamable HTTPを使用します。

## ステートフル or ステートレス

MCPはセッション管理を行うステートフルなプロトコルとして設計されています。（と、私は理解してます）

例えば、ブラウザ操作をするPlaywrightのMCPサーバーの場合、

1. Webサイトを開いて
1. リンクをクリックして画面遷移して
1. ページをスクロールして
1. スクリーンショットを取得する

のように、複数ステップにわたる操作を実施させたい場合があります。しかも、4つのステップを一度に依頼するのではなく、一つのステップごとにユーザーの応答をうけ、チャット形式でやり取りすることも考えられます。

そのため、MCPではセッション管理をしてステートフルな仕様となっていますが、これが **AWS Lambdaと相性が良くありません** 。

MCP仕様の[シーケンス図](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#sequence-diagram)に表現されているのですが、クライアントとサーバーとのコネクションを維持したまま複数のやり取りを行う仕様となっています。

下図の上の赤矢印のところでPOSTリクエストを受けたままコネクションは維持しつつ、下の赤矢印のGETリクエストでツール実行とかのリクエストが来ます。（たぶん）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/0dd15a50-5c48-44b8-9ad5-8d1bcf21f1c4.png)

LambdaでMCPサーバーを実現するとどうなるかというと、POSTを受けるLambdaのインスタンスとGETを受けるLambdaのインスタンスが「 **必ず** 」別のインスタンスになり、セッション情報を共有できません。

なのですが、セッション管理は必須ではないみたいでして、MCPのTypeScript SDKにはセッション管理をしない「**ステートレス**」での利用方法が存在します。

https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#without-session-management-stateless

要はセッションIDをリクエストごとに新しくするような動作をしてそうですが、これであればLambdaでも実現できることがわかりました。

## API Gateway + LambdaとMCP公式SDKは相性が悪い？

MCPの公式SDKを使い、Streamable HTTP方式のMCPサーバーを作成する場合、SDKがいい感じにWebサーバー機能を提供しており、API Gateway + Lambda環境では使いづらいです。（使えるかもしれませんが）


## ということで、API Gateway + LambdaでMCPサーバーを構築するライブラリーを作りました

前置きが長くなりましたが、API Gateway + Lambdaの環境でMCPサーバーを構築するライブラリーを作成し、公開しました🎉🎉🎉

https://github.com/moritalous/lambda-mcp-adaptor

:::note
MCPサーバーをどうコーディングするかの具体例の解説ではなく、ライブラリーの形になっているのには理由がありまして。。

白状すると、Amazon Q Developer CLIが全部作ってくれたので、細かな実装は私も把握してません🙊

ライブラリーの内容が知りたい方はDeepWikiを御覧ください。
https://deepwiki.com/moritalous/lambda-mcp-adaptor
:::

## ライブラリーの使い方を解説

ライブラリーの使い方を手順を追って解説します。

1. SAMプロジェクトを作成
    Node.js22とTypeScriptの環境をベースにします

    ```shell
    sam init --name sam-app \
      --runtime nodejs22.x \
      --dependency-manager npm \
      --app-template hello-world-typescript
    ```

    不格好ですが、`hello-world`のまま進めます。

1. ライブラリーをインストール

    ```shell
    cd sam-app/hello-world/
    npm add github:moritalous/lambda-mcp-adaptor
    ```

1. Lambdaハンドラーを実装
    手順としては、
    1. `createMCPServer`でMCPサーバーを生成
    1. MCPツールを定義。（ツール以外にプロンプト、リソースも対応しており、複数指定可能）
    1. `createLambdaHandler`でLambdaハンドラーを生成

    以上です。
    
    以下のコードが、1つのツールを持ったMCPサーバーLambdaのハンドラーです。全量がこれです。

    :::note
    結構隠蔽しまくりましたので、ほぼMCPツールを定義するだけです
   :::

    ```typescript:sam-app/hello-world/app.ts
    import { createMCPServer, createLambdaHandler } from 'lambda-mcp-adaptor';
    import { z } from 'zod';
    
    // MCPサーバーを定義
    const server = createMCPServer({
        name: 'My MCP Server',
        version: '1.0.0',
        description: 'A powerful MCP server with authentication',
    });
    
    // MCPのツールを定義
    server.tool(
        'calculate',
        {
            operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
            a: z.number(),
            b: z.number(),
        },
        async ({ operation, a, b }) => {
            let result;
            switch (operation) {
                case 'add':
                    result = a + b;
                    break;
                case 'subtract':
                    result = a - b;
                    break;
                case 'multiply':
                    result = a * b;
                    break;
                case 'divide':
                    result = a / b;
                    break;
            }
    
            return {
                content: [{ type: 'text', text: `${a} ${operation} ${b} = ${result}` }],
            };
        },
    );
    
    // Lambdaハンドラーを作成
    export const lambdaHandler = createLambdaHandler(server);
    ```

1. `template.yaml`を修正

    `/mcp`のエンドポイントで`POST`を受けるように設定します

    ```yaml:template.yaml（HelloWorldFunctionのEvents部分のみ抜粋）
      Events:
        MCPPost:
          Type: Api
          Properties:
            Path: /mcp
            Method: post
    ```

    Outputsは`Prod/mcp`に変更しましょう

    ```yaml
    HelloWorldApi:
      Description: "API Gateway endpoint URL for Prod stage for Hello World function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/mcp"
    ```

1. SAMビルドとデプロイ

    ```shell
    sam build
    sam deploy
    ```

これで完成です。

## VSCodeから接続確認

VSCodeをMCPホストとして、接続確認を行います。

1. [表示] -> [コマンドパレット] を選択
1. 「MCP: サーバーの追加」を選択
1. 「HTTP (HTTP またはサーバー送信イベント)」を選択
1. デプロイしたAPI Gatewayのエンドポイント（`https://9999999999.execute-api.{リージョン}.amazonaws.com/Prod/mcp`）を入力
1. 任意の名前（calculate など）を入力
1. 設定を保存する場所を選択

これで設定完了です。

GitHub Copilot Chatをエージェントモードにし、「calculateツールを使って、 2足す3を実行して」と質問します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2cc21ad6-932d-486f-9764-4bcd481eb09f.png)

うまく動作していますね！


## 認証機能もあるよ

認証はAmazon Cognitoでの実現を検討したのですが、MCPの仕様で必須となっている`OAuth 2.0 Dynamic Client Registration Protocol`に未対応のため、採用できませんでした。

代わりに固定のAPIキーを使った認証はできるようにしています。

認証機能を使ったサンプルはこちらに格納していますので参考にしてください。

https://github.com/moritalous/lambda-mcp-adaptor/tree/main/example/mcp-server-auth

---

ぜひ使ってみてください！

気に入っていただけた方はGitHubのスター、Qittaのいいね、いただけると私がニヤニヤします。