MCP サーバー構築の教訓:モデルは指針に従い、サーバーが主導する仕組み
著者は MCP サーバー構築における実戦の知見として、LLM は計画能力に欠けるためサーバー側で明示的なワークフローを設計し、コア動詞と一貫した識別子を用いてツールチェーンを安定させるべきだと提言している。
キーポイント
モデルの計画能力への限界認識
LLM は隠れたプランナーとして機能せず、確率的に次のアクションを選ぶだけであるため、複雑なチェーンを成功させるにはサーバー側が次の呼び出しを明白にする必要がある。
コア動詞セットの重要性
多数のツールを羅列するよりも、主要な意図を網羅する少数の名詞化された動詞(core verbs)に焦点を当てる方が、モデルの理解と実行が容易になる。
次回の呼び出しを示唆する出力設計
各レスポンスは単なる結果だけでなく、次に実行すべきアクションを明確に示唆するように設計し、サーバー側で次のステップを決定しやすくする。
一貫した識別子スキームの採用
呼び出し間で文脈が維持されるよう、行番号ではなくアンカー、ID、パスなどの堅牢な識別子を用いてリソースや状態を管理する必要がある。
影響分析・編集コメントを表示
影響分析
この記事は、MCP(Model Context Protocol)の採用が進む中で、LLM の推論能力の限界を補うための実用的なアーキテクチャ指針を提供しています。開発者が「モデルが勝手に計画してくれる」という誤解から脱却し、サーバー側で厳密な状態管理とフロー制御を行う重要性を説くことで、実運用におけるエラー削減と安定性向上に寄与します。特に複雑なドキュメント処理やオフィス自動化のようなユースケースにおいて、設計思想の転換を促す重要な示唆を含んでいます。
編集コメント
理論的な論文ではなく、実務での失敗経験(ログへの怒り)から導き出された「現場の知恵」が凝縮されており、MCP サーバーを実際に構築・運用している開発者にとって非常に価値のある実践ガイドです。
私はこれまで MCP サーバーの構築を行ってきました。昨年には 一般的なアプローチ について記事を書き、umcp の作成から始め、最近では Office サーバー を公開しました。このサーバーは十分な数のモデルによって多くの実文書に対してテストされ、パターンが定着しています。
私は依然として MCP が好きではありませんが、以下に述べるのはツールチェーンを実際に機能させるために私が学んだことであり、論文を読むよりもログに向かって罵声を浴びせることから凝縮されたものです。
**
免責事項:***これは CHAINING.md の要約版です。この文書自体は、私の Obsidian vault にある一連のメモを綴り合わせたものです。完全版にはより多くのコード例と、Opus がどうしても追加したかったテクニック一覧表が含まれていますが、私はそれらを削除し、元のテキストの大部分(誤字を除く)を復元しました。
短いバージョン: 私が設計する MCP サーバーは作業の大半を行い、モデルはパン屑をたどるだけです。
## モデルは計画を立てない 彼らは会話を見渡し、ツールリストをスキャンし、最も確からしいものをつかむだけです。それだけのことです。隠れたプランナーなど存在しません。どこかしらで意味のある結論に至るチェーンを実現したいなら、*サーバー*が各ステップにおいて次の呼び出しを明白に示さなければなりません。
約 1 年の経験を経て、私のアプローチは以下の 3 つのポイントに集約されました。これらは、あなたが直面する痛みを軽減する度合いの順に並べています:
- 主要な意図の多くをカバーする少数の名詞付き動詞セット
- 次の呼び出しを示唆する出力
- 呼び出し間でも維持されるアドレス指定スキーム – アンカー、ID、パスなど。行番号は除きます。
## コア動詞が表面積に勝る Office サーバーは 100 を超えるツールを公開しています。その get_instructions() は、モデルを以下の 8 つへと誘導します:
*…まずは office_help から始め、次に office_read, office_inspect, office_patch, office_table, office_template, office_audit, word_insert_at_anchor を優先してください。コアフローが不十分な場合のみ、専門ツールはフォールバック、診断、レガシー互換性、またはエキスパート用として扱ってください。*
その一文は、予想以上に大きな役割を果たします。モデルに対して「推奨される道筋が存在する」こと、「その道筋が動詞の形をしている(支援→読書→検査→修正→監査)」こと、そしてそれ以外の機能はすべてオプトイン式であることを伝えます。
これがなければ、モデルは office_read で済むところを word_parse_sow_template を喜んで選び取り、1 つのタスクに対して 5 回の呼び出しが必要となる迂回経路に陥ってしまいます。
そこで私はすぐに、どのツールをいつ表示するかについて、容赦ない判断が必要だと気づきました。専門的なツールは引き続き提供されますが、「専門家向け」という枠組みの下で隠され、一部のレガシーなツールは tools/list 全体からフィルタリングされています。
また、私はアクティベーションセット(activation sets)を自由に活用しています。モデルが「見える」表面は小さく、モデルが「到達できる」表面は大きいです。
## 命名こそが鎖であるこれもまた、モデルは最も可能性が高い(あるいは韻を踏む)ものを連鎖させます。私にとって最も効果的な戦術は、その特性を利用することでした。
すべての Word ツールは word_* で始まり、Excel は excel_*、統合された Office 機能は office_* です。office_inspect を呼び出したモデルは、word_patch_with_track_changes ではなく、プレフィックスが一致するため office_patch を次に選びます。
この特定のサーバーもまた、注釈と、これらのプレフィックスを読み取って readOnlyHint(読み取り専用ヒント)や destructiveHint(破壊的動作の警告)を自動的に割り当てる少しの意図推論ハックを自由に活用しています。つまり、命名の規律が、無料で安全性メタデータへと変換されるのです。
プレフィックスは計画です。動詞はステップです。この投稿全体から一つだけ覚えておいてほしいのは、この概念です…
## Every response nominates the next callこれは、小さなモデルでも動作するようにする唯一の変更点でした。大規模なモデルはツールリストと目標から一連の連鎖を計画しますが、小さなモデルはそうしません–彼らは最初に妥当に見えるツールをつかんで停止してしまいます。
この修正は非常に単純です:すべての応答は、従うべきヒントのパンくずリスト(辞書形式)で終わります。少なくとも next_tools: [...] を含め、現在のツールが次のツールの入力として必要な値を生成した場合は、usage: "<exact call>" も追加します。
スキーマから引数を組み立てられないモデルでも、使用例文字列をそのままコピーできます。実際、彼らはそれを*必ず*コピーします。なぜなら、トークンを埋めていく過程でそれが最も可能性の高い結果であり、その使用例のヒントがモデルが進むべき経路を誘導するからです。
## Discovery as a tool, not documentation私がもう一つ気づいたのは、目印(シグナリング)は curated される必要があるということです。
意図マッピングのページを借用し、office_help(goal=...) は構造化されたレコードを返します–推奨される連鎖と根拠、フォールバック、注意すべき診断文字列、そして一つの命令形による next_step 文です。文章ではありません。README でもスキルリストでもありません。モデルが読解力なしに行動できるデータです。
引数なしで呼び出されるとカタログを返し、未知の目標に対してはエラーを返すのではなく、サポートされているセットを返します。これにより、ワークフローを停止させる可能性のあるエラーを、実際に有用なカタログへと変換します。
## アドレス付け:オフセットではなくアンカー単純なモデルがチェーン操作を遂行できない最大の理由は、呼び出し間の文脈(スレッド)をモデルが失ってしまうことです。「導入部の後に段落を挿入する」という指示は英語としては問題ありませんが、3 つのツール呼び出しにわたってバイトオフセットを記憶することを期待すれば、壊滅的な結果になります。
この特定のシナリオでは、私は少しずるをしました。ほとんどのオフィス文書には見出し(またはセル、あるいは OOXML 内部の構造化パス)があるため、文書からのそのままのテキスト、あるいは移動不可能な座標(ちなみに PowerPoint ではこれが特に困難でした)のいずれかを使用しました。
したがって、提案やヒントに加えて、後でツールが入力として受け付ける識別子を返してください。もしモデルが自然言語であなたに「説明し直さなければならない」データを返しているなら、それはあなたが火曜日の午後に見ていない時に誤作動するチェーンを作ったことになります。
## モードは 1 つのツールを 4 つに変える最初はフォーマットごとに個別の編集ツールを用意しましたが、これは自動化テストを行うには非常に容易でしたが、コンテキスト(文脈)の観点からは極めて非効率でした。そこで一度、初期発見のための処理を大幅に単純化することを決意し、かつすべての出力を検証可能にする必要があるため、利用可能なサブ操作をリスクベースでタグ付けしました。
office_patch は、dry_run(実行前確認)、best_effort(最善の努力)、safe(安全)、strict(厳格)のいずれを要求しても、同じコードパスを実行します。1 つのツール、4 つのモード、tools/list におけるエントリは 1 つです。
発見コストはツールの数に比例して増大し、モードの数には比例しません。また、dry_run → safe → strict という順序は、モデルが指示されることなく自ら見出すエスカレーションチェーンです。
注意深さの違いだけで区別される N 個のツールがある場合、それらを統合してください。皆のコンテキスト予算を無駄にしています。
## 診断をバックエッジとして線形チェーンは簡単です。実際のチェーンにはループがあり、ループが発生するのはサーバーがモデルを再び呼び戻す場合だけです。すべての変更を行うツールは、status(ステータス)、matched_targets(一致したターゲット)、unmatched_targets(未一致のターゲット)、next_tools(次のツール)を含む標準的なエンベロープを返します。
その後、モデルはコンテキスト全体を経由する必要なく「局所的に」少数のオプションに対して分岐し、診断フィールドの名前を指示で再度現れる正確な文字列と一致させることで、それらを強化することができます。
この特定のケースでは、再び私は手抜きをしました。モデルがドキュメントを十分に内省できず、ランダムにツールを呼び出してファイルを破損させ始めたことに気づいたため、私は常に少なくとも 1 つの読み取り専用ツールを提供しました。その結果、「混乱している、もう一度確認する」という場合のペナルティは、破壊的な失敗ではなく、追加の往復通信 1 回だけになります。
- get_instructions() またはローカル equivalent で、5〜10 の中核動詞を選び、名前を付ける
- 表面(surface)ごとに一貫したプレフィックスを使用する
- 推奨事項を文章ではなくデータとして返す発見ツールを提供する
- 発見ツールを閲覧可能にする。引数なしで呼び出せばカタログが、不明な入力でサポートされているセットが表示されるようにする
- すべてのツールのレスポンスに前方のパンくずリスト(フォワード・ブレッドクラム)を組み込む
- アドレスが呼び出し間でも維持されるよう、マップまたはアンカーツールを提供する
- 変更を行うすべてのツールに、dry_run を含むモード列挙型を与える
- 名前付き診断フィールドを返し、回復ツールの参照も行う
- 変更のエンベロープを標準化する。あるツールが特定の方式で何かを変更する場合、他のツールも引数やセマンティクスなどで一貫性を持つようにする
- 不明な引数は厳格に拒否する(これは実行環境によって容易さが異なる)
- モデルが着地できる場所となる監査ツールを提供する
- 回復ループで複数回呼び出されるものはすべてキャッシュする。なぜなら、ツールパスをヒントで慎重に選別していても、実際には数十回呼び出されることになるからだ。
- 繰り返し呼び出しを安全にする。モデルは再試行を行うため、それを許可できるようにする(冪等性は難しく、しばしば不可能である)
schema と説明で地味な作業を行え。推測させるのをやめれば、モデルは喜んで賢い部分をこなしてくれる。
原文を表示
I’ve been building MCP servers for a while now–I wrote about the general approach last year, started out by creating umcp, and I’ve recently opened up an Office server that’s been battered by enough models against enough real documents that the patterns have settled.
I’m still not a fan of MCP, but what follows is what I’ve learned about making tool chains actually work, condensed from swearing at logs rather than reading papers.
Disclaimer:This is a condensed version of CHAINING.md, which was itself stapled together from a bunch of notes in my Obsidian vault. The full version has more code examples and a techniques inventory table that Opus just _had to add, and I’ve since beaten that out of it and restored most of the original text (minus typos).
The short version: the MCP servers I design do most of the work, while the model walks breadcrumbs.
## Models don’t planThey look at the conversation, scan the tool list, and grab whatever looks more probable. That’s it. There is no hidden planner. If you want chains that finish somewhere sensible, the *server* has to make the next call blindingly obvious at every step.
After a year or so, I have pared down my approach into these three things, roughly in order of how much pain they save you:
- A small named core verb set covering most intents
- Output that suggests the next call
- An addressing scheme that survives between calls–anchors, IDs, paths, anything but line numbers.
## Core verbs beat surface areaThe Office server exposes over 100 tools. Its get_instructions() funnels models toward eight:
…start with office_help, then prefer office_read, office_inspect, office_patch, office_table, office_template, office_audit, and word_insert_at_anchor. Treat specialised tools as fallback, diagnostic, legacy-compatibility, or expert tools when the core flow is insufficient.
That single sentence does an outsized amount of work–it tells the model there *is* a recommended path, that the path is verb-shaped (help -> read -> inspect -> patch -> audit), and that everything else is opt-in.
Without it, models cheerfully reach for word_parse_sow_template when office_read would do, and you end up with five-call detours for one-call jobs.
So I quickly realized that I needed to be ruthless about which tools to surface and when. The specialised ones still ship–hidden under a “for experts” framing, and a handful of legacy ones filtered out of tools/list entirely.
I also make liberal use of activation sets–the surface the model *sees* is small; the surface it *can reach* is large.
## Naming is the chainAgain, models chain whatever is most likely (or rhymes), and the most effective tactic, for me, has been taking advantage of that.
All Word tools are word_*, all Excel excel_*, all unified office_*. A model that just called office_inspect will reach for office_patch next, not word_patch_with_track_changes, because the prefix matches.
This particular server also makes liberal use of annotations and a little intent/inferrer hack that reads those prefixes to assign readOnlyHint/destructiveHint automatically, so naming discipline turns into safety metadata for free.
The prefix is the plan. The verb is the step. If you take one thing from this entire post, I’d suggest this notion…
## Every response nominates the next callThis was the single change that made things behave on smaller models. The big ones will plan a chain from a tool list and a goal; the wee ones won’t–they grab the first plausible tool and stop.
The fix is stupid simple: every response ends with a breadcrumb dictionary of hints to follow. At minimum next_tools: [...], plus usage: "<exact call>" whenever the current tool produced a value the next one needs.
A model that can’t assemble arguments from a schema can copy the usage string verbatim. In fact, they *will* copy it, because it is still the most likely outcome as it fills in tokens, and thus those usage hints funnel the path the model takes.
## Discovery as a tool, not documentationAnother thing I hit upon was that signposting needed to be curated.
Borrowing a page from intent mapping, office_help(goal=...) returns a structured record–recommended chain with rationale, fallbacks, diagnostic strings to watch for, one imperative next_step sentence. Not prose. Not a README, not skills. Data the model can act on without reading comprehension.
Called with no arguments, it returns the catalogue. Called with an unknown goal, it returns the supported set rather than an error, which turns a potential workflow-stopping error into an actual useful catalogue.
## Addressing: anchors, not offsetsThe biggest reason simple models can’t follow chains is the model losing the thread between calls. “Insert a paragraph after the introduction” is fine in English but catastrophic if you expect it to remember a byte offset across three tool calls.
In this particular scenario, I cheated and since most Office documents have headings (or cells, or internal structured paths inside OOXML), I used either verbatim text from the document or immovable coordinates (which was particularly hard in PowerPoint, by the way).
So besides suggestions and hints, return identifiers your tools will later accept as input. If you find yourself returning data the model has to *describe back to you* in natural language, you’ve made a chain that will misfire on a Tuesday afternoon when you’re not watching.
## Modes turn one tool into fourI started out with individual editing tools per format, which was very easy to do automated tests for but incredibly wasteful of context, so at one point I decided to make things much simpler for initial discovery, and since I needed to make all outputs auditable, I then tagged available sub-operations risk-wise.
office_patch is the same code path whether you ask for dry_run, best_effort, safe, or strict. One tool, four modes, one entry in tools/list.
Discovery cost scales with tool count, not mode count. And dry_run -> safe -> strict is an escalation chain the model figures out on its own without being told.
If you have N tools that differ only in how cautious they are, collapse them. You’re wasting everyone’s context budget.
## Diagnostics as the back-edgeLinear chains are easy. Real chains have loops, and loops only happen when the server invites the model back in. Every mutating tool returns a standard envelope with status, matched_targets, unmatched_targets, and next_tools.
The model then branches on a small subset of options “locally” without needing to go over the entire context, and if you name the diagnostic fields with exact strings the model will see again in your instructions, it will just reinforce them.
In this particular case, again, I cheated. I figured out that the models were starting to call tools at random because they couldn’t introspect the document well enough and ended up breaking files, so I *always* gave them at least one read-only tool, so the penalty for “I’m confused, let me look again” is one extra round-trip, not a destructive cock-up.
- Pick five to ten core verbs and name them in get_instructions() or your local equivalent
- Use consistent prefixes by surface
- Provide a discovery tool that returns recommendations as data, not prose
- Make the discovery tool browseable–no-arg returns the catalogue, unknown input returns the supported set
- Embed forward breadcrumbs in every tool response
- Provide a map/anchors tool so addresses survive between calls
- Give every mutating tool a mode enum including dry_run
- Return named diagnostic fields and cite the recovery tools
- Standardise the mutation envelope. If one tool changes something in a specific way, make sure the others are consistent (arguments, semantics, etc.)
- Reject unknown arguments strictly (this is much easier in some runtimes than others)
- Provide an audit tool so the model has somewhere to land
- Cache anything the recovery loop calls more than once, because, well, it will get called dozens of times even if you carefully curate paths through your tooling with hints.
- Make repeat calls safe–models retry, and they should be allowed to (idempotence is hard, and often impossible).
Do the boring work in the schema and the descriptions. The model will happily do the clever bit if you stop making it guess.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み