MCP の tool と resource の使い分け

Split-color graphic showing 'tool' (left) and 'resource' (right) with '使い分け' in a white circle; left label 'model-controlled', right label 'application-controlled'.

PSSLの佐々木です

MCP サーバーを書いていると、ある機能を @mcp.tool で実装すべきか @mcp.resource で実装すべきかで迷う場面が出てきます。公式のコース教材でも「ドキュメントを読む」という同じ処理が tool と resource の両方で実装されていて、最初に読んだときは違いがピンと来ませんでした。

仕様を読み直して整理したところ、判断基準は思っていたよりはっきりしていたので、使い分けの考え方としてまとめます。

この記事では、

  • tool と resource の違いは「機能」ではなく「誰が呼ぶか」であること
  • どちらで実装するかを決める判断フロー
  • ユースケース別の使い分け
  • resource 側にしかない機能(URI テンプレート、変更通知)の活かし方
  • 迷ったときに両方出しておく実装パターン
  • ハマりどころ(Messages API の MCP connector は tool のみ、など)

についてまとめました。

1. 何がわからなかったのか

Anthropic のコース教材では、インメモリのドキュメント管理サーバーを題材に、こういう tool が定義されます。

@mcp.tool(
    name="read_doc_contents",
    description="Read the contents of a document and return it as a string."
)
def read_document(
    doc_id: str = Field(description="Id of the document to read")
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

ところが少し後の章で、まったく同じことをする resource が出てきます。

@mcp.resource("docs://documents", mime_type="application/json")
def list_docs() -> list[str]:
    return list(docs.keys())

@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

やっていることは docs[doc_id] を返すだけで、tool 版と 1 行も違いません。なぜ 2 つあるのか。

2. 違いは「誰が呼ぶか」

答えは機能差ではなく 制御主体 です。MCP の仕様では tool は model-controlled、resource は application-controlled と明確に区別されています。

つまり read_doc_contents は「Claude に自分で判断して使ってほしい機能」、docs://documents/{doc_id} は「@ メンション UI のためのデータソース」です。同じ処理でも役割がまったく違う、というのが教材の意図でした。

ここを押さえると、資料でよく見る「resource はデータ、tool はアクション」という説明が、もう一段深く理解できます。読み取り専用かどうかが本質なのではなく、呼ぶ判断をモデルに委ねるのか、アプリが握るのかが本質です。

3. 判断フロー

実装するときは、この順番で考えるとよさそうです。

Q1 は単純です。書き込み・削除・外部への送信は必ず tool にします。resources/read はクライアントが再読み込みやキャッシュをする前提の操作なので、ここに副作用を置くと何回呼ばれるか分からず事故ります。「GET に副作用を持たせない」と同じ話です。

Q2 と Q3 が実質的な分かれ目です。読み取り専用でも、「どのデータが必要かをモデルに判断させたい」なら tool です。ここを「読み取りだから resource」と機械的に決めてしまうと、モデルからは存在しないデータになってしまいます。

仕様側にもこの指針が書かれていて、モデルに対してデータを自動的に公開したい場合は Tools のような model-controlled なプリミティブを使うべきとされています。

4. ユースケース別の使い分け

具体例に落とすとこうなります。

やりたいこと 選択 理由
@ でドキュメントを参照させる resource ユーザーの UI 操作が引き金。モデルの判断は不要
「report.pdf を要約して」に応える tool どのドキュメントが必要かはモデルが判断する
ドキュメントを編集する tool 副作用がある
全文検索して該当箇所を返す tool 検索クエリをモデルが組み立てる
プロジェクト規約や DB スキーマを常に文脈に入れる resource アプリが定型的に注入すればよい
ログの最新状態を追わせる resource + subscribe 変更通知がプロトコル標準にある
チケットを作成する tool 副作用がある
ユーザーが選んだファイルを添付する resource 選択したのはユーザー

「検索は tool、指定は resource」と覚えると整理しやすいと感じています。何を取るかが決まっていないなら tool、決まっているなら resource です。

5. resource 側の機能を活かす

resource を選んだ場合、tool にはない仕組みが使えます。ここを使わないと resource にした旨味が薄くなります。

5.1 URI テンプレートと補完

docs://documents/{doc_id} のようなテンプレートは RFC 6570 の URI Template 構文で、resources/templates/list で discovery できます。パラメータは MCP の completion API で自動補完に対応させられるので、@ メンションの候補表示が標準の枠に乗ります。Python SDK はテンプレートのパラメータを自動でパースして関数のキーワード引数に渡してくれるので、実装側は URI のパースを書く必要がありません。

5.2 変更通知(subscribe)

サーバーが subscribe capability を宣言すると、クライアントは resources/subscribe で個別の URI を購読でき、内容が変わると notifications/resources/updated が飛びます。クライアントはそれを受けて再読み込みします。ログやメトリクスのような「更新され続けるデータ」を扱うなら、ポーリングを自作せずに済みます。

5.3 mime_type

mime_type はクライアントがレンダリングを決めるヒントになります。JSON を返すのに text/plain を書いておくと、クライアントによっては素の文字列として扱われます。SDK が戻り値のシリアライズはやってくれますが、MIME タイプの正しさは面倒を見てくれないので、ここはサボらないほうがいいです。

6. 迷ったら両方出しておく

判断フローで整理しても、実際には「両方あると便利」というケースが出てきます。そのときは 内部実装を 1 本にして、tool と resource の両方から呼ぶ のが素直です。

理由は、resource の扱いがクライアント実装に委ねられているからです。仕様上、ユーザーに明示的に選択させるクライアント、ヒューリスティクスで自動選択するクライアント、モデル自身に選ばせるクライアント、どれもあり得るとされています。実際にも resources/list は実装済みでも read の UX がまちまち、subscribe は未対応、といった差があります。resource しか用意していないと、未対応のクライアントからは中身が空っぽに見えます。

from mcp.server.fastmcp import FastMCP
from pydantic import Field

mcp = FastMCP("DocumentMCP", log_level="ERROR")

docs = {
    "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.",
    "report.pdf": "The report details the state of a 20m condenser tower.",
}

# --- 実装は 1 箇所だけ ---
def _list_doc_ids() -> list[str]:
    return list(docs.keys())

def _read_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

# --- resource: アプリの @ メンション UI 用 ---
@mcp.resource("docs://documents", mime_type="application/json")
def list_docs_resource() -> list[str]:
    return _list_doc_ids()

@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc_resource(doc_id: str) -> str:
    return _read_doc(doc_id)

# --- tool: Claude が自分で判断して呼ぶ用 ---
@mcp.tool(
    name="list_documents",
    description="List the ids of all available documents."
)
def list_documents() -> list[str]:
    return _list_doc_ids()

@mcp.tool(
    name="read_doc_contents",
    description="Read the contents of a document and return it as a string."
)
def read_document(
    doc_id: str = Field(description="Id of the document to read")
):
    return _read_doc(doc_id)

追加コストはデコレータ数行なので、割に合うと思っています。ただし tool を増やすとその定義はモデルのコンテキストを常に消費するので、「何でも tool にも出しておく」は避けたほうがいいです。文脈注入で完結するデータは resource だけに留めます。

動作確認は Inspector が楽です。

uv run mcp dev mcp_server.py

Resources と Resource Templates が別枠で表示されるので、テンプレートのパラメータ解決まで確認できます。

7. 注意点(ハマりどころ)

7.1 Messages API の MCP connector は tool しか使えない

これは事前に知らないと詰みます。公式ドキュメントに明記されていて、MCP 仕様の機能セットのうち 現時点では tool call のみサポート です。さらにサーバーは HTTP で公開されている必要があり、ローカル STDIO サーバーは直接接続できません

mcp_servers パラメータ(MCP connector)
  ├─ tool call     ... OK
  ├─ resource      ... NG
  ├─ prompt        ... NG
  └─ stdio サーバー ... NG

「resource を作ったのに Messages API から読めない」は仕様どおりの挙動です。

7.2 resource を使いたいならクライアント側ヘルパーに寄せる

では API 経由で resource を使う手段が無いのかというと、そうではなく 自分で MCP クライアント接続を管理する 側に回ります。Anthropic SDK にはそのための変換ヘルパーが用意されています。

# pip install "anthropic[mcp]"  (Python 3.10 以降)
from anthropic.lib.tools.mcp import mcp_resource_to_content

resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt")
response = await client.beta.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            mcp_resource_to_content(resource),
            {"type": "text", "text": "Summarize this document"},
        ],
    }],
)

mcp_resource_to_file を使えばそのまま Files API にアップロードもできます。公式の使い分けも明快で、URL で到達できるリモートサーバーで tool だけ使いたいなら mcp_servers パラメータ、ローカルサーバーや prompt / resource を使いたいならクライアント側ヘルパー です。

resource は「Claude が勝手に読むもの」ではなく「アプリが読んでプロンプトに詰めるもの」だという 2 章の話が、SDK の API 設計にそのまま現れています。

7.3 resource link はクライアント側で解決してから渡す

変換ヘルパーは、未対応のコンテンツタイプや MIME タイプ、そして resource link を渡すと例外を投げます(Python なら UnsupportedMCPValueError)。resource link は MCP クライアント側で実体に解決してから変換する必要があります。

7.4 大きい resource はコンテキストを食う

resource の中身は最終的にプロンプトに入ります。ログファイル全体のような resource をうっかり注入すると一撃でコンテキストが埋まります。大きいものは resource link で参照させるか、tool 側でフィルタ・要約してから返す設計にしたほうが安全です。

8. まとめ

  • tool と resource の違いは機能ではなく 制御主体。tool は model-controlled、resource は application-controlled
  • 判断は「副作用があるか」→「呼ぶタイミングをアプリが決められるか」→「モデルに存在を知らせる必要があるか」の順で考える
  • 読み取り専用でも、何を取るかをモデルに判断させたいなら tool。読み取りだから resource、と機械的に決めるとモデルから見えないデータになる
  • resource を選んだら URI テンプレート・補完・subscribemime_type まで使い切ると効果が出る
  • 両方あると便利なケースは、内部関数を共有して二重提供する。ただし tool 定義はコンテキストを消費する点に注意
  • Messages API の MCP connector は tool のみ。resource を使うならクライアント側ヘルパー(mcp_resource_to_content / mcp_resource_to_file)に寄せる

「モデルに判断させる」のか「アプリが決め打ちする」のかを設計として先に決めておくと、実装もレビューもぶれなくなります。何でも tool にしてモデルに探索させるとトークンも増えて挙動も揺れますし、逆に何でも resource にするとモデルからは存在しないデータになります。AI エージェントと組み合わせる開発では、この「どこまでを機械的に確定させるか」の線引きが毎回論点になるなと感じています。

参考リンク


AI エージェント導入のご相談

サイオステクノロジーでは、本記事で扱った MCP サーバーの設計・実装を含む AI エージェントの SI サービスを提供しています。既存システムと AI エージェントの接続、RAG の精度評価と改善など、PoC から本番運用までの実装フェーズをまとめてご支援します。

「自社の業務でどこまで自動化できるのか」「どのデータをエージェントに渡すべきか」といった構想段階のご相談も歓迎です。無料相談も承っておりますので、ご興味のある方は下記サービスサイトからお気軽にお問い合わせください。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です