本記事は、このテーマを段階的に育てていくシリーズの 第1回(お試し検証) です。技術的に動くところまでを確認した段階の記録で、作り込みはこれからです。
TL;DR
- GitHub公式の
github/github-mcp-serverを コードに一切手を入れず、AWS Bedrock AgentCore Runtime にホストして「リモートMCPサーバ」として動かしてみた。まずはお試し検証の段階。 - 理想は、Entra IDなどの認証基盤によるSSOをインバウンド認証に使い、ユーザーが個別にPATを送らずに使える形。今回はMVPとして「各自がPAT持参」にしているが、そこは将来差し替える前提。
- 一番の工夫は 認証の二層化。AgentCoreへの入口は SigV4(IAM)、GitHubへは 各開発者のPATを持参 という構成。SigV4が
Authorizationヘッダを使ってしまうので、PATは別ヘッダX-Github-Tokenで運び、コンテナ内のnginxがAuthorization: Bearerに詰め替えるというトリックで解決した。 - クライアント(IDEやエージェント)はSigV4署名が必要なので、ローカル署名ブリッジ(stdio ↔ InvokeAgentRuntime) を1個用意して解決。
- 「共有トークンを1本サーバに埋め込む」設計を避けたので、Confused Deputy(混乱した代理)を構造的に回避できている。
- ハマりどころ(400 "badly formatted" / 406 / Hostヘッダ検証 / supervisordのABI不整合)も全部書いた。
はじめに
「GitHub操作をAIエージェントから行いたい」というニーズは、これから増えていくのではないか——という仮説を持っています。GitHubは公式のMCPサーバ github/github-mcp-server を出していますが、これは基本ローカル(stdio)で各自が動かす前提。各利用者が個別にセットアップするのは手間ですし、バージョンもバラバラになります。
そこで 「1か所にホストして、複数の利用者が共通のエンドポイントから使える」リモートMCPサーバ(例えばチームや社内での共通利用など)を、その 第一歩(お試し検証) として AWS Bedrock AgentCore Runtime 上に構築してみました。本記事はその設計と、ハマった点・工夫した点の記録です。
環境: リージョン
ap-northeast-1、AWSアカウントは伏せ字(123456789012)で記載します。
前提: AgentCore Runtime は MCP サーバをホストできる
AWS Bedrock AgentCore Runtime は、エージェントだけでなく MCPサーバをそのままホストできるサーバレス実行環境です。MCPをホストする場合の「契約」は明確で:
- トランスポートは Streamable-HTTP 必須(原則ステートレス)
- コンテナは ARM64、
0.0.0.0:8000で待ち受け、POST /mcpを公開 - セッションは
Mcp-Session-Idヘッダでプラットフォームが管理 - インバウンド認証は SigV4(IAM) または OAuth/JWT を選択
(参考: Deploy MCP servers in AgentCore Runtime / MCP protocol contract)
一方、github-mcp-server は http サブコマンドで Streamable HTTP モードを持ちますが、既定ポートは8082、GitHubトークンは リクエストごとに Authorization: Bearer <token> で受け取ります。
この2つの仕様の「ズレ」を埋めるのが、今回の工夫のほとんどです。
全体アーキテクチャ
開発者のIDE / AIエージェント
│ ローカル署名ブリッジ
│ - 開発者のIAM資格でSigV4署名
│ - 開発者のGitHub PATを X-Github-Token ヘッダに付与
▼ InvokeAgentRuntime (SigV4 / HTTPS)
AgentCore Runtime ── ARM64 コンテナ ───────────────────────┐
│ - requestHeaderAllowlist: ["X-Github-Token"] │
│ - Mcp-Session-Id を自動付与 │
▼ │
nginx (0.0.0.0:8000, /mcp) │
│ X-Github-Token → Authorization: Bearer に詰め替え │
▼ │
github-mcp-server (httpモード, 127.0.0.1:8082) ────────────┘
│ Authorization: Bearer <PAT>
▼
GitHub REST / GraphQL API
ポイントは コンテナの中身が「公式MCPサーバ(無改変) + nginxラッパー」 であること。MCPサーバ本体のコードはビルドし直しただけで、変換ロジックはすべて**外側(nginx)**に置きました。
工夫① 公式MCPを「無改変」で載せる(nginxラッパー方式)
AgentCoreの契約(0.0.0.0:8000 / POST /mcp)と、github-mcp-serverの実装(:8082 / Authorization: Bearer)が合いません。本体をパッチするのは保守が辛いので、nginxを前段に置いて吸収しました。
# 1. 公式ソースから arm64 バイナリをビルド(無改変)
FROM golang:1.25-alpine AS build
RUN apk add --no-cache git
RUN git clone --depth 1 https://github.com/github/github-mcp-server /src
WORKDIR /src
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -ldflags="-s -w" -o /out/github-mcp-server ./cmd/github-mcp-server
# 2. nginx + バイナリ + 起動スクリプト
FROM nginx:1.27-alpine
COPY --from=build /out/github-mcp-server /server/github-mcp-server
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8000
CMD ["/entrypoint.sh"]
upstreamを更新したくなっても、本体は触っていないので追従が楽です。
工夫② 認証の二層化と「Authorizationヘッダの取り合い」
ここが本記事の核心です。認証が 二層 あります。
| 層 | 何を認証するか | 方式 |
|---|---|---|
| インバウンド | 開発者 → AgentCore | SigV4 (IAM) |
| アウトバウンド | MCPサーバ → GitHub | 各開発者のPAT |
インバウンドにSigV4を選んだのは、まずお試しで動かすのに手軽だったからです。理想は後述のとおり認証基盤によるSSOですが、検証段階では「利用者がすでにAWS資格情報(IAM)を持っている」ことを利用すれば、新しいログイン基盤を用意せずにすぐ試せます。そこで第一歩としてSigV4を使いました。
ところが問題が。SigV4は署名を Authorization ヘッダに入れます。一方 github-mcp-server は GitHubトークンを 同じ Authorization ヘッダで受け取りたい。1本のヘッダを2つの用途が取り合うのです。
解決: PATは別ヘッダで運び、nginxで詰め替える
AgentCore Runtime は カスタムHTTPヘッダの通過を許可リストで明示すれば転送できます(最大20個・各4KB・x-amz-/x-amzn- 接頭辞は不可)。(Pass custom headers)
そこで:
- クライアントはPATを
X-Github-Tokenという独自ヘッダで送る(SigV4のAuthorizationとは別物) - AgentCoreの
requestHeaderAllowlistでX-Github-Tokenを通す - コンテナ内のnginxが
X-Github-Token→Authorization: Bearerに詰め替えて github-mcp-server に渡す
nginxの該当部分:
location /mcp {
proxy_pass http://127.0.0.1:8082;
# 開発者が送った X-Github-Token を、github-mcp-server が求める
# Authorization: Bearer に詰め替える
proxy_set_header Authorization "Bearer $http_x_github_token";
# Streamable HTTP / SSE 対応
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
}
ランタイム作成時(boto3)の該当部分:
client.create_agent_runtime(
agentRuntimeName="github_mcp_server",
roleArn=ROLE_ARN,
networkConfiguration={"networkMode": "PUBLIC"},
agentRuntimeArtifact={"containerConfiguration": {"containerUri": IMAGE_URI}},
protocolConfiguration={"serverProtocol": "MCP"}, # MCPプロトコル
requestHeaderConfiguration={ # ← カスタムヘッダ許可
"requestHeaderAllowlist": ["X-Github-Token"]
},
# authorizerConfiguration を指定しない = SigV4(IAM) インバウンド
)
この「サーバに共有トークンを埋め込まず、各リクエストが各自のトークンを運ぶ」設計には、もう一つ大きな利点があります(後述のConfused Deputy)。
工夫③ クライアント側: ローカル署名ブリッジ
SigV4インバウンドには弱点があります。リクエストごとにAWS署名が必要なので、VS CodeやClaude等の標準的なMCPクライアントからは直接繋げません(彼らはSigV4を喋れない)。
そこで、各開発者のマシンで動く stdio ↔ InvokeAgentRuntime のローカル署名ブリッジ を用意しました。MCPクライアントから見ると普通のstdio MCPサーバですが、中で:
- 開発者のIAM資格で SigV4署名(boto3が担当)
- 開発者のPATを
X-Github-Tokenヘッダに注入 -
InvokeAgentRuntimeでAgentCoreに転送
ブリッジの心臓部(抜粋):
import boto3
client = boto3.client("bedrock-agentcore", region_name=REGION)
# 送信直前のフックで PAT を X-Github-Token に注入
client.meta.events.register_first(
"before-sign.bedrock-agentcore.InvokeAgentRuntime",
lambda request, **kw: request.headers.add_header("X-Github-Token", PAT),
)
def forward(message: dict):
resp = client.invoke_agent_runtime(
agentRuntimeArn=ARN,
runtimeSessionId=RUNTIME_SESSION_ID, # microVM固定
mcpProtocolVersion="2025-03-26",
contentType="application/json",
accept="application/json, text/event-stream", # ← これが無いと406
mcpSessionId=mcp_session_id, # MCPセッション継続
payload=json.dumps(message).encode(), # MCPメッセージを素通し
)
# 応答(SSE/JSON)を取り出してstdoutへ返す
これをMCPクライアントの設定に登録すれば、「IDEから普通にMCPツールを呼ぶ」だけで、裏でSigV4署名+PAT注入+リモート転送が走ります。tools/list で公式の43ツールがそのまま見えました。
ちなみにこのブリッジは「MCPの中継器(プロキシ)」で、IDEから見ればstdio MCPサーバ、AgentCoreから見ればHTTP MCPクライアント、という二役。MCPがトランスポート非依存だからこそ成立します。
ハマりどころ集
① 400 "Authorization header is badly formatted"
疎通テストで適当なダミートークン(dummy)を送ったら400。一瞬「配線ミス?」と焦りましたが、github-mcp-serverがトークンの形式を検証していて、ghp_/github_pat_ 形式でない文字列を弾いていただけでした。
逆に言うと、この400が返ってきた時点で「X-Github-TokenがAgentCoreを通過し、nginxで詰め替えられ、github-mcp-serverまで届いてパースされた」証拠。形式の正しい(無効でもよい)トークンを送ると initialize は200、get_me はGitHubまで到達して 401 Bad credentials が返りました。配線は全部正しかったわけです。
② 406 Not Acceptable
InvokeAgentRuntime で accept を指定しないと406。MCPのStreamable HTTPは応答をJSONまたはSSEで返すため、クライアントは application/json, text/event-stream の両方を受理する宣言が必須でした。
③ "invalid Host header"
nginxからupstreamへ流すと invalid Host header "ghmcp" で400。github-mcp-serverが DNSリバインディング対策でHostヘッダを検証しているためでした。nginxでループバックを明示して解決:
proxy_set_header Host "127.0.0.1:8082";
④ supervisord が起動しない(ABI不整合)
1コンテナで「nginx + github-mcp-server」の2プロセスを動かすのに最初supervisordを使ったら、nginx:alpine のsupervisorパッケージが XML_SetAllocTrackerActivationThreshold: symbol not found でクラッシュ。supervisordが依存するPythonのC拡張(pyexpat)が、イメージ内のlibexpatと ABI(Application Binary Interface=関数シンボルや構造体配置といったバイナリ互換の約束事)が食い違い、必要なシンボルが見つからなかったのが原因です。依存ゼロのPOSIXシェル製entrypointに置き換えて回避しました。
#!/bin/sh
set -eu
/server/github-mcp-server http & # :8082
nginx -g 'daemon off;' & # :8000
# どちらかが死んだらexit → AgentCoreがmicroVMを作り直す
while kill -0 "$!" 2>/dev/null; do sleep 1; done
exit 1
動作確認: Strandsエージェントから使う
仕上げに、AWS Strands Agents + Bedrock(Claude Sonnet) で「issue番号を渡すとタイトルと要約を返す」テストエージェントを作り、上記ブリッジ経由でリモートMCPを叩かせました。
from mcp import StdioServerParameters, stdio_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
mcp = MCPClient(lambda: stdio_client(StdioServerParameters(
command=PYTHON, args=[BRIDGE_PATH], env={...}))) # ブリッジをstdio MCPとして起動
model = BedrockModel(model_id="apac.anthropic.claude-sonnet-4-...", region_name="ap-northeast-1")
with mcp:
agent = Agent(model=model, tools=mcp.list_tools_sync())
print(agent("Read issue #1 in github/github-mcp-server and summarize it."))
結果:
Title: Port CLI Server
Summary: 既存のCLI MCPサーバーをGitHub公式プロジェクトに移植する提案。...
ローカルのエージェント → Bedrock → ブリッジ → AgentCore → github-mcp-server → GitHub が一気通貫で通りました。
セキュリティ設計: Confused Deputy を構造的に避ける
「共通MCPサーバ」と聞くと、サーバに強力なトークンを1本埋め込んで全員で共有したくなります。しかしこれは典型的な Confused Deputy(混乱した代理) で、権限の弱い利用者がサーバの強い権限を借りられてしまう。
今回の設計では:
-
コンテナにGitHubトークンを一切埋め込まない(起動コマンドは
github-mcp-server httpのみ) - トークンはリクエストごとに各開発者が持参(
X-Github-Token) - サーバは受け取ったトークンをその場で使うだけ(保存もログ出力もしない)
これにより Aさんの操作はAさんのGitHub権限、Bさんの操作はBさんの権限となり、監査も各自に紐づきます。ゲートも二重(① AWS IAMで「誰が呼べるか」、② PATで「GitHubで誰として動くか」)です。
今後の展望(=本当に目指している姿)
今回はあくまで第一歩。本命の到達点は、次の組み合わせで「ユーザーが個別にPATを扱わなくて済む」ようにすることです。
- インバウンドをSSOに: SigV4をやめてOAuth/JWTにし、Entra IDなどの認証基盤(IdP)によるSSOで認証する。利用者は普段使っているアカウントでログインするだけで、AWS資格もローカル署名ブリッジも不要になる。
- GitHub側を OAuth(3LO) に昇格: AgentCore Identityで「GitHub連携」ボタン方式に。ユーザーが個別にPATを発行・送信する必要がなくなる(今回のMVP最大の手間を解消)。
この2つが入って初めて、本来やりたかった「誰でも手軽に使える共通基盤」に近づきます。さらに用途次第で:
- AgentCore Gateway: 複数のMCP/APIを1つのマネージドな入口に集約したい場合。
- API Gateway + Lambda: MCPをそのまま外出しするのではなく、「issue番号→要約」のような独自REST APIに隠蔽したい場合のファサードとして有効。
うれしいことに、いずれも コンテナ本体(公式MCP + nginx)はそのままで、認証の入口や前段だけを差し替え/追加する形で発展できます。この続きは本シリーズで順に書いていく予定です。
まとめ
- 公式MCPサーバを無改変でAgentCoreに載せ、足りない部分(ポート・認証ヘッダ・Host検証)はnginxラッパーで吸収した。
- 認証は二層。SigV4が
Authorizationを使う制約を、X-Github-Tokenの許可リスト通過 + nginx詰め替えで回避。 - SigV4を喋れないクライアント向けにローカル署名ブリッジを用意。
- 共有トークンを埋め込まないことでConfused Deputyを構造的に回避し、各自の権限・監査を維持。
- 400/406/Host/supervisordのハマりも、原理を理解すれば全部腹落ちする。
「既存のMCPサーバを、手を入れずに、認証付きで、リモートサービスにする」——その第一歩としては、AgentCoreはかなり素直に使えました。ただし本当に使いやすい形(SSO認証・PATレス)はこれから。本シリーズで続きを書いていきます。同じことをやりたい方の参考になれば幸いです。