本文へ移動
🧠

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か
includeContextserver contextをどこまで混ぜるか
tools / toolChoicesampling内で別tool loopが起きるか
model preferenceclient任せか、特定model前提か
human review送信前・返却前に確認できるか
downstream write生成結果が外部writeへ直結するか
loggingprompt / 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問で決めます。

  1. このmodel callはserverの責任か、host agentの責任か
  2. promptに外部コンテンツやcustomer dataが入るか
  3. 生成結果がwrite操作へ直結するか
  4. model、cost、prompt、completionを監査できるか
  5. deprecated featureとしての退役日があるか

1つでも曖昧なら、新しいsampling利用は追加しない方がよいです。

参考資料

WRITTEN BY nidoneko

Full-stack engineer with 8+ years of experience in TypeScript, React, Node.js, and cloud-native development across healthcare, finance, HR, and IoT domains.

View Profile →