MCPで巨大なレスポンスを返してはいけない理由|トークン消費を減らす設計

MCPで巨大なレスポンスを返してはいけない理由|トークン消費を減らす設計 AI開発

MCP Toolを作るとき、取得できたデータをすべてそのまま返したくなることがあります。たとえばログ検索Toolで、

避けたい設計1
1万行のログをそのまま返す

Database検索Toolで、

避けたい設計2
1000件のRecordをすべて返す

GitHub連携Toolで、

避けたい設計3
Issue本文
Comment
Event履歴
差分
Metadata

をまとめて返す、といった設計です。技術的には返せる場合でも、AIエージェント向けのToolとしては巨大なレスポンスを避けたほうがよいでしょう。

特に問題になりやすいのがcontentです。現在のMCP Python SDKでは、Tool Resultのcontentはモデルが読む情報、structured_contentはClient Applicationが利用する構造化データとして整理されています。つまり大量のTextをcontentへ返すと、MCP Hostがその内容をモデルへ与える設計では、その分だけモデルが扱うContextも大きくなります。

結果として、Token消費だけでなく、重要な情報が大量のノイズへ埋もれたり、次のTool Callで不要な履歴まで持ち回ったりする問題につながります。MCP Toolは「取得できる情報を全部返すAPI」ではなく、モデルが次の判断をするために必要な情報だけを返すインターフェースとして設計することが重要です。

この記事では、MCPで巨大なTool Resultを返さないほうがよい理由と、limit、Cursor、Summary、詳細取得Tool、Resource Link、Structured Outputなどを使ってレスポンスを小さくする方法を解説します。

スポンサーリンク
  1. MCP Toolのcontentはモデルが読む
  2. 「HTTPレスポンスが大きい」と「モデルのToken消費」は同じではない
  3. 巨大なレスポンスは重要な情報を埋もれさせる
  4. Tool ResultはConversationの後続処理にも影響する
  5. 1万件取得してモデルに選ばせる設計を避ける
  6. limitをinputSchemaへ入れる
  7. デフォルト値を小さくする
  8. 一覧Toolと詳細Toolを分ける
  9. 検索結果では全文ではなくIDを返す
  10. snippetを返す設計が便利
  11. Resource Linkを使って本文を後から読ませる
  12. EmbeddedResourceとResourceLinkを使い分ける
  13. 画像やBinaryを何でもbase64で埋め込まない
  14. structuredContentも必要なFieldだけ返す
  15. contentとstructuredContentへ巨大データを二重に入れない
  16. まずSummaryだけ返す
  17. ログToolならtailと時間範囲を必須にする
  18. SQL ToolでSELECT結果を無制限に返さない
  19. truncatedを明示する
  20. tools/callの結果はMCP標準Paginationが自動分割してくれるわけではない
  21. Tool独自のcursorを設計する
  22. offsetよりcursorが向くケースもある
  23. totalCountを毎回計算しなくてもよい
  24. 「何件欲しいか」をモデル任せにしすぎない
  25. 全文が必要な処理ならChunk単位で読ませる
  26. 検索と読み取りを分けるとRAGに近い構造になる
  27. Server側でAggregationする
  28. モデルに計算させる前にToolで前処理する
  29. キャッシュで同じ巨大データを何度も取得しない
  30. 変化しない巨大データはTool ResultよりResourceを検討する
  31. Tool ResultへDebug情報を全部入れない
  32. エラー時にStack Trace全文を返さない
  33. 「最大何文字なら安全か」という固定値はない
  34. Server側にレスポンス上限を持たせる
  35. Toolのdescriptionにも件数の性質を書く
  36. 巨大なレスポンスを返すToolは役割が広すぎる可能性がある
  37. MCPで巨大なレスポンスを返してはいけない理由に関するよくある質問
  38. MCPのレスポンス設計は「全部渡す」から「必要になったら取る」へ変える

MCP Toolのcontentはモデルが読む

まず理解しておきたいのが、Tool ResultのcontentとstructuredContentの違いです。たとえば商品検索Toolが次の結果を返したとします。

Tool実行結果の例
{
  "content": [
    {
      "type": "text",
      "text": "RTX 5070搭載PCが3件見つかりました。最安値は189,800円です。"
    }
  ],
  "structuredContent": {
    "total": 3,
    "minPrice": 189800
  }
}

現在のMCP Python SDKでは、contentはモデルが読む情報、structured_contentはClient Applicationが利用する型付きデータとして説明されています。そのためToken消費という観点では、特にcontentへ何を入れるかが重要です。

たとえば検索結果1000件をJSONへ変換し、

避けたい実装
return json.dumps(rows)

としてText Resultへ全部入れると、その巨大なTextをモデルへ渡すClientでは大量のContextを消費する可能性があります。

「HTTPレスポンスが大きい」と「モデルのToken消費」は同じではない

ここは区別して考える必要があります。MCP Serverから返したByte数が、そのまま同じ数のLLM Tokenになるわけではありません。またstructuredContentについても、MCPの役割上はClient Application向けのデータであり、必ずしもそのまますべてモデルContextへ投入されるとは限りません。

したがって、

成り立たない等式
MCP Response 1MB
=
必ずLLMへ1MB投入される

とは言えません。実際に何がモデルへ送られるかはMCP HostやClientの実装にも依存します。ただし巨大なResultは、モデルへ渡さなかったとしても、Network転送・JSON Parse・Client Memory・ログ・Trace・UI描画・履歴保存の負荷になります。つまり巨大レスポンスを避ける理由はTokenだけではありません。

巨大なレスポンスは重要な情報を埋もれさせる

たとえばエラー調査Toolが10万文字のログを返したとします。その中で本当に重要なのは、ERROR・Exception・Stack Trace・直前の20行程度かもしれません。しかしToolがINFO・DEBUG・Health Check・正常なRequest・定期Jobまでまとめて返すと、モデルは大量の不要情報から異常箇所を探す必要があります。

AIエージェントでは「情報量が多い=判断精度が高い」とは限りません。必要な証拠だけを絞ったほうが、次のActionを判断しやすくなります。

Tool ResultはConversationの後続処理にも影響する

AIエージェントは1回Toolを呼んで終わるとは限りません。たとえば、

複数Stepの例
ログを検索
↓
設定を確認
↓
コードを読む
↓
別のToolを実行
↓
修正案を作る

と複数Stepで動きます。最初のToolで巨大なログを返してしまうと、その情報が後続の推論でもContextとして保持される構成があります。その結果、本来必要な現在のエラー・設定内容・変更差分以外の情報まで持ち回ることになります。Agentを長時間動作させる場合ほど、Tool Resultを小さくする意味が大きくなります。

1万件取得してモデルに選ばせる設計を避ける

たとえば商品検索Toolを次のように作ったとします。

避けたい実装
@mcp.tool()
def search_products(
    query: str
) -> list[dict]:
    return database.search(query)

Database側で1万件Hitすると、その1万件を全部返してしまいます。しかしユーザーが「RTX 5070搭載で安いPCを3台教えて」と質問しているなら、1万件をモデルへ渡す必要はありません。Server側でGPU・価格・在庫・並び順・件数を処理し、必要な候補だけ返すべきです。ToolをDatabase Dumpの薄いWrapperにするのではなく、AIが使いやすい粒度までServer側で絞り込みます。

limitをinputSchemaへ入れる

最も単純な対策がlimitです。たとえば、

server.py
@mcp.tool()
def search_products(
    query: str,
    limit: int = 10
) -> list[dict]:
    return database.search(
        query=query,
        limit=limit
    )

とします。さらにPydanticなどで、

server.py
from typing import Annotated
from pydantic import Field

@mcp.tool()
def search_products(
    query: str,
    limit: Annotated[
        int,
        Field(
            ge=1,
            le=20,
            description="最大取得件数。1~20件"
        )
    ] = 5
) -> list[dict]:
    ...

とすれば、モデルが10000など極端な値を指定することも防げます。生成されるinputSchemaでは概念的に、

生成されるinputSchema
{
  "limit": {
    "type": "integer",
    "minimum": 1,
    "maximum": 20,
    "default": 5
  }
}

となります。「ユーザーが指定できるLimit」だけでなく、Server側でもHard Limitを持つのが安全です。

デフォルト値を小さくする

limitがあってもdefault = 1000ではあまり意味がありません。AI向け検索Toolなら、最初は5件や10件など少数を返し、必要なら追加取得させる設計が使いやすくなります。たとえば、

段階的な取得Flow
最初の検索
→ 上位5件

必要なら
→ 次の5件

特定候補が決まったら
→ 詳細取得

というFlowです。これは人間が検索Engineを利用するときの、検索結果一覧→気になるページを開くという構造にも近いでしょう。

一覧Toolと詳細Toolを分ける

巨大レスポンスを防ぐうえで非常に有効なのが、一覧取得と詳細取得を分離する方法です。たとえば1つのToolで商品名・価格・説明文・レビュー全文・仕様・画像情報・履歴まで返すのではなく、検索Toolでは最小限のSummaryだけ返します。

server.py
@mcp.tool()
def search_products(
    query: str,
    limit: int = 5
) -> list[dict]:
    return [
        {
            "id": "pc-001",
            "name": "Gaming PC A",
            "price": 189800
        }
    ]

必要になった商品だけ、

server.py
@mcp.tool()
def get_product(
    product_id: str
) -> dict:
    ...

で詳細取得します。Agent側のFlowは、search_products→候補を選ぶ→get_productとなります。これなら候補にならなかった商品の巨大な詳細情報をContextへ入れずに済みます。

検索結果では全文ではなくIDを返す

Document検索でも同じです。悪い例は、

避けたい設計
{
  "results": [
    {
      "id": "doc-1",
      "content": "数万文字の全文..."
    },
    {
      "id": "doc-2",
      "content": "数万文字の全文..."
    }
  ]
}

です。検索段階では、

推奨する設計
{
  "results": [
    {
      "id": "doc-1",
      "title": "Authentication Guide",
      "snippet": "OAuth authentication is..."
    },
    {
      "id": "doc-2",
      "title": "MCP Deployment Guide",
      "snippet": "For production deployments..."
    }
  ]
}

程度にします。モデルが必要と判断したDocumentだけ、get_document(doc_id)で取得します。これだけでもレスポンス量を大幅に抑えられます。

snippetを返す設計が便利

検索ToolではIDだけだと、モデルがどの結果を開けばよいか判断できません。そこで短いsnippetを返します。たとえば、

snippetの例
{
  "id": "doc-17",
  "title": "OAuth Security",
  "snippet": "Access tokens must be validated against the intended resource..."
}

程度です。重要なのは、全文を返さない一方で、次に読むべきか判断できる情報は返すというバランスです。検索Resultを「最終回答」ではなく「次の取得対象を決めるIndex」として設計すると扱いやすくなります。

Resource Linkを使って本文を後から読ませる

MCPには、Tool ResultからResourceへのPointerだけを返すResourceLinkがあります。現在のPython SDKでは、Clientが後からresources/readできるResourceを返したい場合、本文をTool Resultへ埋め込む代わりにResourceLinkを返せます。たとえば、

server.py
from mcp.types import ResourceLink

@mcp.tool()
def find_manual(
    query: str
) -> ResourceLink:
    return ResourceLink(
        uri="docs://manual/oauth",
        name="OAuth manual",
        mimeType="text/markdown"
    )

のような考え方です。Tool Resultへ何万文字ものManualを直接入れるのではなく、docs://manual/oauthというURIだけを返します。必要ならClientがその後、resources/readで本文を取得できます。

EmbeddedResourceとResourceLinkを使い分ける

Resource関連ではEmbeddedResourceもあります。EmbeddedResourceはResource本文自体をTool Resultへ埋め込みます。一方のResourceLinkはPointerだけです。Python SDKの現行Media Documentationでも、大きなResourceを後からresources/readさせたい場合はResourceLinkを返す方法が案内されています。

したがって巨大なDocumentなら、

使い分け
EmbeddedResource
→ 本文まで送る

ResourceLink
→ URIだけ送る

という違いを意識します。サイズ削減が目的ならResource Linkのほうが適しています。

画像やBinaryを何でもbase64で埋め込まない

MCP Toolは画像やAudio、Blobも返せます。しかしBinary DataをInlineで返すと、Base64化によってPayloadが大きくなります。たとえば巨大なPDFや画像を、base64として毎回Tool Resultへ入れるより、Resourceとして保存しPointerを返したほうがよいケースがあります。

Python SDKでもBinary ResourceはBlobResourceContentsとしてBase64で埋め込めますが、Pointerだけを送りたい場合にはResourceLinkを使えます。プレビューが必要ならThumbnailだけ返し、原本はResource Linkにする設計も考えられます。

structuredContentも必要なFieldだけ返す

outputSchemaとstructuredContentの違いで解説したstructuredContentを使えば、Application側が扱いやすいJSONを返せます。ただしStructured Outputだから巨大でも問題ない、というわけではありません。

たとえばDatabase Rowをそのまま返して、

避けたい設計
{
  "id": 100,
  "name": "Alice",
  "email": "...",
  "createdAt": "...",
  "updatedAt": "...",
  "internalMemo": "...",
  "auditHistory": [...],
  "permissions": [...],
  "preferences": {...},
  "metadata": {...}
}

とするより、そのToolで本当に必要なFieldだけをSchemaへ含めます。

推奨する設計
{
  "id": 100,
  "name": "Alice"
}

で十分なら、それだけ返します。現在のMCPではstructured_contentはClient Application向けの型付きデータとして利用されます。Clientが使わない巨大なFieldまで転送する必要はありません。

contentとstructuredContentへ巨大データを二重に入れない

Structured Outputを利用すると、contentとstructuredContentの両方へ同じ内容を持たせることがあります。これは互換性の面では有用ですが、大量データではPayloadをさらに膨らませる可能性があります。

たとえば1000件のRecordを、

二重化の例
content
→ JSON文字列1000件

structuredContent
→ JSON Object 1000件

とすれば、同じ情報をほぼ二重に運ぶことになります。現在のMCP Python SDKでは両者の役割が明確に分かれており、contentはモデル、structured_contentはApplicationが読むものです。モデルには「上位5件を取得しました。詳細はStructured Resultを参照できます。」のような短いSummaryだけを与え、Application用データも必要最小限へ絞る設計を検討できます。ただし利用するClientの互換性要件も確認してください。

まずSummaryだけ返す

巨大なTool Resultを抑える最も汎用的な方法がSummaryです。たとえばログ分析Toolなら、生ログ1万行ではなく、

Summaryの例
{
  "totalLines": 12450,
  "errorCount": 17,
  "warningCount": 83,
  "topErrors": [
    {
      "message": "Connection timeout",
      "count": 11
    },
    {
      "message": "Authentication failed",
      "count": 6
    }
  ]
}

を返します。モデルはこれを見て「Connection timeoutを詳しく調べる」と判断できます。必要になったら別Toolでerror_type = Connection timeoutの前後だけ取得します。

ログToolならtailと時間範囲を必須にする

ログ取得Toolで、

避けたい設計
@mcp.tool()
def get_logs() -> str:
    return read_all_logs()

という設計は危険です。代わりに、

server.py
@mcp.tool()
def search_logs(
    query: str,
    minutes: int = 15,
    limit: int = 50
) -> list[dict]:
    ...

など、最初から範囲を限定します。さらにServer側でminutes <= 60・limit <= 100などHard Limitを設定します。必要に応じてモデルが追加Queryを行えばよく、一度に全部読む必要はありません。

SQL ToolでSELECT結果を無制限に返さない

MCPからDatabaseを操作する場合も同様です。たとえば、

避けたい設計
@mcp.tool()
def run_sql(sql: str):
    return db.execute(sql).fetchall()

とすると、SELECT * FROM access_logs;によって数百万件返る可能性があります。AI向けDatabase Toolなら、任意SQLそのものを許可する設計も含め慎重に検討し、少なくとも結果件数には上限を設けます。たとえばServer側で最大100行までに制限し、それ以上なら、

切り詰めの明示
{
  "returned": 100,
  "truncated": true
}

と返します。これならモデルも「結果が切り詰められている」ことを認識できます。

truncatedを明示する

Responseを勝手に途中で切るだけでは、モデルが「これが全件」と誤解する可能性があります。そこでStructured Resultへ、

Metadataの例
{
  "items": [...],
  "returned": 20,
  "total": 1842,
  "truncated": true
}

のようなMetadataを付けます。あるいはCursor方式なら、

nextCursorの例
{
  "items": [...],
  "nextCursor": "eyJvZmZzZXQiOjIwfQ=="
}

とします。「省略したこと」と「次を取得する方法」を同時に伝えることが重要です。

tools/callの結果はMCP標準Paginationが自動分割してくれるわけではない

ここは特に注意したいポイントです。MCPにはCursor Paginationがあります。しかし公式Python SDKのPagination Documentationで扱われているのは、tools/list・resources/list・prompts/list・resources/templates/listなどの一覧Methodです。大量のResource一覧などを一度にSerializeせず、Serverがnext_cursorを返し、Clientが次のPageを要求できます。

一方、tools/callで検索結果10000件を返したとき、そのResultをMCP Protocolが自動的に10Pageへ分割してくれるわけではありません。検索Tool自身にPaginationを持たせる必要があります。

Tool独自のcursorを設計する

たとえばDocument検索Toolなら、

server.py
@mcp.tool()
def search_documents(
    query: str,
    cursor: str | None = None,
    limit: int = 10
) -> dict:
    ...

とできます。返却値は、

返却値の例
{
  "items": [
    {
      "id": "doc-101",
      "title": "OAuth Guide"
    }
  ],
  "nextCursor": "abc123",
  "hasMore": true
}

とします。次のTool Callでは、

次のリクエスト
{
  "query": "oauth",
  "cursor": "abc123",
  "limit": 10
}

を渡します。ここでいうcursorは自分のTool APIとして設計しているものです。MCPのtools/list用Protocol Paginationとは別です。

offsetよりcursorが向くケースもある

単純なData Setなら、offset=0・offset=20・offset=40でも実装できます。しかし検索中にRecordが追加・削除される環境では、Offset方式で重複や取りこぼしが起こる場合があります。Database QueryのSort KeyなどからOpaque Cursorを生成し、nextCursorを返す方式のほうが安定するケースがあります。

モデル側にCursor内部を理解させる必要はありません。前回受け取った文字列をそのまま次のCallへ渡させます。MCP自体のList PaginationでもCursorはOpaque Tokenとして扱い、Clientが解析・推測せず、そのまま次のRequestへ渡す考え方になっています。独自Tool Paginationでも同じ考え方を採用すると分かりやすいでしょう。

totalCountを毎回計算しなくてもよい

Pagination APIを作ると{"total": 3851294}を必ず返したくなります。しかし巨大Databaseでは正確なTotal Count自体が高コストになる場合があります。モデルが必要としているのが「次のPageがあるか」だけなら、

最小限のMetadata
{
  "hasMore": true,
  "nextCursor": "..."
}

で十分です。Tool Resultを小さくするだけでなく、Server側の処理コストも抑えられます。

「何件欲しいか」をモデル任せにしすぎない

limitをInputへ公開しても、最大値なしでは、モデルが大きな件数を指定する可能性があります。たとえば{"limit": 100000}です。そこでInput Schemaでは、

上限を持たせる
{
  "type": "integer",
  "minimum": 1,
  "maximum": 50,
  "default": 10
}

のように上限を持たせます。さらにHandler内部でもMaximumを保証します。Schemaは便利ですが、Server側のBusiness LogicでもLimitを検証すると安全です。

全文が必要な処理ならChunk単位で読ませる

Code RepositoryやDocumentを分析する場合、最終的には全文が必要になることもあります。それでも一度に全部返す必要はありません。たとえばdocument_id・start・lengthを受け取るToolを作れます。

server.py
@mcp.tool()
def read_document_chunk(
    document_id: str,
    start: int,
    length: int = 5000
) -> dict:
    ...

結果は、

結果の例
{
  "documentId": "doc-1",
  "start": 0,
  "end": 5000,
  "hasMore": true,
  "text": "..."
}

とします。モデルは必要な範囲だけ続きを取得できます。ただし単純な全文読み取りなら、MCP Resourceとして公開したほうが設計上自然な場合もあります。

検索と読み取りを分けるとRAGに近い構造になる

大量Documentを扱う場合、検索→候補を絞る→本文を読むという形が有効です。たとえばsearch_documentsToolで、

検索結果の例
{
  "id": "doc-38",
  "score": 0.91,
  "title": "MCP Authorization",
  "snippet": "..."
}

だけ返します。必要になったらdocs://doc-38というResourceをReadします。これはMCPのtools・resources・promptsの違いで解説した、Tool→モデルが検索処理を実行、Resource→必要なDocumentを参照という役割分担とも一致します。

Server側でAggregationする

大量データをモデルへ渡し、モデル自身に集計させる必要がない場合もあります。たとえば100万件の注文から月別売上・平均注文額・上位カテゴリを知りたいなら、100万件の注文を返すのではなくServer側でAggregationします。

Aggregation結果の例
{
  "month": "2026-09",
  "revenue": 12450000,
  "averageOrderValue": 8432,
  "topCategory": "GPU"
}

だけ返せば十分です。DatabaseやSearch Engineが得意な処理をAIへ押し付けないことが重要です。

モデルに計算させる前にToolで前処理する

同様に、重複除去・Sort・Filter・Group By・Top N・正規表現検索などはServer側で処理できます。たとえば10万行のアクセスログをモデルへ渡して「一番多い404 URLを探して」とさせるより、

server.py
@mcp.tool()
def top_404_urls(
    minutes: int = 60,
    limit: int = 10
) -> list[dict]:
    ...

というToolを作ったほうが効率的です。AIには高度な判断をさせ、機械的なData ProcessingはProgram側へ任せます。

キャッシュで同じ巨大データを何度も取得しない

2026-07-28のMCPでは、tools/list、prompts/list、resources/list、resources/templates/list、resources/readなどにttlMsとcacheScopeによるCache Hintが導入されています。ClientはServerの宣言を見て、結果をどの程度再利用できるか判断できます。

たとえば変化の少ないDocument Resourceを毎回取り直すのではなく、適切なTTLを設定すれば不要な再取得を減らせます。ただしこのCache Hintは、任意のtools/call結果を自動キャッシュするための万能機能ではありません。Tool Resultの再利用が必要ならApplication側のCache設計も別途検討します。

変化しない巨大データはTool ResultよりResourceを検討する

たとえばAPI仕様書・Coding Guideline・商品Catalog・Schema Definition・Manualなど、URIで識別できるデータを毎回Tool Resultとして全文返しているならResource化を検討できます。MCP ResourceならClientが必要なタイミングでresources/readできます。さらに2026-07-28ではResource Read ResultにもCache Hintがあり、内容が一定時間変化しないことをClientへ伝えられます。Toolは「どのResourceを見るべきか検索する」役割に絞ることもできます。

Tool ResultへDebug情報を全部入れない

開発中は、

避けたい設計
{
  "sql": "...",
  "executionPlan": "...",
  "rawRows": [...],
  "debug": {...},
  "result": [...]
}

のように大量のDebug DataをResultへ付けたくなることがあります。しかし本番ではモデルに必要ありません。Debug情報はServer LogやTraceへ送り、Tool Resultには判断に必要な情報だけを返します。2026-07-28 MCPではTrace Contextの扱いも標準化され、OpenTelemetry互換の分散Traceへつなげやすくなっています。「モデルにDebug Logを全部見せる」ことと「運用上詳細を追跡できる」ことは分けて考えるべきです。

エラー時にStack Trace全文を返さない

Tool Errorでも巨大Resultが発生することがあります。たとえばExceptionを、

避けたい実装
except Exception:
    return traceback.format_exc()

としてしまうケースです。巨大なStack Traceや内部状態をすべてモデルへ返す必要はありません。さらに内部Path・SQL・Credential・API Key・個人情報が混入する危険もあります。モデルには「Database connection timed out after 5 seconds」程度を返し、詳細なTraceはServer Logへ残します。現在のMCP Python SDKでもTool Errorはis_error=Trueとしてcontentへモデルが読めるError Messageを返し、通常のPython Crashとは区別して扱います。

「最大何文字なら安全か」という固定値はない

ではTool Resultを何KBまでにすればよいのか、という疑問が出てきます。MCP Protocolとして「Tool Resultは必ず10KB以下」のような普遍的な上限があるわけではありません。適切なサイズは、利用するモデル・MCP Host・Context Window・他のPrompt量・同時に使うTool・用途によって変わります。

そのため固定サイズだけで設計するより、最初は少なく返す・必要なら続きを取得できる・全文ではなくSummaryを返すというProgressive Disclosureの設計にするほうが堅牢です。

Server側にレスポンス上限を持たせる

想定外の巨大Resultを防ぐため、Server側へSize Guardを入れる方法もあります。たとえば、

server.py
MAX_RESULTS = 50
MAX_TEXT_LENGTH = 20_000
server.py
results = search_database(query)

results = results[:MAX_RESULTS]

とします。Textが大きすぎる場合は、全文を切るだけでなく、

続きの取得方法を返す
{
  "truncated": true,
  "resourceUri": "logs://search/abc123"
}

のように続きの取得方法を返します。突然途中で切れたTextだけを返すより安全です。

Toolのdescriptionにも件数の性質を書く

モデルへ「このToolは最大10件返す」と伝えておくのも有効です。たとえば、

server.py
@mcp.tool()
def search_documents(
    query: str,
    limit: int = 5
) -> list[dict]:
    """
    Search documents.

    Returns at most 20 summary results.
    Use get_document when full text is required.
    """

とします。これならモデルは「全文が欲しい→search_documentsを1000件で呼ぶ」のではなく、「search→get_document」という正しいFlowを選びやすくなります。

巨大なレスポンスを返すToolは役割が広すぎる可能性がある

レスポンスがいつも巨大になる場合、Tool自体の責務が広すぎる可能性があります。たとえばget_project_everythingというToolが、README・Git Log・Issue・Pull Request・全ソースコード・設定ファイル・Dependency・CI履歴を全部返すとします。これは便利そうですが、ほとんどのTaskでは大部分が不要です。

代わりに、

用途別に分けたTool
search_code
get_file
list_recent_commits
get_issue
get_pull_request

など用途別に分けます。MCPのtools・resources・promptsの違いで触れた「Toolを増やしすぎる問題」とのバランスは必要ですが、1つのMega Toolが巨大Resultを返す設計も避けたほうがよいでしょう。

MCPで巨大なレスポンスを返してはいけない理由に関するよくある質問

QMCP Serverが返したデータ量とAIモデルのToken消費量は同じですか

A同じではありません。ByteサイズがそのままToken数になるわけではなく、structuredContentもClient Application向けのデータで必ずしもすべてモデルContextへ投入されるとは限りません。ただしNetwork転送やJSON Parse、Client Memoryなど別の負荷はあるため、Tokenだけを基準に判断しないほうがよいでしょう。

Qtools/callの結果もMCP標準のPaginationで自動的にページ分割されますか

Aされません。MCP標準のCursor Paginationはtools/list・resources/list・prompts/list・resources/templates/listなどの一覧Methodが対象で、tools/callの結果を自動分割する仕組みではありません。検索結果などを分割したい場合は、Tool自身にlimitやcursorといった独自のPaginationを実装する必要があります。

Q検索結果は全文を返さずIDやsnippetだけ返すべきですか

A多くの場合はその方が扱いやすくなります。全文をすべて返すとレスポンスが肥大化しやすいため、まずはIDやタイトル、短いsnippetだけを返し、モデルが必要と判断したものだけ詳細取得Toolで本文を取得する設計が有効です。

QResourceLinkとEmbeddedResourceはどちらを使うべきですか

Aサイズを抑えたいならResourceLinkが向いています。ResourceLinkはURIへのPointerだけを返すため、Tool Result自体は小さく保てます。EmbeddedResourceは本文そのものを埋め込むため、小さいデータや必ず即座に内容が必要な場合に向いています。

QTool Resultのサイズには公式に決まった上限がありますか

A普遍的な固定値はありません。適切なサイズは利用するモデルやMCP Host、Context Windowなどによって変わります。固定サイズを決めるより、最初は少なく返し必要なら追加取得できるようにするProgressive Disclosureの設計が実用的です。

MCPのレスポンス設計は「全部渡す」から「必要になったら取る」へ変える

MCP Toolで巨大なレスポンスを返す最大の問題は、Serverが持っている情報量と、モデルが今必要としている情報量が同じではないことです。Databaseに100万件あっても、モデルが次の判断に必要なのは上位5件だけかもしれません。Documentが5万文字あっても、検索段階ではTitleとSnippetだけで十分かもしれません。

そのため、取得できるものを全部返すのではなく、必要な最小情報を返す→モデルが次の対象を選ぶ→詳細を追加取得するという設計にします。

MCP Python SDK自身も、大量のResourceなどを1レスポンスへSerializeしないためにCursor Paginationを提供しており、大きなData Setを一度に返したくない場合の仕組みとして案内されています。ただし標準Paginationは主にresources/listなどのList Method向けです。検索Toolのtools/call結果を自動的にPage分割してくれるわけではないため、Tool側にはlimitやCursorを設計します。

巨大DocumentならTool Resultへ本文を全部埋め込まず、ResourceLinkだけを返し、必要になった時点でresources/readさせる方法もあります。検索結果なら全文ではなく、

最終的なまとめ例
{
  "id": "doc-38",
  "title": "MCP Authorization",
  "snippet": "OAuth access tokens..."
}

程度を返し、選ばれたDocumentだけ詳細取得します。ログなら全行ではなくError Summaryを返します。DatabaseならRaw Rowを大量に渡すのではなく、Filter、Aggregation、Top NをServer側で処理します。

MCP ToolのResult設計で重要なのは、「モデルに処理できる最大量」を返すことではなく、「モデルが次の判断をするために必要な最小量」を返すことです。この考え方にするとToken消費だけでなく、Network負荷、Client Memory、ログ量、推論時のノイズも減らせます。

AIエージェント向けMCPでは、巨大レスポンスを1回返す設計より、Summary → 選択 → 詳細取得という段階的な取得方法を基本にすると、長時間のAgent処理でもContextを効率よく使いやすくなります。