MCPクライアント自作ガイド — 公式チュートリアルでチャットボットを作る
MCPクライアントをPythonとTypeScriptで自作する手順。接続からツール呼び出しの実装、チュートリアルのコードが省いている点、言語別の勘所とつまずきまで扱います。
MCPクライアントを自作するとはどういうことか
MCPクライアントとは、MCPサーバーへの接続を1本持ち、ツールの一覧取得と呼び出しをClaudeとの会話ループに組み込む、ホスト側のプログラムです。自分のアプリケーションにMCP対応を組み込みたいときに書きます。
チュートリアルはPython・TypeScript・Java・Kotlin・C#・Ruby・Rustの7言語で用意されています。どの言語も「質問をClaudeに送り、Claudeが選んだツールをMCPサーバー経由で実行し、結果を返して自然文の回答を得る」流れを実装します。本記事はPythonとTypeScriptを軸に、共通する設計と、言語ごとに変わる部分を扱います。
自作する必要があるのか — 使い分けの判断
クライアントを書く前に、既製のホストで足りないかを確かめます。次の3つに当てはまるかで分かれます。
- すでにClaude Code上で完結する用途なら、
claude mcp addでサーバーを登録するだけです。クライアントを書く必要はありません(手順はClaude Code MCP設定ガイド)。 - Claudeの標準的なエージェントループにMCPサーバーを組み合わせたいだけなら、Agent SDKが接続処理を内蔵しています。構成はAgent SDK MCP接続ガイドにあります。
- 独自のホストアプリケーションを作る、あるいはMCPプロトコルそのものの挙動を手元で確かめたい場合は、本記事の手順が向きます。
動作確認には、サーバー側の応答をそのまま覗けるMCP Inspectorが役立ちます。複数のサーバーを束ねて大規模に運用する段階では、MCPホストのスケーリングパターンも参考になります。
始める前に — サーバー側が公開するもの
サーバーはResources(ファイルのようなデータ)・Tools(LLMが呼び出せる関数)・Prompts(定型テンプレート)の3種類を公開できます。クライアントのチュートリアルは、このうちToolsだけを使う構成です。
自作サーバーの作り方はMCPサーバー自作ガイドで扱っています。先にサーバー側を書いておくと、クライアントのコードで何が起きているかを追いやすくなります。MCP自体の全体像はMCPとはにあります。
手順1 — 開発環境とAPIキーを準備する
言語ごとのプロジェクトを作り、Anthropic APIキーを設定します。Python版はMCP Python SDKの2.0.0以上とuvを要求し、TypeScript版はNode.js 20以上を要求します。
# Python版のセットアップ
uv init mcp-client && cd mcp-client
uv venv && source .venv/bin/activate
uv add mcp anthropic python-dotenv
touch client.py# TypeScript版のセットアップ
mkdir mcp-client-typescript && cd mcp-client-typescript
npm init -y
npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
npm install -D @types/node typescriptAPIキーは.envファイルに書き、.gitignoreに追加してからコードへ読み込みます。
echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
echo ".env" >> .gitignore手順2 — Clientオブジェクトでサーバーに接続する
Python版ではClientが接続そのものを表します。ツール一覧の取得・呼び出し・リソース読み取りは、すべてこのオブジェクトのメソッドです。async withブロックに入るとサーバーが起動してプロトコルのバージョンを取り決め、出ると切断してサブプロセスも終了します。connectとcloseの対を書く必要がありません。
async with Client(stdio_client(server_params(sys.argv[1]))) as client:
tool_list = await client.list_tools()
tool_names = [tool.name for tool in tool_list.tools]
print("Connected to server with tools:", tool_names)
await chat_loop(client)TypeScript版はClientクラスとStdioClientTransportを分け、接続後にlistTools()でツール定義を取得します。Clientの生成時に名前とバージョン(チュートリアルではmcp-client-cliと1.0.0)を渡す点も、Python版にない手順です。
this.transport = new StdioClientTransport({ command, args: [serverScriptPath] });
await this.mcp.connect(this.transport);
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
}));どちらもファイル拡張子で接続先を判定し、サーバーをサブプロセスとして起動します。通信はstdio(標準入出力)で、ネットワークは経由しません。ただし起動コマンドは同じではありません。Python版は.pyならpython、.jsならnodeを固定で使います。TypeScript版のPythonサーバーは、Windowsならpython、それ以外ならpython3で起動します。JavaScriptのサーバーは、今動いているNode.js自身(process.execPath)が起動します。
手順3 — クエリ処理とツール呼び出しを実装する
中核は、質問をClaudeに送り、Claudeがツールを使うと判断したら実際に呼び出し、その結果を会話に足してもう一度Claudeに聞く往復です。Python版の骨格は次の形です。
response = anthropic.messages.create(
model=MODEL, max_tokens=1000, messages=messages, tools=available_tools
)
for content in response.content:
if content.type == "tool_use":
result = await client.call_tool(content.name, content.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": content.id,
"content": "\n".join(b.text for b in result.content if isinstance(b, TextContent)),
"is_error": result.is_error,
})見落としやすいのが、ツールが失敗したときの扱いです。call_toolは例外を投げません。失敗したツールも成功と同じCallToolResultとして返り、is_errorが立つだけです。このフラグをtool_resultにそのまま渡すと、Claudeは失敗の内容を読んで別の手段を試せます。例外処理と同じ感覚で書くと、失敗を拾えないまま話が進みます。
モデルIDはコード冒頭の定数MODELにまとまっています。チュートリアルの値はclaude-opus-5です。TypeScript版は同じIDを2か所に直接書いているので、変えるときは両方を直します。
手順4 — 会話1往復の流れ
質問1件が処理される流れは次の7ステップです。
質問1件が処理される流れ
- 1
ツール一覧を取得する
クライアントが接続済みのサーバーからツール一覧を取り出します。
- 2
質問とツール定義をClaudeへ送る
ユーザーの質問と一緒にツールの説明を渡します。
- 3
Claudeがツールを選ぶ
使うツールと引数がClaudeの応答に入ります。
- 4
サーバー経由で実行する
クライアントがMCPサーバーにツールを呼ばせます。
- 5
結果をClaudeへ戻す
実行結果を会話に足して、もう一度Claudeに渡します。
- 6
Claudeが自然文にまとめる
ツールの結果を踏まえた回答が返ります。
- 7
ユーザーに表示する
クライアントが回答を画面に出します。
手を入れやすいのは3〜5の間です。チュートリアルが挙げるカスタマイズの入口は3つあります。
- ツール処理:
process_queryを書き換えて、特定のツール種別だけ独自のエラー処理をする - レスポンス処理: ツール結果の整形やフィルタリング、ロギングの追加
- インターフェース: CLIからGUIやWeb UIへの載せ替え、コマンド履歴や補完
チュートリアルのコードが省いている点
チュートリアルのコードは最小構成です。そのまま製品に持ち込むと困る点が、コードを読むと見えてきます。2言語の実装は、同じ流れでも細部が違います。
PythonとTypeScriptの実装差
Python版
複数のツール呼び出しの結果をtool_resultとしてまとめ、直前のassistantの応答とともに会話へ足します。2回目の呼び出しにもtoolsを渡し、is_errorも引き継ぎます。
TypeScript版
ツール結果を通常のユーザー発言(テキスト)として会話へ足し、2回目の呼び出しにはtoolsを渡しません。isErrorは読んでいません。
どちらにも共通する限界が2つあります。
- 追加のツール呼び出しを処理しない: 2回目の応答で読むのはテキストだけです。Claudeが結果を見たうえで別のツールを使おうとしても、その要求は無視されます。複数段のツール利用が必要な用途では、往復を繰り返す形に書き換えます。
- 会話が1質問で終わる:
process_queryは呼ぶたびにmessagesを作り直すので、前の質問は次の質問に引き継がれません。会話の文脈を持たせるには、messagesを呼び出し側で持ち続けます。
複数段のツール利用に対応する骨格は、stop_reasonがtool_useでなくなるまで繰り返す形です。
while True:
response = anthropic.messages.create(
model=MODEL, max_tokens=1000, messages=messages, tools=available_tools
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
# tool_use ブロックごとに call_tool を実行し、tool_results を作る
messages.append({"role": "user", "content": tool_results})Python版の入力まわりにも工夫があります。組み込みのinput()は処理をブロックするため、asyncio.to_threadで別スレッドに逃がしています。入力待ちの間も、イベントループが接続の面倒を見続けられます。標準入力が閉じられた場合(EOFError)は終了し、1回の質問が失敗してもError:を表示してセッションは続きます。
対応言語ごとの実装の違い
自分のスタックに近い行だけ確認すれば足ります。
| 言語 | 主なパッケージ | 接続方式 | 前提・備考 |
|---|---|---|---|
| Python | 主なパッケージmcp / anthropic | 接続方式async with Client(...) | 前提・備考uv、SDK 2.0.0以上 |
| TypeScript | 主なパッケージ@modelcontextprotocol/client | 接続方式Client + StdioClientTransport | 前提・備考Node.js 20以上。cleanup()でclose() |
| Java | 主なパッケージSpring AI MCPスターター | 接続方式application.ymlで宣言的に設定 | 前提・備考Java 17以上。Streamable HTTPにも対応 |
| Kotlin | 主なパッケージio.modelcontextprotocol:kotlin-sdk | 接続方式AutoCloseable実装のラッパークラス | 前提・備考JDK 11以上。サーバーを自前で起動 |
| C# | 主なパッケージModelContextProtocol(prerelease) | 接続方式McpClient.CreateAsync(transport) | 前提・備考.NET 8.0以上。Microsoft.Extensions.AIと統合 |
| Ruby | 主なパッケージmcp gem | 接続方式MCP::Client.new(transport:).connect | 前提・備考Ruby 3.2.0以上。.rbのサーバーも起動できる |
| Rust | 主なパッケージrmcp / genai | 接続方式cargo newで開始 | 前提・備考最新安定版のRustとCargo |
Java版だけ毛色が違い、Spring AIのオートコンフィグレーションでapplication.ymlにサーバー設定を書きます。サンプルはBrave Searchサーバーと組み合わせたチャットボットで、streamable-http.connectionsにURLを書けばリモートサーバーにも接続できます。WebFlux版のスターターを使うStreamable HTTPは、本番向けに勧められています。
RustはClaudeへの要求にgenaiクレート、MCPの接続にrmcpクレートと、役割ごとに別のライブラリを使います。Claudeの呼び出しに別クレートのgenaiを使う点が、ほかの言語との違いです。
よくあるつまずき
症状から原因を絞ります。
- サーバーパスが通らない: 相対パスで動かないときは絶対パスに切り替えます。Windowsはスラッシュ(
/)か、エスケープしたバックスラッシュ(\\)のどちらかに統一し、混在させません。Python版とTypeScript版は.pyか.js、Ruby版は.rb・.py・.jsのいずれかでないと、サーバー起動前にエラーになります。Kotlin版は.jar、C#版は.csprojやディレクトリも受け付けます。 FileNotFoundError: サーバーのパスを疑います。Errno::ENOENT(Ruby): サーバーのパスと、ruby・python3・nodeといった起動コマンドが使える状態かを確かめます。Cannot find module(TypeScript): ビルドフォルダを確かめ、TypeScriptのコンパイルが成功しているかを見ます。Connection refused: サーバーが起動していないか、パスが誤っています。Tool execution failed: ツールが必要とする環境変数が設定されているかを確かめます。- タイムアウト: Python版では
Clientのread_timeout_secondsを引き上げます。ツールの処理に時間がかかるときは、この値を引き上げることを検討します。 - 最初の応答が遅い: サーバーの起動・Claudeの推論・ツール実行が重なり、初回は30秒ほどかかることがあります。2回目以降は速くなるので、待っている間に中断しないでください。
ツール名にも仕様上の制約があります。使える文字種や長さの形式から外れた名前は、クライアント側のバリデーションで弾かれることがあります。命名規則とサーバー間の名前衝突対策はMCPツール名の命名規則とサーバー間衝突の防ぎ方にまとめています。原因不明のエラーが出たら、サーバーが公開するツール名がこの規則に沿っているかも確かめます。
セキュリティで気をつけること
APIキーを.envで管理するだけでは足りません。チュートリアルは、サーバーからの応答を検証すること、ツールに渡す権限を絞ることを共通のベストプラクティスに挙げています。
接続先のサーバーが何者であっても、返ってくるツール一覧やツール結果は外部から届いた入力です。データ流出や権限昇格を狙う攻撃ベクトルを念頭に、ファイル書き込みや外部送信へそのまま使わず、間に検証を挟みます。
まとめ
サーバー側がis_errorで失敗を返し、チュートリアルのコードが1往復で終わる以上、製品に載せる前に直すのは「失敗の引き継ぎ」と「複数段のツール呼び出し」の2点です。既製のホストやAgent SDKで足りるなら、自作は見送る選択もあります。