
社内の複数利用者が同じMCPサーバーとツールを使っても、見てよいデータは人ごとに違います。ローカルMCPと共有HTTP MCPの違いから始め、Platformatic MCP v1.2.1とv2.4.0で偽装ヘッダー、JWT由来の認証文脈、権限なしの一覧・直接実行、プロキシ設定を実測しました。requestを通信の手掛かり、authContextを認可の材料として分ける設計を説明します。
まずMCPサーバーとは何か
MCPは、AIアプリと外部の道具をつなぐ共通の約束です。AIが会話だけで答えられないとき、顧客データを検索する、社内文書を読む、経費申請を作る、といった処理をMCPの「ツール」として呼び出せます。1214
登場人物は三つに分けると分かりやすくなります。
| 役割 | 具体例 | 担当すること |
|---|---|---|
| Host | デスクトップAI、社内チャット | 利用者と会話する |
| Client | Host内のMCP接続部分 | サーバーへ要求を送る |
| Server | CRMや社内DBの前に置くプログラム | ツールを公開し、外部システムを操作する |
たとえば利用者が「A社の直近の商談をまとめて」と頼むと、AIは customer_search を選び、company: "A社" のような引数を送ります。MCPサーバーはCRMを検索し、結果をAIへ返します。MCPサーバー自体がAIとは限りません。AIから安全に呼べるよう、既存のAPIやデータベースを包む入口だと考えると理解しやすいでしょう。
一人で使うMCPと共有するMCPは何が違うか
最初にMCPを試すときは、AIアプリが同じ端末でMCPサーバーを子プロセスとして起動する stdio 構成がよく使われます。利用者ごとに別のプロセスが動き、その人の端末にある設定や認証情報を使います。接続の範囲が狭いため、「誰の処理か」は端末やプロセスの境界に隠れがちです。1214
社内へ広げると事情が変わります。全員のPCへ同じサーバーを入れて更新するより、会社のネットワーク上に一つの遠隔MCPサーバーを置き、共通のURLへ接続させた方が管理しやすいからです。現在の標準的な遠隔通信はStreamable HTTPで、一つの独立したサーバープロセスが複数クライアントの要求を扱えます。1215
共有が起きるのは、たとえば次のような場面です。
| 共有するツール | 利用者 | 人ごとに変えるもの |
|---|---|---|
| CRM検索 | 営業担当 | 担当顧客、所属部門 |
| 請求書検索 | 経理担当 | 法人、閲覧権限 |
| 障害チケット更新 | サポート担当 | 担当案件、書き込み権限 |
| デプロイ操作 | 開発者 | 対象環境、承認済み操作 |
ここで共有するのは、URL、サーバーのコード、ツール定義です。利用者の権限や、見せてよいデータまで共有するわけではありません。同じ改札を通っても、持っている切符によって入れる場所が違うのと同じです。

同じURLで取り違えが起きる理由
一つの /mcp には、営業Aの要求の直後に営業Bの要求が届きます。サーバーが現在の利用者をグローバル変数へ保存したり、前回のテナントIDを使い回したりすれば、次の要求へ情報が漏れます。利用者の特定は接続時に一度だけ済ませるのではなく、原則として要求ごとに行う必要があります。
もう一つの落とし穴が、呼び出し側の自己申告を信じることです。次のような値はいずれも要求へ書けます。
x-tenant-id: customer-b
MCP arguments: { "tenantId": "customer-b" }
これらは「customer-bを見たい」という希望には使えます。しかし、「この人はcustomer-bを見てよい」という証明にはなりません。AIが誤った値を組み立てる場合も、利用者が開発者ツールから直接書き換える場合もあります。信頼境界を越えてきた値を、そのまま権限へ昇格させるところで取り違えが起きます。
遠隔MCPの認証仕様はHTTPの認可をOAuth系の仕組みで扱い、アクセストークンの署名だけでなく、有効期限と対象サーバーを検証するよう求めています。別サービス向けのトークンを受け流す「トークン・パススルー」も禁止事項です。13 サーバー側で確認した身元と権限は、呼び出し側が自由に書ける値から分けます。
MCPの引数とHTTPの情報は別の層にある
一回のツール呼び出しは、入れ子の三層として届きます。
| 層 | 例 | 役割 | 信頼の仕方 |
|---|---|---|---|
| MCPの引数 | query、limit、tenantId | AIがツールへ頼む仕事 | 業務入力として検証する |
| HTTP要求 | URL、ヘッダー、IP、Origin | その仕事を運んだ通信 | 原則は未検証の入力 |
| 認証文脈 | 利用者ID、権限、client ID | トークン検証後に確定した事実 | 認可の材料にする |
Platformatic MCPは、Node.jsのWebフレームワークFastify上でMCPサーバーを作るためのプラグインです。Platformatic MCP v1.2.0は2025年8月26日、Fastifyの request と reply をツール、リソース、プロンプトのハンドラへ渡す機能を加えました。翌27日のv1.2.1はビルド修正版です。12
request からは要求URL、ヘッダー、接続元などを読めます。reply は応答ヘッダーや状態を返すためのものです。この機能で、要求IDをログへつないだり、利用した処理版を応答へ載せたりできます。ただし、HTTPの情報へアクセスできることと、その値が本人確認済みであることは別です。910
まず固定版の全265テストを通した
機能が追加された時点を確認するため、公式リポジトリをv1.2.1、コミット 6d012761 へ固定。対象機能だけのテストは7スイート11件で、すべて成功しました。4
| 確認したこと | 結果 |
|---|---|
| URL、任意ヘッダー、クエリ | ハンドラから取得できた |
| 応答ヘッダー | reply から設定できた |
| 従来の1引数ハンドラ | 変更せず成功した |
| 通信経路 | 通常のPOSTとSSE互換設定で成功した |
| 対象 | ツール、リソース、プロンプトで成功した |
前回はRedisを起動せず、全体265件のうち10件が失敗。そこでRedis 8.2.1を一時ディレクトリへソースから構築し、127.0.0.1:6379 だけで再実行しました。結果は69スイート、265テスト、失敗0件。テスト後のRedisは停止済みです。
ここでいうSSEのテストは、外部クライアントが長時間のGETストリームを維持する相互運用試験ではありません。Fastifyの app.inject でSSE互換設定のMCPエンドポイントへPOSTした付属テストです。確認できた範囲を広げすぎないよう、この点は分けておきます。
偽の組織ヘッダーを入れてみた
次に、署名済みJWTの利用者を verified-user-42 とし、同じ要求へ偽装ヘッダーを追加しました。
Authorization: Bearer <署名済みJWT>
x-tenant-id: forged-admin-tenant
v1.2.1では、生の x-tenant-id をそのまま読めます。一方、検証済みJWTの sub は request.tokenPayload.sub に verified-user-42 として残りました。この単純なHTTP経路では authContext が作られません。古い版で認可を書くなら「文脈オブジェクトがあるはず」と思い込まず、実際に使う認証経路で検証結果の格納先を確かめる必要があります。
同じ実験をv2.4.0、コミット 75937d3c で行うと、結果は次のようになりました。
{
"rawTenantHeader": "forged-admin-tenant",
"verifiedUserId": "verified-user-42",
"verifiedScopes": ["tenant:read"]
}
生の文字列は消えません。ただし、署名検証から作られた authContext.userId と authContext.scopes は別に保たれました。ツールへ「どの組織を表示したいか」という入力を渡すことと、「その利用者がどの組織を読めるか」を確定することは、別の仕事です。
現行版は一覧と実行の両方を絞れる
v2.4.0には canAccessTool があり、ツール一覧の取得と実行の両方で、要求ごとに利用可否を判定できます。6 権限のないツールは一覧から省かれ、直接名前を指定して呼ばれても、存在しないツールと同じ形で応答します。
今回は独自テストをもう一段増やしました。最初の要求には tenant:read 権限を持つJWTと偽装ヘッダーを送り、ツール実行まで確認。次の要求では同じ偽装ヘッダーのまま、権限を profile だけに変更しました。
| 要求 | x-tenant-id | 検証済み権限 | 一覧 | 直接実行 | ハンドラ実行回数 |
|---|---|---|---|---|---|
| 1 | forged-admin-tenant | tenant:read | 表示 | 成功 | 1 |
| 2 | forged-admin-tenant | profile | 非表示 | -32601 | 1のまま |
二つ目は Tool 'tenant-report' not found となり、ハンドラは呼ばれませんでした。偽装ヘッダーが同じでも、許可を決めたのは検証済み権限です。独自テスト1件はv2.4.0上で成功しました。
実装の芯は次のようになります。
await app.register(mcpPlugin, {
authorization: authorizationConfig,
allowedOrigins: ["https://ai.example.jp"],
canAccessTool: (toolName, { authContext }) => {
if (toolName !== "tenant-report") return true
return authContext?.scopes.includes("tenant:read") === true
}
})
実際の業務では、権限名だけで終わらせず、トークンの利用者IDからサーバー側で所属組織を引き、MCP引数で指定された組織がその範囲に入るかを照合します。ヘッダーの組織IDは表示先の候補にはなっても、許可の根拠にはしません。
Originの許可リストも別に必要です。v2.4.0の付属テストでは、不許可のOriginは403、許可したOriginは成功しました。7 認証が「誰か」を確かめ、認可が「何をしてよいか」を決め、Origin検証が「どのWeb画面から来たか」を絞ります。三つは似て見えても役割が違います。

プロキシを信じる範囲も狭くする
HTTPの request.ip、host、protocol なら信頼できる、とも限りません。Fastifyは trustProxy を有効にすると、X-Forwarded-For、X-Forwarded-Host、X-Forwarded-Proto を解釈します。公式文書も、転送ヘッダーはプロキシが確実に上書きする構成で使うよう注意しています。911
Fastify 5.6.1へ偽の転送ヘッダーを送り、設定を変えて比べました。
trustProxy | request.ip | host | protocol |
|---|---|---|---|
false | 127.0.0.1 | localhost:80 | http |
true | 203.0.113.9 | evil.example | https |
"127.0.0.1" | 203.0.113.9 | evil.example | https |
今回は接続元が許可した 127.0.0.1 なので、文字列指定でも転送値が採用されました。この結果から分かるのは、true が危険で文字列なら常に安全、ということではありません。最後に接続するプロキシのIPやCIDRを限定し、そのプロキシが外部から来た転送ヘッダーを削除して付け直すところまでが一組です。IPやhostを認可の主根拠にしない方が安全です。
全体テストで一件だけずれた理由
v2.4.0の全体テストは2回実行し、どちらも512件中511件成功、1件失敗でした。失敗したのは権限を拒否したツールの実行完了イベントです。通信上は存在しないツールと同じ応答になりますが、監視用イベントの内部結果が not-found ではなく access-denied になり、タグ時点のテスト期待値と一致しませんでした。
これは、拒否したツールが実行されたという失敗ではありません。v2.4.0より後のmain、コミット f4ed3dba では、外部応答に合わせて監視イベントも not-found へ正規化する変更が入っています。8 固定タグのテスト不一致と、認可をすり抜けたかどうかは別問題です。
ツールへ渡す値を三種類に分ける
実装レビューで使った色分けは、次の三種類です。
| 種類 | 例 | 扱い |
|---|---|---|
| 生の入力 | ヘッダー、クエリ、MCP引数 | 形式と意味を検証する |
| 通信上の手掛かり | 要求ID、接続元IP、Origin | 追跡や入口の制限に使う |
| 検証済みの事実 | 利用者ID、権限、所属 | 認証・認可層だけで確定する |
request と reply が使える利点は大きい。要求IDをログへつなぎ、処理版を応答ヘッダーで返し、呼び出し元ごとの監査記録を残せます。910 ただし、各ツールがCookie、キャッシュ、転送ヘッダーを好きに操作し始めると、HTTP層の方針が分散しかねません。利用者情報をグローバル変数に置かない、認証文脈を要求単位で作る、ツールから返せるヘッダー名を追跡用途へ限定する、といった小さな制約が有効です。
共有MCPの設計で筆者が最初に書くようになった問いは、「何を共有するのか」。共有対象はサーバーとツールであり、利用者の権限ではありません。この一文が決まると、HTTPヘッダーは入力、検証済みトークンは身元、canAccessTool は許可、という役割が自然に分かれます。便利な request を隠すのではなく、生の入力だと分かる名前のまま残す。その上で、誰のデータを触ってよいかを一か所で見直せる認証文脈を置くことが、複数利用者で安全に共有するための土台になります。
参照リンク15件
- Platformatic@platformatic/mcp v1.2.0 release
- Platformatic@platformatic/mcp v1.2.1 release
- Platformatictool-context-access.test.ts at v1.2.1
- Platformatic@platformatic/mcp v2.4.0
- Platformaticauth-context-propagation.test.ts at v2.4.0
- Platformatictool-authorization.test.ts at v2.4.0
- Platformaticprotocol-negotiation.test.ts at v2.4.0
- Platformatichandlers.ts after v2.4.0
- FastifyRequest reference
- FastifyReply reference
- Microsoft LearnMCPツールの使用
- FastifyServer reference trustProxy
- Model Context ProtocolTransports
- Model Context ProtocolAuthorization
- Microsoft LearnAzure API ManagementでMCPサーバーを公開する
2026年8月29日時点の公式資料を確認しています。仕様は更新される可能性があります。



