AWS Lambda でスケーラブルな MCP サーバーを構築する：実践ガイド

community.awsに投稿したこちらの記事の日本語訳です。

https://community.aws/content/2vzj07Wyk6Lw281Tvs1Lw7kJJNW/building-scalable-mcp-servers-on-aws-lambda-a-practical-guide

---

モデルコンテキストプロトコル（MCP）は、大規模言語モデル（LLM）アプリケーションが様々なデータソースやツールと接続できるようにする標準化されたプロトコルとして登場しました。MCP を活用することで、LLM アプリケーション開発者はデータ統合の実装に時間を費やすのではなく、ビジネスロジックとユーザーエクスペリエンスの向上に集中できます。

この記事では、AWS Lambda 上に MCP サーバーを実装するための実践的なガイドを提供します。「Streamable HTTP（セッションレス）」通信方式を使用して MCP サーバーを Lambda 関数として実装し、AWS Lambda Web Adapter を使用して効率的にデプロイすることに焦点を当てます。この組み合わせにより、スケーラブルでコスト効率の高い MCP サーバーを構築できます。

## MCP の基本と AWS Lambda との互換性

### MCP の役割と価値

モデルコンテキストプロトコル（MCP）は、LLM アプリケーションがデータソースやツールと対話するための共通言語として機能します。MCP は AI モデル向けの「USB-C」と考えることができます - USB-C が様々なデバイスと周辺機器を接続するための標準化された方法を提供するように、MCP は AI モデルと異なるデータソースやツールを接続するための標準化された方法を提供します。

```mermaid
---
config:
  theme: neo
  look: neo
---
flowchart LR
    subgraph "ユーザー環境"
        Host["ホストアプリケーション<br/>(MCP Client)"]
        S1["MCP Server A"]
        S2["MCP Server B"]
        S3["MCP Server C"]
        Host <-->|"MCP Protocol"| S1
        Host <-->|"MCP Protocol"| S2
        Host <-->|"MCP Protocol"| S3
        S1 <--> D1[("ローカル<br/>データソースA")]
        S2 <--> D2[("ローカル<br/>データソースB")]
    end
    subgraph "インターネット"
        S3 <-->|"Web API"| D3[("リモート<br/>サービスC")]
    end
```


MCP を使用する主な利点は以下の通りです：

- **統合の簡素化**：事前構築された統合機能を活用して開発時間を短縮
- **ベンダーロックインの回避**：LLM プロバイダー間の切り替えが容易
- **セキュリティの強化**：データアクセスに関する標準化されたベストプラクティス

### AWS Lambda が最適な理由

AWS Lambda は以下の理由から MCP サーバーの実装に特に適しています：

1. **スケーラビリティ**：リクエスト量に基づいて自動的にスケールし、トラフィックの変動に対応
2. **コスト効率**：使用した分だけ支払うモデルにより、低トラフィック期間のコストを抑制
3. **簡単な管理**：サーバーのプロビジョニングや管理が不要で、開発者はコードに集中可能
4. **シンプルな統合**：API Gateway や他の AWS サービスとの簡単な統合

## MCP 通信方式と Lambda に最適な選択

MCP は現在、クライアント-サーバー通信のための複数の標準トランスポートメカニズムを定義しています。AWS Lambda 実装では、「Streamable HTTP（セッションレス）」が特に最適です。

### Streamable HTTP（セッションレス）の利点

Lambda 関数は基本的にステートレスであり、各呼び出しは独立しています。セッションレスの Streamable HTTP 通信方式は、以下の理由から Lambda に理想的です：

- **ステートレス設計**：Lambda の実行モデルと完全に一致
- **水平スケーリング**：状態を共有する必要がなく、Lambda のスケーリング特性を最大化
- **コールドスタートの最小化**：セッション状態の復元が不要
- **シンプルな実装**：複雑なセッション管理ロジックが不要

## Lambda Web Adapter の活用

AWS Lambda Web Adapter は、Express や Flask などの一般的な Web フレームワークで構築されたアプリケーションを Lambda 上で簡単に実行できるようにするツールです。MCP サーバーの実装において、Lambda Web Adapter はいくつかの重要な役割を果たします：

### Lambda Web Adapter の利点

1. **既存の Web フレームワークをそのまま使用**：Express などの馴染みのあるフレームワークを修正なしで活用
2. **レスポンスストリーミングのサポート**：MCP の Streamable HTTP 通信に必要なストリーミング機能をサポート
3. **シームレスな統合**：Lambda Function URL や API Gateway との簡単な統合
4. **最小限の変更**：コードの変更を最小限に抑えて Lambda 上で実行

### Lambda Function URL の活用

この実装では、API Gateway ではなく Lambda Function URL を使用してエンドポイントを公開します。Lambda Function URL にはいくつかの利点があります：

1. **シンプルな設定**：API Gateway のセットアップなしで HTTP エンドポイントを簡単に作成
2. **レスポンスストリーミング**：MCP の Streamable HTTP 通信に必要なストリーミング機能をネイティブにサポート
3. **低レイテンシー**：API Gateway を排除することによるレイテンシーの削減
4. **コスト効率**：API Gateway のコストを削減

Function URL の `RESPONSE_STREAM` モードは、MCP サーバーのような双方向通信を必要とするアプリケーションに特に効果的です。

## 実装手順

AWS Lambda 上に MCP サーバーを実装するための具体的な手順を見ていきましょう。

### 1. ローカル環境での MCP サーバーの実装

まず、ローカル環境で MCP サーバーを実装し、基本的な機能を確認します。

#### プロジェクトのセットアップ

```bash
mkdir mcp-local
cd mcp-local

npm init -y
npm install --save-dev typescript ts-node @types/node
npx tsc --init
npm install --save @modelcontextprotocol/sdk express
npm install --save-dev @types/express
```

#### サーバーの実装

`src/server.ts` を作成します：

```typescript:src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { z } from "zod";

// MCP サーバーを作成
const server = new McpServer({
  name: "Example MCP Server",
  version: "1.0.0"
});

// Express アプリケーションを作成
const app = express();
app.use(express.json());

// ツールを追加 - シンプルな計算機を実装
server.tool(
  "calculate",
  { a: z.number(), b: z.number(), operation: z.enum(["add", "subtract", "multiply", "divide"]) },
  async ({ a, b, operation }) => {
    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: `結果: ${result}` }]
    };
  }
);

// Streamable HTTP トランスポート（セッションレス）を設定
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined, // セッション管理を無効化
});

// ルートを設定
app.post('/mcp', async (req, res) => {
  try {
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('MCP リクエスト処理中のエラー:', error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: '内部サーバーエラー',
        },
        id: null,
      });
    }
  }
});

app.get('/mcp', async (req, res) => {
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "メソッドは許可されていません。"
    },
    id: null
  }));
});

app.delete('/mcp', async (req, res) => {
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "メソッドは許可されていません。"
    },
    id: null
  }));
});

// サーバーを起動
const PORT = process.env.PORT || 8080;
server.connect(transport).then(() => {
  app.listen(PORT, () => {
    console.log(`MCP サーバーがポート ${PORT} でリッスン中`);
  });
}).catch(error => {
  console.error('サーバーのセットアップに失敗しました:', error);
  process.exit(1);
});
```

#### クライアントの実装

テスト用のクライアントを `src/client.ts` として作成します：

```typescript:src/client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const serverUrl = process.env.MCP_SERVER_URL || "http://localhost:8080";

const transport = new StreamableHTTPClientTransport(new URL(serverUrl + "/mcp"));

const client = new Client(
    {
        name: "example-client",
        version: "1.0.0"
    }
);

async function main(): Promise<void> {
    await client.connect(transport);

    // 利用可能なツールのリストを取得
    const tools = await client.listTools();
    console.log("利用可能なツール:", tools);

    // calculate ツールを呼び出す
    const toolResult = await client.callTool({ 
      "name": "calculate", 
      "arguments": { "a": 1, "b": 2, "operation": "add" } 
    });
    console.log("計算結果:", toolResult.content);
}

main().catch((error: unknown) => {
    console.error('MCP クライアント実行中のエラー:', error);
    process.exit(1);
});
```

#### ローカルテスト

サーバーを起動します：

```bash
npx ts-node src/server.ts
```

別のターミナルで、クライアントを実行します：

```bash
npx ts-node src/client.ts
```

すべてが正常に動作すれば、以下のような出力が表示されるはずです：

```
利用可能なツール: { tools: [ { name: 'calculate', inputSchema: [Object] } ] }
計算結果: [ { type: 'text', text: '結果: 3' } ]
```

### 2. AWS Lambda プロジェクトの設定

ローカルでの機能を確認した後、AWS Lambda 用のプロジェクトをセットアップしましょう。Lambda 関数のデプロイを簡素化するために AWS SAM（Serverless Application Model）を使用します。

#### SAM プロジェクトの作成

```bash
sam init --name mcp-lambda \
  --runtime nodejs22.x \
  --dependency-manager npm \
  --app-template hello-world-typescript \
  --no-interactive

# 不要なファイルを削除し、MCP 関数用のディレクトリを作成
rm -rf mcp-lambda/hello-world/
mkdir mcp-lambda/mcp-function
```

#### Lambda Web Adapter の設定

Lambda Web Adapter を設定します。まず、MCP サーバーを起動するシェルスクリプトを作成します：

```shell:mcp-function/run.sh
#!/bin/bash

node bundle.js
```

次に、Lambda Web Adapter を設定するために SAM テンプレートを修正します：

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  mcp-lambda

  AWS Lambda 上の MCP サーバー実装
  
Globals:
  Function:
    Timeout: 60

Resources:
  MCPStreamableFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: mcp-function/
      Handler: run.sh
      Runtime: nodejs22.x
      MemorySize: 1024
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
          AWS_LWA_INVOKE_MODE: response_stream
          RUST_LOG: info
          PORT: 8080
      Layers:
        - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:24
      # Function URL を使用して HTTP エンドポイントを公開
      # RESPONSE_STREAM モードはストリーミングレスポンスをサポート
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: RESPONSE_STREAM
    Metadata:
      BuildMethod: makefile

Outputs:
  MCPStreamableFunctionUrl:
    Description: "MCP Streamable HTTP 関数の Function URL"
    Value: !GetAtt MCPStreamableFunctionUrl.FunctionUrl
```

### 3. Lambda 関数の実装

Lambda 関数として実行する MCP サーバーを実装します。実装は基本的にローカルバージョンと同じですが、Lambda 環境に合わせていくつかの調整を行います。

#### プロジェクトのセットアップ

```bash
cd mcp-lambda/mcp-function

npm init -y
npm install --save-dev typescript ts-node @types/node
npx tsc --init
npm install --save @modelcontextprotocol/sdk express
npm install --save-dev @types/express
```

#### サーバーの実装

`mcp-function/src/server.ts` をローカルバージョンと同じ内容で作成します。

#### ビルド設定

Lambda 関数としてデプロイするには、TypeScript コードを JavaScript にコンパイルし、依存関係をバンドルする必要があります。これには esbuild を使用します：

```bash
npm install --save-dev esbuild
```

```javascript:mcp-function/esbuild.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/server.js'],
  bundle: true,
  minify: true,
  platform: 'node',
  target: 'node22',
  outfile: 'bundle.js',
  external: ['aws-sdk', '@aws-sdk/*'], // AWS SDK は Lambda 環境に既に存在するため除外
  metafile: true,
}).then(result => {
  // バンドルサイズ情報を出力
  const outputSize = Object.entries(result.metafile.outputs).reduce((acc, [file, data]) => {
    return acc + data.bytes;
  }, 0);
  console.log(`バンドルサイズ: ${(outputSize / 1024 / 1024).toFixed(2)} MB`);
}).catch(() => process.exit(1));
```



```makefilee:mcp-function/Makefile
build-MCPStreamableFunction:
	mkdir -p $(ARTIFACTS_DIR)
	cp -r src run.sh package.json package-lock.json tsconfig.json esbuild.js $(ARTIFACTS_DIR)
	cd $(ARTIFACTS_DIR) && npm ci && npx tsc && node esbuild.js
	cd $(ARTIFACTS_DIR) && rm -rf src package.json package-lock.json tsconfig.json esbuild.js dist node_modules
	chmod +x $(ARTIFACTS_DIR)/run.sh
```

### 4. デプロイとテスト

SAM を使用して Lambda 関数をビルドしデプロイします：

```bash
cd ..  # SAM プロジェクトのルートディレクトリに移動
sam build
sam deploy --guided
```

デプロイが成功すると、Function URL エンドポイントが表示されます：

```
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                               
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 MCPStreamableFunctionUrl                                                                                                                          
Description         MCP Streamable HTTP 関数の Function URL                                                                                                     
Value               https://xxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/                                                                                             
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
```

このエンドポイントは API Gateway を使用せずに直接 HTTP リクエストを受け付けることができる Lambda Function URL です。先ほど作成したクライアントから Lambda 上の MCP サーバーにアクセスするには、次のように実行します：

```bash
MCP_SERVER_URL=https://xxxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws npx ts-node src/client.ts
```

## 実装の洞察と最適化のヒント

### Lambda Web Adapter の仕組み

Lambda Web Adapter は Lambda 関数と Web アプリケーションの間のブリッジとして機能します。その動作フローは以下の通りです：

1. Lambda 関数が呼び出されると、Lambda Web Adapter が起動
2. アダプターは Web アプリケーションの準備ができるのを待機（準備確認）
3. アプリケーションの準備ができたら、アダプターは Lambda ランタイムを開始し、呼び出しを Web アプリケーションに転送
4. Web アプリケーションからのレスポンスはアダプターによって Lambda レスポンス形式に変換

### Function URL とストリーミングレスポンス

この実装では、Lambda Function URL の `RESPONSE_STREAM` モードを使用しており、これには以下の利点があります：

1. **リアルタイム通信**：ストリーミング形式でクライアントにデータを送信
2. **長時間実行**：最大 15 分間のレスポンスストリーミングをサポート
3. **アーキテクチャの簡素化**：API Gateway を使用せずに HTTP エンドポイントを直接公開

このストリーミング機能は、MCP の Streamable HTTP 通信方式において重要な役割を果たします。

### パフォーマンス最適化のヒント

Lambda 上で実行される MCP サーバーのパフォーマンスを最適化するためのヒントをいくつか紹介します：

1. **コールドスタートの最小化**
   - 重要なワークロードにはプロビジョンドコンカレンシーを使用してコールドスタートを回避
   - 依存関係を最小限に抑え、バンドルサイズを小さく保つ

2. **メモリ設定の最適化**
   - CPU パワーとコストのバランスを取るために Lambda 関数のメモリ割り当てを調整
   - 一般的に、MCP サーバーには少なくとも 1024MB のメモリを割り当てる

3. **タイムアウト設定**
   - 潜在的に長時間実行されるリクエストに対応するために適切なタイムアウト値を設定

4. **エラー処理の強化**
   - すべてのエラーケースを適切に処理し、クライアントに意味のあるエラーメッセージを返す

## 結論

この記事では、AWS Lambda 上にモデルコンテキストプロトコル（MCP）サーバーを実装する方法を示しました。Streamable HTTP（セッションレス）通信方式を使用して MCP サーバーを Lambda 関数として実装し、AWS Lambda Web Adapter と Lambda Function URL を使用してデプロイすることに焦点を当てました。

MCP と AWS Lambda の組み合わせには、いくつかの利点があります：

1. **スケーラビリティ**：トラフィックに応じて自動的にスケール
2. **コスト効率**：使用した分だけ支払い
3. **簡単な管理**：サーバーのプロビジョニングや管理が不要
4. **高可用性**：AWS インフラストラクチャの活用
5. **シンプルなエンドポイント公開**：Lambda Function URL を使用して HTTP エンドポイントを直接公開

この実装アプローチにより、AI アプリケーション開発者はインフラストラクチャ管理ではなく、ビジネスロジックとユーザーエクスペリエンスの向上に集中できます。MCP の標準化されたインターフェースと AWS Lambda のサーバーレスアーキテクチャを組み合わせることで、LLM アプリケーションのための柔軟でスケーラブルな基盤を構築できます。

## 参考文献

- [Model Context Protocol 公式ドキュメント](https://modelcontextprotocol.io)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [AWS Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter)
- [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html)
- [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/)