MCPの開発背景
MCPの誕生は、prompt engineeringが新たな発展段階に入ったことを象徴しています。より構造化されたコンテキスト情報を提供することで、モデルの能力が大幅に向上しました。promptを設計する際には、より具体的な情報(ローカルファイル、データベースの内容、またはネットワークのリアルタイムデータなど)を統合し、モデルが実際の問題をよりよく理解して解決できるようにすることを目指しています。
従来方法の限界
MCPがなかった時代には、複雑な問題を解決するために、以下のことをしなければなりませんでした。
- 手動でデータベースから情報を選別する
- ツールを使用して関連情報を検索する
- 選別した情報を1つずつpromptに追加する
この方法は、簡単な問題(大規模モデルに要約を行わせるなど)を処理する場合には効果的ですが、問題の複雑さが増すにつれて、対応が困難になってきました。
Function Callの登場
これらの課題を克服するために、多くの大規模言語モデル(LLM)プラットフォーム(OpenAIやGoogleなど)がfunction call機能を導入しました。この仕組みにより、モデルは必要に応じて事前定義された関数を呼び出してデータを取得したり、特定の操作を実行したりすることができ、自動化の程度が大幅に向上しました。
Function Callの限界
しかしながら、function callにも明らかな限界があります。
- プラットフォームへの高い依存度
- 異なるLLMプラットフォーム間でのAPI実装の違い
- 開発者がモデルを切り替える際にはコードを書き直さなければならず、開発コストが増加する
- セキュリティや対話性などの面での課題
MCPの設計理念
実際には、データやツールは常にそこに存在しています。重要なのは、それらをより賢く、より統一的にモデルに接続する方法です。Anthropicはこのようなニーズに基づいてMCPを設計し、AIモデルの「万能アダプター」として、LLMがデータにアクセスしたり、ツールを呼び出したりできるようにしました。
MCPの利点
モデルがどのようにしてAgent/ツールを賢く選択するか
MCPの核心は、複数のツールを簡単に呼び出せるようにすることです。では、LLM(モデル)はいつどのツールを使用するかを決定するのでしょうか?
ツール選択の流れ
Anthropicは詳細な説明を提供しています。ユーザーが質問をすると、以下のような流れでツールが選択されます。
- クライアント(Claude Desktop/Cursor)が質問をLLMに送信する
- LLMが利用可能なツールを分析し、どのツール(または複数のツール)を使用するかを決定する
- クライアントがMCP Serverを通じて選択されたツールを実行する
- ツールの実行結果がLLMに返される
- LLMが実行結果を組み合わせ、要約した後、自然言語でユーザーに表示する
モデルはどのようにしてどのツールを使用するかを決定するのか?
MCP公式が提供するclient exampleコードを分析することで、モデルはpromptに基づいて現在利用可能なツールを識別していることがわかります。具体的な方法は以下の通りです。
- 各ツールの使用説明をテキスト形式でモデルに渡す
- モデルがどのツールが利用可能かを把握できるようにする
- リアルタイムの状況に基づいて最適な選択を行う
重要なコード分析
async def start(self):
# すべての mcp server を初期化する
for server in self.servers:
await server.initialize()
# すべての tools を取得して all_tools と命名する
all_tools = []
for server in self.servers:
tools = await server.list_tools()
all_tools.extend(tools)
# すべての tools の機能説明を文字列にフォーマットして LLM で使用できるようにする
tools_description = "\\n".join(
[tool.format_for_llm() for tool in all_tools]
)
# LLM(Claude)にどのツールを使用すべきかを尋ねる
system_message = (
"あなたはこれらのツールにアクセスできる役に立つアシスタントです。\\n\\n"
f"{tools_description}\\n"
"ユーザーの質問に基づいて適切なツールを選択してください。 "
"ツールが必要ない場合は、直接返信してください。\\n\\n"
"重要: ツールを使用する必要がある場合は、以下の正確なJSONオブジェクト形式でのみ返信してください。他のものは何も返さないでください。\\n"
"{\\n"
' "tool": "tool-name",\\n'
' "arguments": {\\n'
' "argument-name": "value"\\n'
" }\\n"
"}\\n\\n"
"ツールの応答を受け取った後:\\n"
"1. 生データを自然な会話形式の応答に変換する\\n"
"2. 応答を簡潔で分かりやすく保つ\\n"
"3. 最も関連する情報に焦点を当てる\\n"
"4. ユーザーの質問の適切なコンテキストを使用する\\n"
"5. 生データを単に繰り返さないようにする\\n\\n"
"上で明示的に定義されたツールのみを使用してください。"
)
messages = [{"role": "system", "content": system_message}]
while True:
# ここではユーザーメッセージ入力が処理されたと仮定する
messages.append({"role": "user", "content": user_input})
# system_message とユーザーメッセージ入力を一緒に LLM に送信する
llm_response = self.llm_client.get_response(messages)
ツールのフォーマット
ツールの説明情報はどのようにフォーマットされるのでしょうか?
class Tool:
"""ツールのプロパティとフォーマットを表すクラスです。"""
def __init__(
self, name: str, description: str, input_schema: dict[str, Any]
) -> None:
self.name: str = name
self.description: str = description
self.input_schema: dict[str, Any] = input_schema
# ツールの名前、ツールの用途(description)、およびツールに必要なパラメータ(args_desc)をテキストに変換する
def format_for_llm(self) -> str:
"""ツール情報を LLM 用にフォーマットします。
Returns:
ツールを説明するフォーマットされた文字列。
"""
args_desc = []
if "properties" in self.input_schema:
for param_name, param_info in self.input_schema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', '説明なし')}"
)
if param_name in self.input_schema.get("required", []):
arg_desc += " (必須)"
args_desc.append(arg_desc)
return f""" ツール: {self.name} 説明: {self.description} 引数: {chr(10).join(args_desc)} """
ツール情報のソース
ツールの説明とinput_schemaはどこから来るのでしょうか?MCPのPython SDKソースコードを分析することで、以下のことがわかります。
ほとんどの場合、@mcp.tool()
デコレータを使用して関数を装飾すると、対応する名前や説明などは直接、ユーザーが定義した関数の関数名や関数のdocstringなどから取得されます。
@classmethod
def from_function(
cls,
fn: Callable,
name: str | None = None,
description: str | None = None,
context_kwarg: str | None = None,
) -> "Tool":
"""関数からToolを作成します。"""
func_name = name or fn.__name__ # 関数名を取得する
if func_name == "<lambda>":
ValueError("ラムダ関数には名前を指定してください")
func_doc = description or fn.__doc__ or "" # 関数のdocstringを取得する
is_async = inspect.iscoroutinefunction(fn)
まとめ:モデルはprompt engineering、つまりすべてのツールの構造化された説明とfew-shotの例を提供することで、どのツールを使用するかを決定します。一方、AnthropicはClaudeに対して特別な訓練を行っていることは間違いありません。自社のプロトコルであるため、Claudeはツールのpromptをより理解し、構造化されたtool call jsonコードを出力することができます。
ツールの実行と結果のフィードバックメカニズム
ツールの実行は比較的簡単で直接的です。前のステップを受けて、system prompt(命令とツール呼び出しの説明)とユーザーメッセージを一緒にモデルに送信し、その後モデルの返信を受け取ります。
実行の流れ
モデルがユーザーの要求を分析した後、ツールを呼び出す必要があるかどうかを決定します。
- ツールが必要ない場合: モデルは直接自然言語の返信を生成する
- ツールが必要な場合: モデルは構造化されたJSON形式のツール呼び出し要求を出力する
返信に構造化されたJSON形式のツール呼び出し要求が含まれている場合、クライアントはこのjsonコードに基づいて対応するツールを実行します。
コードの実装
async def start(self):
# 上で説明した通り、モデルがどのようにツールを選択するか
while True:
# ここではユーザーメッセージ入力が処理されたと仮定する
messages.append({"role": "user", "content": user_input})
# LLM の出力を取得する
llm_response = self.llm_client.get_response(messages)
# LLM の出力を処理する(tool call がある場合は対応するツールを実行する)
result = await self.process_llm_response(llm_response)
# result が llm_response と異なる場合、tool call が実行されたことを意味する(追加情報がある)
# その場合は、tool call の結果を再度 LLM に送信して処理させる
if result != llm_response:
messages.append({"role": "assistant", "content": llm_response})
messages.append({"role": "system", "content": result})
final_response = self.llm_client.get_response(messages)
logging.info("\\nFinal response: %s", final_response)
messages.append(
{"role": "assistant", "content": final_response}
)
# そうでない場合は、tool call が実行されていないことを意味するので、LLM の出力を直接ユーザーに返す
else:
messages.append({"role": "assistant", "content": llm_response})
エラー処理
tool callのjsonコードに問題がある場合、またはモデルが幻覚を起こした場合、システムは無効な呼び出し要求をスキップします。
結論と実践上の提案
上記の原理分析から、ツールドキュメントは非常に重要であることがわかります。モデルはツールの説明テキストに依存して、適用可能なツールを理解して選択します。これは、丁寧に書かれたツール名、ドキュメント文字列(docstring)、およびパラメータの説明が特に重要であることを意味します。
MCPの選択メカニズムはpromptに基づいて実装されているため、理論的には、対応するツールの説明を提供できるあらゆるモデルはMCPと互換性があり、使用することができます。