|
| 1 | +--- |
| 2 | +title: "API GatewayとLambdaで実装するプライベートなMCPサーバー" |
| 3 | +date: 2026/03/24 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - MCP |
| 7 | + - Lambda |
| 8 | + - Dify |
| 9 | + - APIGateway |
| 10 | +category: |
| 11 | + - Infrastructure |
| 12 | +thumbnail: /images/2026/20260324a/thumbnail.png |
| 13 | +author: 木村太陽 |
| 14 | +lede: "Strategic AI Group/MLOpsチームでアルバイトをしている木村です。アルバイトでは最新技術の調査を担当し、社内や案件にて活用することを想定したシステム導入の検証を実施しています。AWS上のLambdaで自作したプライベートなMCPサーバーをDify上で使用する手順について記事にします。" |
| 15 | +--- |
| 16 | +Strategic AI Group/MLOpsチームでアルバイトをしている木村です。アルバイトでは最新技術の調査を担当し、社内や案件にて活用することを想定したシステム導入の検証を実施しています。プライベートではエンジニアにありがちな(?)運動不足解消として、マラソンをしていて、47都道府県制覇を目指しています。 |
| 17 | + |
| 18 | +今回はAWS上のLambdaで自作したプライベートなMCPサーバーをDify上で使用する手順について記事にします。これから社内でもどんどん広がっていくであろうMCPを使っていてとても面白かったので、ぜひ皆さんも本記事を参考に試してみてください! |
| 19 | + |
| 20 | +# 概要 |
| 21 | + |
| 22 | +「社内でLLMに様々なスキルを持たせたい」というのは、多くの企業が抱える要望です。本記事では、AWS Lambda上に自作のプライベートなMCP(Model Context Protocol)サーバーを構築し、それをDifyから呼び出す手順を解説します。 |
| 23 | + |
| 24 | +セキュリティを担保しつつ、誰もが簡単にAIアプリを構築できる環境の「第一歩」を目指します。 |
| 25 | + |
| 26 | +最終的には以下のようにDifyからMCPサーバーにアクセスできます。 |
| 27 | + |
| 28 | +以下のツールではこんにちはに対してHELLOを返すcalculate_helloというmcpツール、計算について、足し算、掛け算、引き算を行うcalculate_add,calculate_product,calculate_subというmcpツールが使われています。 |
| 29 | + |
| 30 | +<img src="/images/2026/20260324a/image.png" alt="image.png" width="844" height="724" loading="lazy"> |
| 31 | + |
| 32 | +これを応用してWeb検索を行ったり、Googleカレンダーに予定を自動で入れることができます。 |
| 33 | + |
| 34 | +# 本記事のキーテクノロジー |
| 35 | + |
| 36 | +## Dify |
| 37 | + |
| 38 | +- [Dify公式サイト](https://dify.ai/jp) |
| 39 | + |
| 40 | +Difyは、LLMアプリを直感的に開発できるオープンソースのLLMアプリ開発プラットフォームです。RAG(検索拡張生成)やエージェント機能を手軽に実装できるのが特徴です。 |
| 41 | + |
| 42 | +本記事では社内ネットワークのEC2でDifyを動かすことでプライベートな構築を実現しています。 |
| 43 | + |
| 44 | +## MCP |
| 45 | + |
| 46 | +Anthropicが公開したMCPは(参考:[Model Context Protocol(MCP)とは](https://zenn.dev/cloud_ace/articles/model-context-protocol))、LLMと外部データ(DB、API、ローカルファイル等)を接続するためのオープンプロトコルです。これまではツールごとに専用の繋ぎ込みが必要でしたが、MCPという「共通規格」を通すことで、一つのMCPサーバーを作るだけで様々なAIクライアントからデータを利用可能になります。似ているものでRAGがありますがRAGは「知識の検索・参照」に特化しているのに対し、MCPは「機能(ツール)の呼び出し・実行」に特化しています。 |
| 47 | + |
| 48 | +<img src="/images/2026/20260324a/image_2.png" alt="image.png" width="709" height="330" loading="lazy"> |
| 49 | + |
| 50 | +## API Gateway+Lambdaによるプライベートな環境 |
| 51 | + |
| 52 | +社内DBや秘匿性の高い情報が流出するリスクを排除するため、社内ネットワーク内での運用が求められるケースがあります。 |
| 53 | + |
| 54 | +今回は上記を実現するべく、API Gatewayのアクセスを社内ネットワークからに制限し、すべてのリソースを社内に置いておくことでプライベートな環境を実現することを目指しました。 |
| 55 | +※前提として、社内ネットワークとAWS環境がVPN接続されていることとします。 |
| 56 | + |
| 57 | +## 通信方式 |
| 58 | + |
| 59 | +MCPには、ローカルMCP(stdio)とリモートMCP(Streamable HTTP)の2種類があります。ローカルMCPの場合、同じ環境を相手のPCにも構築する必要があり、共有に手間がかかってしまうので、リモートMCPを採用しました。 |
| 60 | + |
| 61 | +また、リモートMCPをサーバレスで実行するために通信方式は以下の設定にしています。(参考:[MCPの通信方式](https://qiita.com/nogataka/items/7fbeb58339703ec98a86)) |
| 62 | + |
| 63 | +- stateless_http: stateless_http=True に設定し、本来Statefulな通信を前提とするMCPプロトコルをステートレスなHTTP通信に変換することでLambdaのような1回切りのリクエストに対応させます |
| 64 | +- Streamable_http:Lambdaでの1回限りの重い処理を、タイムアウトで切断される前に小出しで届けて完結させる |
| 65 | + |
| 66 | +# 構成図 |
| 67 | + |
| 68 | +本構成では、セキュリティを最優先し、API Gatewayを「プライベート」モードでデプロイします。これにより、社内ネットワークからのみAIツールを呼び出すことが可能になります。 |
| 69 | + |
| 70 | +<img src="/images/2026/20260324a/image_3.png" alt="image.png" width="696" height="364" loading="lazy"> |
| 71 | + |
| 72 | +## Lambda採用理由 |
| 73 | + |
| 74 | + EC2でMCPサーバをホストした場合、常時EC2を起動しておく必要があり、利用していない期間も不要な料金が発生します。そこでサーバレスなLambdaでホストすることで、MCPサーバーが呼ばれた時だけ料金が発生するので、コストを抑えられます。 |
| 75 | + |
| 76 | +## 本構成のデメリット |
| 77 | + |
| 78 | +大量のログ解析や複雑なデータ集計など、完了までに時間がかかるタスクを依頼すると、AIに結果が返る前にAPI Gatewayのタイムアウト制限より、接続が切れてエラーになる可能性があります。 |
| 79 | + |
| 80 | +また、VPCエンドポイントは月10ドルほどの固定費がかかるというデメリットも挙げられます。 |
| 81 | + |
| 82 | +# 構築手順 |
| 83 | + |
| 84 | +1. まずは開発環境(EC2)にAWS SAM CLIやPython 3.11をインストールし、必要なIAMロールを付与します |
| 85 | +2. コードを作成してAWSにデプロイ |
| 86 | + |
| 87 | +ファイル構成は以下の通りです。 |
| 88 | + |
| 89 | +```sh |
| 90 | +. |
| 91 | +├── mcp_server |
| 92 | +│ ├── __init__.py |
| 93 | +│ └── __main__.py |
| 94 | +├── requirements.txt |
| 95 | +├── run.sh |
| 96 | +└── template.yaml |
| 97 | +``` |
| 98 | + |
| 99 | +mcp_server/`__init__.py` は空ファイルで大丈夫です。 |
| 100 | + |
| 101 | +```bash |
| 102 | +touch mcp_server/'__init__.py' |
| 103 | +``` |
| 104 | + |
| 105 | +```py mcp_server/__main__.py |
| 106 | +import uvicorn |
| 107 | +from fastmcp import FastMCP |
| 108 | + |
| 109 | +# FastMCPの初期化 |
| 110 | +# stateless_http=True にすることで、Lambdaのような1回切りのリクエストに対応させます |
| 111 | +mcp = FastMCP("MyRemoteMCP", stateless_http=True) |
| 112 | + |
| 113 | +# ツール定義:型ヒントとドキュメント文字列がそのままMCPの定義になります |
| 114 | +@mcp.tool() |
| 115 | +def calculate_add(a: float, b: float) -> str: |
| 116 | + """2つの数値を足し合わせます""" |
| 117 | + result = a + b |
| 118 | + return f"計算結果: {a} + {b} = {result}" |
| 119 | + |
| 120 | +# @mcp.tool()でいくつでもツールを追加可能 |
| 121 | + |
| 122 | +# FastMCPが内部で生成したFastAPIアプリを抽出 |
| 123 | +app = mcp.http_app() |
| 124 | + |
| 125 | +if __name__ == "__main__": |
| 126 | + # Lambda Web Adapterが待機する8080ポートで起動 |
| 127 | + uvicorn.run(app, host="0.0.0.0", port=8080) |
| 128 | +``` |
| 129 | + |
| 130 | +```sh run.sh |
| 131 | +#!/bin/bash |
| 132 | +exec python -m mcp_server |
| 133 | +``` |
| 134 | + |
| 135 | +run.shに実行権限を付与。 |
| 136 | + |
| 137 | +```bash |
| 138 | +chmod +x run.sh |
| 139 | +``` |
| 140 | + |
| 141 | +```text requirements.txt |
| 142 | +fastmcp==2.14.5 |
| 143 | +fastapi==0.128.1 |
| 144 | +uvicorn==0.40.0 |
| 145 | +``` |
| 146 | + |
| 147 | +3. AWSリソースの構築 |
| 148 | + |
| 149 | +以下のSam templateを実行すれば本記事のAWSリソースが構築できます。 |
| 150 | +[]内は各々の環境に合わせて変更してください。 |
| 151 | + |
| 152 | +```yaml |
| 153 | +Transform: |
| 154 | + - AWS::Serverless-2016-10-31 |
| 155 | + - AWS::LanguageExtensions |
| 156 | + |
| 157 | +Parameters: |
| 158 | + VpcId: { Type: String, Default: vpc-[VPCID] } |
| 159 | + SubnetId1: { Type: String, Default: subnet-[サブネットID] } |
| 160 | + SubnetId2: { Type: String, Default: subnet-[サブネットID] } |
| 161 | + |
| 162 | +Resources: |
| 163 | + # --- セキュリティグループ --- |
| 164 | + InternalServiceSG: |
| 165 | + Type: AWS::EC2::SecurityGroup |
| 166 | + Properties: |
| 167 | + GroupDescription: Allow internal traffic for MCP Server |
| 168 | + VpcId: !Ref VpcId |
| 169 | + SecurityGroupIngress: |
| 170 | + - IpProtocol: tcp |
| 171 | + FromPort: 443 |
| 172 | + ToPort: 443 |
| 173 | + CidrIp: [CiderIp] |
| 174 | + |
| 175 | + # --- VPCエンドポイント (API Gateway用) --- |
| 176 | + MyVpce: |
| 177 | + Type: AWS::EC2::VPCEndpoint |
| 178 | + Properties: |
| 179 | + ServiceName: !Sub "com.amazonaws.${AWS::Region}.execute-api" |
| 180 | + VpcEndpointType: Interface |
| 181 | + VpcId: !Ref VpcId |
| 182 | + SubnetIds: [ !Ref SubnetId1, !Ref SubnetId2 ] |
| 183 | + SecurityGroupIds: [ !Ref InternalServiceSG ] |
| 184 | + PrivateDnsEnabled: true |
| 185 | + |
| 186 | + # --- API Gateway (Private) --- |
| 187 | + MyApi: |
| 188 | + Type: AWS::Serverless::Api |
| 189 | + DependsOn: MyVpce |
| 190 | + Properties: |
| 191 | + Name: mcp-api |
| 192 | + StageName: prod |
| 193 | + EndpointConfiguration: |
| 194 | + Type: PRIVATE |
| 195 | + VPCEndpointIds: |
| 196 | + - !Ref MyVpce |
| 197 | + Auth: |
| 198 | + ResourcePolicy: |
| 199 | + CustomStatements: |
| 200 | + - Effect: Allow |
| 201 | + Principal: "*" |
| 202 | + Action: "execute-api:Invoke" |
| 203 | + Resource: "execute-api:/*/*/*" |
| 204 | + - Effect: Deny |
| 205 | + Principal: "*" |
| 206 | + Action: "execute-api:Invoke" |
| 207 | + Resource: "execute-api:/*/*/*" |
| 208 | + Condition: |
| 209 | + StringNotEquals: |
| 210 | + "aws:SourceVpce": !Ref MyVpce |
| 211 | + DefinitionBody: |
| 212 | + openapi: "3.0.1" |
| 213 | + paths: |
| 214 | + /{proxy+}: |
| 215 | + x-amazon-apigateway-any-method: |
| 216 | + x-amazon-apigateway-integration: |
| 217 | + httpMethod: POST |
| 218 | + type: aws_proxy |
| 219 | + uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" |
| 220 | + responses: {} |
| 221 | + |
| 222 | + # --- Lambda関数(VPC内) --- |
| 223 | + Function: |
| 224 | + Type: AWS::Serverless::Function |
| 225 | + Properties: |
| 226 | + Architectures: [arm64] |
| 227 | + Runtime: python3.11 |
| 228 | + Timeout: 30 |
| 229 | + CodeUri: . |
| 230 | + Handler: run.sh |
| 231 | + Layers: |
| 232 | + - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:18 |
| 233 | + VpcConfig: |
| 234 | + SecurityGroupIds: [ !Ref InternalServiceSG ] |
| 235 | + SubnetIds: [ !Ref SubnetId1, !Ref SubnetId2 ] |
| 236 | + Environment: |
| 237 | + Variables: |
| 238 | + AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap |
| 239 | + PORT: 8080 |
| 240 | + Events: |
| 241 | + ProxyApiRoot: |
| 242 | + Type: Api |
| 243 | + Properties: |
| 244 | + RestApiId: !Ref MyApi |
| 245 | + Path: /{proxy+} |
| 246 | + Method: ANY |
| 247 | + |
| 248 | + # --- API GatewayからLambdaを呼ぶ許可 --- |
| 249 | + LambdaInvokePermission: |
| 250 | + Type: AWS::Lambda::Permission |
| 251 | + Properties: |
| 252 | + Action: lambda:InvokeFunction |
| 253 | + FunctionName: !Ref Function |
| 254 | + Principal: apigateway.amazonaws.com |
| 255 | + SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/*" |
| 256 | + |
| 257 | +Outputs: |
| 258 | + FunctionArn: |
| 259 | + Description: "Lambda Function ARN" |
| 260 | + Value: !GetAtt Function.Arn |
| 261 | + VpceId: |
| 262 | + Description: "VPC Endpoint ID" |
| 263 | + Value: !Ref MyVpce |
| 264 | + ApiUrl: |
| 265 | + Description: "API Gateway URL" |
| 266 | + Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" |
| 267 | + |
| 268 | +``` |
| 269 | + |
| 270 | +本構成で重要な部分をピックアップして解説します。 |
| 271 | + |
| 272 | +API Gateway: |
| 273 | + |
| 274 | +- エンドポイントタイプ:プライベート |
| 275 | +- VPCエンドポイントID:[エンドポイントID(vpce-xxxx)] |
| 276 | +- リソースポリシー:特定のVPCエンドポイント経由のアクセスのみ許可 |
| 277 | +- 統合タイプ:Lambda関数 |
| 278 | +- Lambdaプロキシ統合:ON |
| 279 | +- Lambda関数:SAMで作った関数の名前(mcp-server-stack-Function-xxx) |
| 280 | +- デプロイの際のステージ名:prod |
| 281 | + |
| 282 | +Lambda: |
| 283 | + |
| 284 | +- LambdaをVPCのプライベートサブネットを指定することで、インターネットからは直接見えない状態に設定 |
| 285 | + |
| 286 | +VPCエンドポイント: |
| 287 | + |
| 288 | +- VPCEndpoint(Interface型)の採用によりAPI Gatewayをインターネットに公開せず、VPC内部のプライベートIPだけで叩けるようにします。 |
| 289 | + |
| 290 | +## Difyからの接続 |
| 291 | + |
| 292 | +Difyのツール>MCPからツールを追加します。 |
| 293 | + |
| 294 | +- サーバー名:https://[自分のVPCEのDNS名(上から2つ目)]/prod/mcp |
| 295 | +- ヘッダー名:HOST |
| 296 | +- ヘッダーの値:[自分のAPIのID].execute-api.ap-northeast-1.amazonaws.com |
| 297 | + |
| 298 | +<img src="/images/2026/20260324a/image_5.png" alt="image.png" width="546" height="876" loading="lazy"> |
| 299 | + |
| 300 | +# 利用結果 |
| 301 | + |
| 302 | +Difyのツール設定から外部MCPツールを登録し、実際に計算を依頼した結果です。 |
| 303 | + |
| 304 | +<img src="/images/2026/20260324a/image_6.png" alt="image.png" width="844" height="724" loading="lazy"> |
| 305 | + |
| 306 | +VPCエンドポイント経由の閉域網通信でありながら、Difyのエージェント機能によってmcpツールが呼び出されていることが確認できました。 |
| 307 | + |
| 308 | +# まとめ |
| 309 | + |
| 310 | +- したことまとめ |
| 311 | + - AWS Lambda + API Gateway (Private) によるセキュアでサーバレスなMCPサーバーの構築 |
| 312 | + - Dify との連携による実用的なAIエージェント環境の構築 |
| 313 | +- 次やりたいこと |
| 314 | + - 今回は数値計算のシンプルなツールでしたが、次はGoogleカレンダーAPIとの連携によるスケジュール調整の自動化に挑戦し、最終的には秘書のようなAIを作りたいと考えています |
0 commit comments