MCP serverを作っていると、「server側で少しLLMに考えさせたい」という場面があります。検索結果を要約したい、候補名を生成したい、toolの途中で分類だけモデルに任せたい。そこで旧仕様の sampling/createMessage を使えば、serverがclientのLLMへcompletionを依頼できます。
ただし、2026-07-28以降のMCPでは、この選択は慎重に扱うべきです。Samplingはdeprecatedになり、新規実装では採用しない方針が示されています。既存実装も、server-to-client requestをそのまま維持するのではなく、MRTRの input_required 経由で互換させるか、server自身が明示的にmodel provider APIを呼ぶ形へ移す判断が必要です。
この記事では、MCP samplingを新しく増やさず、旧 sampling/createMessage 実装を安全に畳む設計だけを扱います。MCP 2026-07-28全体の移行順は MCP 2026-07-28 移行ガイド、input_required / requestState の往復処理は MCP MRTR 設計ガイド、prompt injectionの入口整理は MCP prompt injection 対策ガイド、tool resultの返し方は MCP structuredContent 設計ガイド に分けます。
先に結論: samplingは便利な裏口ではなく、権限付きmodel call
私なら、2026-07-28以降のMCP serverでは次の方針にします。
| 状況 | 判断 |
|---|---|
| 新規serverでLLMが必要 | sampling/createMessage ではなく、serverまたはhostが明示的なmodel callを持つ |
| 旧serverがsamplingを使っている | まず利用箇所、prompt内容、includeContext、承認、ログを棚卸しする |
| client互換が必要 | MRTRの input_required 内でsampling requestを扱う互換層に限定する |
| 外部コンテンツを要約する | read-only sessionで処理し、write toolと同居させない |
| tool loop付きsampling | 原則やめる。必要なら別agent workflowとして設計する |
Samplingは「serverがclientのモデルを借りる」仕組みです。つまり、serverにAPI keyを置かなくて済む一方で、client側のmodel access、context、承認UI、監査ログ、prompt injection対策を巻き込みます。
ここを軽く扱うと、MCP serverが間接的に高権限のLLM実行権を持ちます。単なる補助機能ではありません。
なぜsamplingを新規採用しないのか
MCP 2026-07-28では、protocol coreがstatelessになり、server-to-client requestの扱いが変わりました。旧来の sampling/createMessage、elicitation/create、roots/list のようにserverからclientへ直接requestする形は、MRTRの input_required に置き換わります。
さらに、Sampling自体はdeprecated featureです。理由は実務感覚にも合っています。
- client実装が重い
- human-in-the-loop approvalが必要になる
- model selectionをclient側で持つ必要がある
- prompt injectionとdata exfiltrationの入口になる
- tool loop対応まで入れるとagent基盤そのものになる
- serverが直接model provider APIを呼ぶ代替がある
個人のローカル実験なら動かせます。組織導入やremote MCPでは、責任境界がぼやけます。
samplingで起きる責任境界のズレ
たとえば、GitHub issueを読むMCP serverが次のようなsamplingを要求するとします。
{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "このissueを要約して、修正方針を出してください。"
}
}
],
"includeContext": "thisServer",
"maxTokens": 800
}
}
一見すると要約だけです。しかし実際には、次の問いに答えないといけません。
- issue本文にprompt injectionが入っていたら誰が除去するのか
includeContextでどのresourceがLLMへ渡るのか- client側のどのmodelが使われるのか
- 生成結果をserverがそのままwrite toolへつなげられるのか
- model callのcostを誰の予算で見るのか
- promptとcompletionを監査ログへどの粒度で残すのか
これらはMCP serverだけでは完結しません。client、host、組織のAI利用ルール、監査基盤が関わります。
だから私は、samplingを「MCP serverの便利機能」とは見ません。clientのmodel権限をserverへ一時委任する操作として扱います。
既存実装を棚卸しする
まずcodeとtraffic logから、sampling利用を一覧化します。
mcp_sampling_inventory:
server: github-workflow
protocol_versions:
observed: ["2025-11-25", "2026-07-28"]
sampling_calls:
- key: issue_summary
origin_method: tools/call
prompt_source: issue_body
include_context: thisServer
tools_enabled: false
human_review_before_send: unknown
human_review_before_return: unknown
downstream_write: maybe
log_policy: prompt_and_completion_redacted
最低限、次を見ます。
| 項目 | 見る理由 |
|---|---|
| 呼び出し元tool | どの業務操作の途中でmodel callが走るか |
| prompt source | 外部コンテンツか、固定templateか |
includeContext | server contextをどこまで混ぜるか |
| tools / toolChoice | sampling内で別tool loopが起きるか |
| model preference | client任せか、特定model前提か |
| human review | 送信前・返却前に確認できるか |
| downstream write | 生成結果が外部writeへ直結するか |
| logging | prompt / completion / contextをどこへ残すか |
この棚卸しなしにSDKだけ上げると、wireは動いても安全設計は古いまま残ります。
移行先は3択に分ける
Samplingの移行先は、全部同じではありません。私は次の3択で分けます。
1. server-owned model callへ移す
server自身がLLM機能を本当に必要としているなら、model provider APIを明示的に呼ぶ方が責任境界は分かりやすいです。
model_call_owner: mcp_server
api_key_owner: platform-team
allowed_inputs:
- normalized_issue_summary
- internal_schema_excerpt
blocked_inputs:
- raw_secret
- raw_customer_data
- browser_content
audit:
prompt_template_id: issue_triage_v3
model: pinned
retention_days: 30
この形なら、server ownerがmodel、prompt、予算、ログ、redactionに責任を持てます。clientのmodel accessを借りないため、hostごとの実装差にも左右されにくくなります。
向いているのは、分類、要約、正規化、候補生成のようにserver機能として閉じている処理です。
2. host-owned workflowへ移す
model callが「serverの処理」ではなく「agent workflowの一部」なら、MCP serverに持たせない方がよいです。
MCP server: raw dataを安全に返す
Host / agent: どのmodelで要約するか決める
Reviewer: 生成結果を見てwrite可否を判断する
たとえば、issue検索serverはissue一覧と本文だけ返します。要約、優先度付け、修正案の生成はCodexやClaude Code側のworkflowに置きます。
この方が、AIエージェントのコンテキスト設計ガイド や AIエージェント監査ログ設計ガイド と接続しやすくなります。serverはdata provider、hostはreasoning owner、reviewerは承認者として分かれます。
3. MRTR互換に閉じる
すぐに廃止できない場合だけ、MRTR内の input_required で互換させます。
ただし、互換層でも無条件に sampling/createMessage を返してはいけません。2026-07-28のMRTRでは、serverはclientがcapabilityで宣言していない inputRequests を送れません。したがって、初回requestの _meta.io.modelcontextprotocol/clientCapabilities.sampling を確認し、sampling未対応ならhost-owned workflowやserver-owned model callへ落とします。
{
// _meta.io.modelcontextprotocol/clientCapabilities.sampling を確認済みの場合だけ返す
"resultType": "input_required",
"inputRequests": {
"summary": {
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "次の検証ログを要約してください。"
}
}
],
"includeContext": "none",
"maxTokens": 400
}
}
},
"requestState": "signed-short-lived-state"
}
この例は、clientがsampling capabilityを宣言している場合だけ返します。未宣言のclientへは、同じ input_required で別の対応可能な入力を求めるか、samplingなしで完了できる結果へ縮退します。
ただし、これは延命策です。旧clientとの互換、段階移行、限定されたinternal server以外では増やしません。
includeContext はまず切る
Samplingで危ないのは、prompt本文だけではありません。includeContext によって、MCP server側のcontextが追加でLLMへ渡ります。
旧実装で thisServer や allServers を使っているなら、まず none へ落とせるか確認します。
{
"method": "sampling/createMessage",
"params": {
"messages": [],
"includeContext": "none",
"maxTokens": 400
}
}
必要なcontextがあるなら、暗黙に混ぜるのではなく、tool resultやresource URIとして明示します。
{
"content": [
{
"type": "text",
"text": "検証ログの要約対象を作成しました。"
}
],
"structuredContent": {
"summaryInputUri": "mcp://logs/runs/run_01JZ/summary-input",
"redactionApplied": true
}
}
暗黙contextは便利ですが、監査で「何をmodelへ渡したか」を説明しにくくなります。生成AIの組織利用では、この説明不能さがそのままリスクになります。
tool loop付きsamplingは別物として扱う
Samplingにはtoolsを渡し、client側でtool useを含む生成を返す形もあります。これは単なるcompletionではありません。serverがclientのmodelを使って、小さなagent loopを走らせる構造です。
私は、tool loop付きsamplingをMCP serverの内部処理として持つのは避けます。
理由は単純です。
- toolの危険度分類が二重になる
- approvalの責任者が分かれにくい
- loop回数、cost、timeoutをserverとclientのどちらが管理するか曖昧になる
- prompt injectionを受けた外部inputが、別tool callへつながりやすい
- traceが分断され、監査ログで再現しにくい
必要なら、MCP serverではなくagent workflowとして設計します。tool riskは AIエージェント tool risk tier 設計ガイド、作業後の受け入れ判断は Codex レビュー完了ゲート設計ガイド に寄せます。
承認は2段に分ける
Sampling互換を残す場合、承認は1回では足りません。
| 承認点 | 確認すること |
|---|---|
| modelへ送る前 | prompt、context、cost、model、外部送信範囲 |
| serverへ返す前 | completion内容、機密混入、writeへの接続可否 |
特に外部コンテンツ由来のpromptでは、送信前reviewを省略しない方がよいです。issue、chat、web、logには、自然文の中に命令が混ざります。
運用では、sampling requestを次のように表示します。
sampling_review:
server: github-workflow
request_key: issue_summary
model_hint: low-cost
max_tokens: 400
include_context: none
prompt_source:
- issue_body
- repo_labels
untrusted_input: true
downstream_write: false
この画面を見て、人間またはpolicy engineが送信可否を判断します。
監査ログはmodel call単位で残す
Samplingを残すなら、MCP tool callのログだけでは足りません。toolの途中で別のmodel callが走るからです。
最低限、次を別eventとして残します。
{
"event": "mcp_sampling_requested",
"operationId": "op_20260817_001",
"server": "github-workflow",
"originMethod": "tools/call",
"requestKey": "issue_summary",
"promptTemplateId": "issue_summary_v2",
"includeContext": "none",
"untrustedInput": true,
"humanReviewedBeforeSend": true,
"humanReviewedBeforeReturn": true,
"downstreamWriteAllowed": false
}
promptやcompletionを丸ごと保存するかは、組織のデータ境界で決めます。保存する場合でも、raw secret、個人情報、customer dataをそのまま長期保存しない方が安全です。
ログは「後から責任追跡できる粒度」と「保存してよいデータ」のバランスで決めます。ここを雑にすると、監査のためのログが別の情報漏えいリスクになります。
移行手順
私なら、次の順番で進めます。
手順1: sampling利用を検出する
rg "sampling/createMessage|setRequestHandler\\(['\\\"]sampling/createMessage|includeContext|modelPreferences" .
文字列検索だけでなく、traffic logも見ます。SDK wrapperの中に隠れている場合、codeだけでは漏れます。
手順2: 新規利用を止める
新しいMCP server templateからsampling helperを外します。review checklistにも入れます。
mcp_review_gate:
forbidden_new_features:
- sampling/createMessage
- roots/list
- logging/setLevel
exceptions_require:
- migration_issue
- owner
- removal_date
例外を残すなら、期限とownerを必ず置きます。
手順3: 低リスク処理からhost-owned workflowへ移す
要約や分類のような処理は、MCP serverから外しやすいです。
before:
tools/call -> server -> sampling/createMessage -> server returns generated summary
after:
tools/call -> server returns normalized data
host agent -> model call -> reviewer sees summary
serverが返すdataは MCP structuredContent 設計ガイド の形で、ID、件数、参照URI、redaction状態を分けます。
手順4: 残すものだけMRTR互換へ移す
すぐ廃止できないものは、MRTRの input_required に寄せます。
このとき、requestState は署名し、短命にし、元tool、arguments、user、tenantへbindします。clientから返る inputResponses は未信頼入力として扱います。ここは MCP MRTR 設計ガイド と同じ基準です。
手順5: 退役日を決める
Deprecated featureは「動くから残す」になりがちです。例外ごとに退役日を置きます。
sampling_deprecation_plan:
server: github-workflow
remaining_calls:
- issue_summary
temporary_mode: mrtr_compat
owner: platform-ai
removal_target: 2026-10-31
replacement: host_owned_summary_workflow
退役日がない互換層は、実質的に恒久機能になります。
よくある失敗
SDK v2へ上げたので移行完了とみなす
不十分です。SDKが互換shimで吸収しているだけで、設計上のsampling依存は残っている場合があります。wire、ログ、承認UI、host supportを見ます。
includeContext: "allServers" を便利だから残す
危険です。どのserver contextがLLMへ渡ったか説明しにくくなります。まず none に落とし、必要なcontextだけ明示的に渡します。
completionをそのままwrite toolへ渡す
Sampling結果は提案であって、承認済み事実ではありません。write前には別のpolicy checkと人間reviewを通します。
tool loopをserver内部の実装詳細として隠す
隠してはいけません。tool loopはagentic workflowです。cost、timeout、承認、traceをhost側のrunとして見える化します。
判断基準
最後は次の5問で決めます。
- このmodel callはserverの責任か、host agentの責任か
- promptに外部コンテンツやcustomer dataが入るか
- 生成結果がwrite操作へ直結するか
- model、cost、prompt、completionを監査できるか
- deprecated featureとしての退役日があるか
1つでも曖昧なら、新しいsampling利用は追加しない方がよいです。