Claude Media
MCPクライアント自作ガイド — 公式チュートリアルでチャットボットを作る

MCPクライアント自作ガイド — 公式チュートリアルでチャットボットを作る

MCPクライアントをPythonとTypeScriptで自作する手順。Client接続からツール呼び出しループの実装、言語別の勘所とよくあるつまずきまで扱います。

MCPクライアントを自作するとはどういうことか

MCPクライアントとは、MCPサーバーへの接続を1本持ち、ツールの一覧取得と呼び出しをClaudeとの会話ループに組み込む、ホスト側のプログラムです。既製のホスト(Claude CodeやClaude Desktop)を使わず、自分のアプリケーションにMCP対応を組み込みたいときに書きます。

公式チュートリアルはPython・TypeScript・Java・Kotlin・C#・Ruby・Rustの7言語で用意されています。いずれも「ユーザーの質問をClaudeに送り、Claudeが選んだツールをMCPサーバー経由で実行し、結果をClaudeに返して自然文の回答を得る」という同じ流れを実装します。本記事はPythonとTypeScriptの実装を軸に、7言語に共通する設計と、言語ごとに変わる部分を扱います。

始める前に確認すること — サーバー側の役割との切り分け

クライアントを書く前に、MCPサーバー側が何を提供しているかを押さえておくと実装の見通しが良くなります。サーバーはResources(ファイルのようなデータ)・Tools(LLMが呼び出せる関数)・Prompts(定型テンプレート)の3種類を公開できます。多くのクライアントチュートリアルは、このうちToolsだけを使う構成で書かれています。

自作サーバーの作り方はMCPサーバー自作ガイドで扱っています。まだサーバー側を書いたことがない場合は、先にそちらに目を通すとクライアント側のコードで何が起きているかが掴みやすくなります。MCP自体の全体像(仕組み・採用状況・Claudeでの使い方)は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 typescript

APIキーは.envファイルに書き、.gitignoreに追加してからコードへ読み込みます。

echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
echo ".env" >> .gitignore

手順2 — Clientオブジェクトでサーバーに接続する

Python版ではClientが接続そのものを表すオブジェクトで、ツール一覧の取得・呼び出し・リソース読み取りはすべてこの1つのオブジェクトのメソッドとして呼び出します。接続の開始と終了は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()でツール定義を取得します。

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,
}));

どちらの言語でも、接続先のサーバーがPythonスクリプト(.py)かNode.jsスクリプト(.js)かをファイル拡張子から判定します。そのうえで対応するコマンド(python / node)により、サーバーをサブプロセスとして起動します。サーバーとの通信はstdio(標準入出力)で、ネットワークを経由しません。

手順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フラグが立つだけです。このフラグをそのままClaudeへのtool_resultに渡すことで、Claudeは失敗を読み取って別の手段を試せます。ここを普通の例外処理と同じ感覚で書くと、ツール失敗時に会話がそのまま止まってしまいます。

手順4 — 会話1往復の全体像とカスタマイズの余地

質問1件が処理される流れは次の7ステップです。

  1. クライアントが接続済みサーバーからツール一覧を取得する
  2. ユーザーの質問とツール定義を一緒にClaudeへ送る
  3. Claudeがどのツールを使うか判断する
  4. 判断されたツールをMCPサーバー経由で実行する
  5. 結果をClaudeに送り返す
  6. Claudeが自然文で回答をまとめる
  7. 回答をユーザーに表示する

このうち手を入れやすいのは3〜5の間です。公式チュートリアルが挙げているカスタマイズの入口は3つあります。

  • ツール処理: process_queryを書き換えて、特定のツール種別だけ独自のエラー処理をする
  • レスポンス処理: ツール結果の整形やフィルタリング、ロギングの追加
  • インターフェース: CLIのままにせずGUIやWeb UIに載せ替える、コマンド履歴や補完を足す

最小構成のチャットループから始め、必要になった箇所だけこの3方向に広げていくのが公式が想定する育て方です。

対応言語ごとの実装の違い

7言語のチュートリアルは同じ流れを実装していますが、パッケージと接続の書き方は言語ごとに変わります。自分のスタックに近い列だけ確認すれば十分です。

言語主なパッケージ接続方式備考
Python主なパッケージmcp / anthropic接続方式async with Client(...)備考connect/closeが不要な設計
TypeScript主なパッケージ@modelcontextprotocol/client接続方式Client + StdioClientTransport備考cleanup()で明示的にclose()
Java主なパッケージSpring AI MCPスターター接続方式application.ymlで宣言的に設定備考Streamable HTTPにも対応
Kotlin主なパッケージio.modelcontextprotocol:kotlin-sdk接続方式AutoCloseable実装のラッパークラス備考サーバーをプロセスとして自前管理
C#主なパッケージModelContextProtocol(prerelease)接続方式McpClient.CreateAsync(transport)備考Microsoft.Extensions.AIと統合
Ruby主なパッケージmcp gem接続方式MCP::Client.new(transport:).connect備考拡張子で.rb判定も追加
Rust主なパッケージrmcp crate(MCP SDK+子プロセストランスポート) / genai crate(Claudeへのリクエスト)接続方式cargo newで開始備考最新安定版のRust/Cargoが必要

Java版だけ毛色が異なり、Spring AIのオートコンフィグレーションを使ってapplication.ymlにサーバー設定を書く宣言的なスタイルです。ローカルのstdioサーバーだけでなく、streamable-http.connectionsでリモートサーバーへの接続もプロパティで指定できます。

よくあるつまずき

サーバーパスの指定ミスはよくあるつまずきです。相対パスが通らないときは絶対パスに切り替えます。Windowsではスラッシュ(/)かエスケープしたバックスラッシュ(\\)のどちらかで統一し、混在させないことが公式の推奨です。

初回応答は遅くなりがちです。サーバーの起動・Claudeの推論・ツール実行が重なるため、最大30秒ほどかかることがあります。2回目以降は速くなるので、初回のタイムアウトだけ長めに設定するのが実用的です。

タイムアウトエラーが出た場合、公式はClient側のread_timeout_seconds(Python版)を引き上げることを挙げています。ツールの処理が本質的に重い場合は、まずこの値を疑います。

Connection refusedはサーバーが起動していないかパスが誤っているとき、Tool execution failedはツールが必要とする環境変数が未設定のときに出やすいエラーです。

ツール名にも仕様上の制約があります。MCP仕様はツール名のフォーマット(使える文字種や長さ)を定義しており、自作サーバー側でこの形式から外れた名前を付けると、クライアント側のバリデーションで弾かれます。接続時に原因不明のエラーが出たら、まずサーバーが公開しているツール名がこの命名規則に沿っているかを疑うとよいでしょう。

セキュリティで気をつけること

APIキーを.envで管理するだけでなく、公式チュートリアルは2点を共通のベストプラクティスとして挙げています。1つはサーバーからの応答を鵜呑みにせず検証すること、もう1つはツールに渡す権限を慎重に絞ることです。自作したクライアントは、接続先のMCPサーバーが何者であっても同じ扱いをします。サーバーが返すツール一覧やツール結果は、外部から届いた入力として扱い、そのままファイル書き込みや外部送信に使わないよう検証を挟みます。

自作クライアントとClaude Code / Agent SDKとの使い分け

ここまでの手順は「自分のアプリケーションにMCPクライアントを組み込む」ケース向けです。すでにClaude Code上で完結する用途なら、claude mcp addでサーバーを登録するだけで済みます。クライアントを自分で書く必要はありません(詳細はClaude Code MCP設定ガイド)。

Node.jsやPythonでエージェントを自作していて、Claudeの標準的なエージェントループにMCPサーバーを組み込みたいだけの場合も事情が違います。Agent SDKが接続処理をすでに内蔵しているためです(構成はAgent SDK MCP接続ガイド)。本記事の手順が向くのは、Agent SDKを使わない独自のホストアプリケーション、あるいはMCPプロトコルそのものの挙動を手元で確認したい場合です。

自作したクライアントの動作確認には、サーバー側の応答をそのまま覗けるMCP Inspectorが役立ちます。複数のMCPサーバーを束ねて自作クライアントを大規模に運用する段階になったら、MCPホストのスケーリングパターンも参考になります。

まとめ

MCPクライアントの実装は、突き詰めると「サーバーからツール一覧を取る」「Claudeにツール定義を渡す」「Claudeが選んだツールを実行して結果を返す」の3ステップの繰り返しです。言語ごとの違いは接続オブジェクトの書き方とライフサイクル管理に集約され、ロジックの骨格はPythonでもTypeScriptでも変わりません。まずは公式チュートリアルの言語を1つ選んで動かし、is_errorの扱いとタイムアウト設定だけ自分のユースケースに合わせて調整するところから始めるのが近道です。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →