0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub公式MCPサーバを「無改変のまま」AWS Bedrock AgentCoreに載せてみた

0
Posted at

本記事は、このテーマを段階的に育てていくシリーズの 第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 必須(原則ステートレス)
  • コンテナは ARM640.0.0.0:8000 で待ち受け、POST /mcp を公開
  • セッションは Mcp-Session-Id ヘッダでプラットフォームが管理
  • インバウンド認証は SigV4(IAM) または OAuth/JWT を選択

(参考: Deploy MCP servers in AgentCore Runtime / MCP protocol contract)

一方、github-mcp-serverhttp サブコマンドで 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)

そこで:

  1. クライアントはPATを X-Github-Token という独自ヘッダで送る(SigV4のAuthorizationとは別物)
  2. AgentCoreの requestHeaderAllowlistX-Github-Token を通す
  3. コンテナ内のnginxが X-Github-TokenAuthorization: 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

InvokeAgentRuntimeaccept を指定しないと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レス)はこれから。本シリーズで続きを書いていきます。同じことをやりたい方の参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?