【Claude Code 連載 第84回】MCP で外部ツールに接続する(agent-sdk/mcp)

スポンサーリンク
【Claude Code 連載 第84回】MCP で外部ツールに接続する(agent-sdk/mcp) 用語解説
【Claude Code 連載 第84回】MCP で外部ツールに接続する(agent-sdk/mcp)
この記事は約43分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

公式ドキュメント:https://code.claude.com/docs/ja/agent-sdk/mcp

1. 一言でいうと

Agent SDK で作ったエージェントに、外部の MCP サーバー(GitHub、Slack、データベースなど)をつないで、Claude がそのツールを使えるようにする方法を定めたページです。サーバーの起動方法(ローカルのコマンドか、URL か)の指定、ツールの許可、認証情報の渡し方、接続に失敗したときの見分け方までを扱います。

MCP(Model Context Protocol)は、AI エージェントを外部のツールやデータにつなぐための公開された標準です。前回(第83回)は、自分のアプリの中で動く MCP サーバーを作りました。今回は、すでに誰かが作って公開している MCP サーバーを、自分のエージェントから使う側の話です。自分でツールを実装しなくても、データベースへの問い合わせや GitHub・Slack との連携ができるようになります。

なお、このページは Agent SDK での設定の話です。Claude Code の CLI に MCP サーバーを追加して全プロジェクトで使う方法(インストールスコープ)は、CLI 側の MCP のページ(第8回)の範囲です。


2. どういう場面で役立つか

シーン1:GitHub のイシューを要約する社内ボット

「このリポジトリの最近のイシューを3件教えて」と頼むと、エージェントが GitHub の MCP サーバーを通じてイシューを取得し、要約して返します。GitHub の API を叩くコードを自分で書く必要はなく、接続先の URL とアクセストークンを設定するだけです。

シーン2:データベースに自然言語で質問する

「先週の新規登録者数を日別に」と頼むと、Claude が SQL を書き、データベースの MCP サーバーがそれを実行して結果を返します。原文の例では、MCP サーバー側を読み取り専用に設定し、Claude が誤って書き込みの SQL を出してもデータが変わらないようにしています。

シーン3:プロジェクトごとに接続先を切り替える

リポジトリの直下に .mcp.json を置いておけば、そのプロジェクトで動かしたときだけ決まった MCP サーバーに接続させられます。接続先の定義をコードではなく設定ファイルで管理できます。

不要・向かないケース

  • 自分の業務ロジックを Claude に使わせたい:公開されたサーバーではなく、第83回のカスタムツール(インプロセスの SDK サーバー)を作るほうが早い場合があります。
  • ブラウザでのログイン(OAuth)が必要なサーバーを、何もせずにつなぎたい:SDK はブラウザを開いて OAuth の手続きをしてくれません。アプリ側で手続きを済ませてトークンを渡す必要があります(後述)。
  • Claude Code を対話で使っていて、全プロジェクトで同じサーバーを使いたいだけ:CLI の MCP 設定で足ります。

3. コードと仕組みの解説

原文の fenced コードブロックは34本です。Python と TypeScript の対訳は原則として両方載せますが、次のように切り分けます。

  • 引用(20本):クイックスタート、コード内での設定、.mcp.json、利用可能なツールの確認、.mcp.json での認証(環境変数・ヘッダー)、OAuth 後のトークン受け渡し、GitHub と データベースの完全な例(準備のコマンドと設定ファイルを含む)、エラー処理。
  • 表と散文に圧縮(14本):設定の断片だけを示す対訳6組12本(許可リストの書き方、stdio・SSE・HTTP の各設定、環境変数の渡し方、トラブルシューティングの許可設定)と、トラブルシューティングの「failed の確認」対訳2本。前者の TypeScript 側は、ドキュメントの表示用に const _ = {...} という包みで囲まれていて、そのまま引用すると読者を混乱させます。中身は同じページの完全な例や .mcp.json の例と同じ形なので、違う部分だけを表にしました。後者はエラー処理の完全な例の一部です。

引用したコードは原文から機械的に抜き出したもので、入れ子に由来する先頭インデントを外した以外は変えていません。

3-0. 前提

バージョン要件は1つだけです。

挙動必要バージョン
最初のターンの待ち時間を CLAUDE_CODE_MCP_STARTUP_WAIT_MS で自分で決めるClaude Code v2.1.274 以降

MCP ツールは、使う前に明示的な許可が必要です。 許可がないと、Claude はツールがあることは分かっても呼び出せません。どのサーバーのどのツールを許可するかは allowedTools で指定します(3-4)。

.mcp.json は既定で読み込まれます。 プロジェクトの設定ソースは既定の query() オプションで有効なので、作業ディレクトリに .mcp.json があれば、コードで何も書かなくてもそのサーバーに接続しにいきます。第75回で扱った claude -p の「信頼していないフォルダでも .mcp.json に接続する」問題と同じ構図で、他人のリポジトリで SDK のエージェントを動かすときは注意が要ります。settingSources を明示的に指定して "project" を外せば読み込まれません(第79回。この注意書きは筆者の対応づけ)。

3-1. クイックスタート

Claude Code のドキュメントを提供する MCP サーバーに HTTP でつなぎ、そのサーバーの全ツールを許可する例です。

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Use the docs MCP server to explain what hooks are in Claude Code",
  options: {
    mcpServers: {
      "claude-code-docs": {
        type: "http",
        url: "https://code.claude.com/docs/mcp"
      }
    },
    allowedTools: ["mcp__claude-code-docs__*"]
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            "claude-code-docs": {
                "type": "http",
                "url": "https://code.claude.com/docs/mcp",
            }
        },
        allowed_tools=["mcp__claude-code-docs__*"],
    )

    async for message in query(
        prompt="Use the docs MCP server to explain what hooks are in Claude Code",
        options=options,
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

エージェントはドキュメントのサーバーにつなぎ、hooks について調べて結果を返します。mcpServers のキー claude-code-docs がサーバー名になり、allowedTools の mcp__claude-code-docs__* はそのサーバーの全ツールを意味します。名前の付き方は第83回で見たカスタムツールと同じです。

3-2. MCP サーバーを追加する

追加の方法は2つあります。query() を呼ぶときにコードで渡す方法と、.mcp.json ファイルに書いて設定ソース経由で読み込ませる方法です。

コードで渡す場合。 mcpServers オプションに直接書きます。次の例は、/Users/me/projects を対象にしたローカルのファイルシステム用 MCP サーバーを起動します。パスは自分のマシンのディレクトリに置き換えます。

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "List files in my project",
  options: {
    mcpServers: {
      filesystem: {
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
      }
    },
    allowedTools: ["mcp__filesystem__*"]
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            "filesystem": {
                "command": "npx",
                "args": [
                    "-y",
                    "@modelcontextprotocol/server-filesystem",
                    "/Users/me/projects",
                ],
            }
        },
        allowed_tools=["mcp__filesystem__*"],
    )

    async for message in query(prompt="List files in my project", options=options):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

設定ファイルから読み込む場合。 プロジェクトの直下に .mcp.json を作ります。このファイルはプロジェクトの設定ソースが有効なときに読み込まれ、既定の query() オプションでは有効です。settingSources を明示的に指定する場合は、"project" を含めないと読み込まれません。

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    }
  }
}

コードでの書き方と .mcp.json での書き方はほぼ同じ形です。違いは、コードでは言語の変数(process.env.X や os.environ[...])で値を埋め込み、.mcp.json では ${X} という記法で実行時に環境変数を展開する点です(3-7)。

3-3. 接続のタイミング

MCP サーバーへの接続には時間がかかります。そのため、「Claude が最初の返答を始める前に、どのサーバーの接続をどれだけ待つか」が細かく決められています。待ち終わった時点のサーバーの状態が、init メッセージ(セッションの最初に届くシステムメッセージ)で報告されます。

options.mcpServers で渡したサーバーは、種類によって次のように扱われます。

サーバーの種類最初のターンを待たせるか最初のターンの待ちの期限
stdio サーバー、またはツール一覧のキャッシュが無い HTTP/SSE サーバーはい。接続できるまでMCP_TIMEOUT(既定30秒)。期限で接続は失敗
ツール一覧のキャッシュがあるリモートサーバー(以前の接続で Claude Code が保存)いいえ。キャッシュのツールが最初のターンから使えるなし。最初のツール呼び出しのときに接続し、その接続には別のタイムアウトがある
インプロセスの SDK サーバーはい。接続してツール一覧を取るまでなし。接続とツール一覧の取得に、それぞれ別のタイムアウトがある

設定ファイル(.mcp.json など)やプラグインから読み込まれたサーバーは、通常 init メッセージで pending(保留中)と表示されます。これらを最初のターンでどこまで待つかは、次の条件で決まります。

  • options.mcpServers に stdio・HTTP・SSE のサーバーがある場合:保留中のサーバーも、MCP_TIMEOUT まで待つ
  • options.mcpServers が空か、SDK サーバーだけの場合:最大2秒だけ待つ

さらに、ツール検索の有無で「待つ対象」が変わります。

  • ツール検索あり(既定):待つのは alwaysLoad: true を設定した保留中のサーバーだけです。他はバックグラウンドで接続を続け、Claude はつながったあとでツール検索を通じてそのツールに届きます。
  • ツール検索なし:保留中のサーバーを全て待ちます。たとえば disallowedTools で ToolSearch ツールを外すと、ツール検索なしで動きます。

permissionPromptToolName を設定している場合は、どの条件でも、そのツールのサーバーを MCP_TIMEOUT まで待ちます。

待ち時間を自分で決めたい場合は、env オプションに CLAUDE_CODE_MCP_STARTUP_WAIT_MS を入れます(例:CLAUDE_CODE_MCP_STARTUP_WAIT_MS: "5000")。すると最初のターンは、ツール検索の有無に関係なく、保留中のサーバーを全てそのミリ秒数だけ待ちます。この期限は、options.mcpServers の stdio・HTTP・SSE サーバーの MCP_TIMEOUT による待ちも置き換えます。0 にすると待ちません。待ち終わっても保留中のサーバーは、バックグラウンドで接続を続けます。ただし permissionPromptToolName のサーバーだけは、この値に関係なく MCP_TIMEOUT まで待ちます。

init メッセージより前の起動段階で待たせたい場合は、別の方法が2つあります。

  • MCP_CONNECTION_NONBLOCKING を 0 にすると、接続の一群全体を待ってから起動します。この待ちは既定で5秒が上限で、MCP_CONNECT_TIMEOUT_MS でミリ秒単位に調整できます。期限を過ぎたサーバーはバックグラウンドで接続を続けます。
  • サーバーの設定に alwaysLoad: true を付けると、そのサーバーのツールが最初のターンから完全なスキーマで使えます。Claude Code はそのサーバーのツールを起動時に待ち(上限は同じ)、他のサーバーはバックグラウンドで接続を続けます。キャッシュのあるリモートサーバーは、接続せずにキャッシュのツールを出します。

仕組みは複雑ですが、実務上の要点は2つです(筆者の整理)。

  • init の時点でつながっていないサーバーがあるのは普通のことです。特にツール検索が既定で有効な場合、大半のサーバーはバックグラウンドで接続されます。
  • 「最初のターンから確実に使えてほしいサーバー」には alwaysLoad: true を付けるか、CLAUDE_CODE_MCP_STARTUP_WAIT_MS で待ち時間を明示します。

3-4. MCP ツールを許可する

MCP ツールの名前は mcp__<server-name>__<tool-name> の形です。たとえば "github" という名前のサーバーに list_issues ツールがあれば、mcp__github__list_issues になります。

allowedTools に書いたツールは、権限の確認なしで使えます。原文は許可リストの書き方として、次の3通りを示しています(TypeScript・Python とも同じ文字列)。

書き方意味
"mcp__github__*"github サーバーの全ツール
"mcp__db__query"db サーバーの query ツールだけ
"mcp__slack__send_message"slack サーバーの send_message ツールだけ

ワイルドカード * を使えば、ツールを1つずつ並べずにサーバーの全ツールを許可できます。

MCP ツールの許可には、権限モードではなく allowedTools を使うよう原文は勧めています。

  • permissionMode: "acceptEdits" は MCP ツールを自動承認しません。承認するのはファイルの編集とファイル操作系の Bash コマンドだけです(第80回)。
  • permissionMode: "bypassPermissions" は MCP ツールも自動承認しますが、他の安全のための確認もほとんど無効にするので、必要以上に広すぎます。
  • allowedTools のワイルドカードなら、必要な MCP サーバーだけに権限を与え、それ以上は与えません。

許可するツールの範囲は、できるだけ狭くしておくのが基本です。3-8 の GitHub の例では、ワイルドカードではなく mcp__github__list_issues だけを許可しています。GitHub のサーバーにはイシューの作成やコメントなど書き込みのツールもあるはずで、一覧の取得しか要らないエージェントにそれらまで許可する理由はありません(筆者の指摘)。

3-5. 使えるツールを確かめる

MCP サーバーがどんなツールを出しているかは、サーバーのドキュメントを見るか、init メッセージの tools 配列を調べれば分かります。MCP ツールの名前は mcp__ で始まります。

init メッセージは 3-3 の最初のターンの待ちが終わったあとに出るので、tools 配列に載るのは次の2種類だけです。

  • その時点で接続済みのサーバーの mcp__ ツール
  • ツール一覧のキャッシュがあるサーバー(最初に使うときに接続する)のツール

まだつながっていない他のサーバーのツールは載りません。次のコードは MCP ツールの名前だけを抜き出して表示します。

import { query } from "@anthropic-ai/claude-agent-sdk";

const options = {
  mcpServers: {
    // your servers
  },
};

for await (const message of query({ prompt: "...", options })) {
  if (message.type === "system" && message.subtype === "init") {
    const mcpTools = message.tools.filter((name) => name.startsWith("mcp__"));
    console.log("Available MCP tools:", mcpTools);
  }
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            # your servers
        },
    )
    async for message in query(prompt="...", options=options):
        if isinstance(message, SystemMessage) and message.subtype == "init":
            mcp_tools = [t for t in message.data.get("tools", []) if t.startswith("mcp__")]
            print("Available MCP tools:", mcp_tools)


asyncio.run(main())

init メッセージの読み方が言語で違います。 TypeScript は message.tools を直接読み、Python は SystemMessage の message.data.get("tools", []) から読みます。この違いは 3-9 の mcp_servers でも同じです。

Claude に「このサーバーで使えるツールを一覧にして」と頼む方法もあります。

3-6. 接続方式(トランスポート)

MCP サーバーとエージェントの通信方式は複数あります。どれを使うかは、サーバーのドキュメントの書き方で判断できます。

ドキュメントの書き方使う方式
実行するコマンドが書いてある(例:npx @modelcontextprotocol/server-filesystem)stdio
URL が書いてあるHTTP または SSE
自分のコードでツールを作るSDK MCP サーバー(第83回)

各方式の設定の形は次のとおりです。原文にはそれぞれの方式の設定断片の対訳がありますが、形は 3-1・3-2・3-7 の引用コードと同じなので、違う部分だけを示します。

方式設定に書くもの原文の断片の例
stdiocommand と args(必要なら env)3-2 と同じファイルシステムのサーバー。許可を mcp__filesystem__read_file と mcp__filesystem__list_directory の2つに絞っている
SSEtype: "sse"、url、必要なら headersurl は https://api.example.com/mcp/sse、ヘッダーに Authorization の Bearer トークンを環境変数 API_TOKEN から入れ、mcp__remote-api__* を許可
HTTPtype: "http"、url、必要なら headers3-1 のクイックスタートと 3-7 の例
  • stdio サーバーは、標準入出力でやり取りするローカルのプロセスです。同じマシンで動かす MCP サーバーに使います。
  • HTTP・SSE サーバーは、クラウドで動く MCP サーバーやリモートの API に使います。

"streamable-http" という書き方に注意してください。 ストリーミング対応の HTTP 方式には "type": "http" を使います。.mcp.json などの JSON の設定ファイルでは "streamable-http" も "http" の別名として受け付けられますが、SDK の McpHttpServerConfig 型は "http" しか宣言していません。コードで渡すサーバーには "http" を使います。

SDK MCP サーバー(第83回のインプロセスのサーバー)は、別のサーバープロセスを動かす代わりに、アプリのコードの中でツールを定義します。initialize 制御リクエストで登録された SDK MCP サーバーは、Claude Code がそのリクエストを処理した時点で接続を始めます。

ツール検索。 MCP ツールを多く設定すると、ツールの定義だけでコンテキストウィンドウの大部分を使ってしまうことがあります。ツール検索は、ツールの定義をコンテキストに入れずに保留し、各ターンで Claude が必要とするツールだけを読み込みます。ツール検索は既定で有効です。設定の方法や使いどころはツール検索のページに回されています。

3-7. 認証

ほとんどの MCP サーバーは、外部サービスにアクセスするために認証が必要です。認証情報は、環境変数やヘッダーを通じてサーバーの設定で渡します。

環境変数で渡す(主に stdio サーバー)

env フィールドで、API キーやトークンなどを MCP サーバーのプロセスに渡します。.mcp.json では次のように書きます。

{
  "mcpServers": {
    "api-server": {
      "command": "npx",
      "args": ["-y", "@your-org/api-mcp-server"],
      "env": {
        "API_KEY": "${API_KEY}"
      }
    }
  }
}

${API_KEY} は、実行時に環境変数 API_KEY の値に展開されます。秘密の値そのものをファイルに書かずに済むので、.mcp.json をリポジトリに入れても鍵は漏れません。

コードで渡す場合の書き方は、言語ごとに次のとおりです(原文の対訳断片から、値の部分だけを抜き出したもの)。

言語env に入れる値
TypeScriptAPI_KEY: process.env.API_KEY
Python"API_KEY": os.environ["API_KEY"]

HTTP ヘッダーで渡す(HTTP・SSE サーバー)

HTTP と SSE のサーバーでは、認証ヘッダーをサーバーの設定に直接書きます。.mcp.json では次のとおりです。

{
  "mcpServers": {
    "secure-api": {
      "type": "http",
      "url": "https://api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${API_TOKEN}"
      }
    }
  }
}

${API_TOKEN} も実行時に環境変数から展開されます。コードで書く場合は、次の OAuth の例と同じ形で headers に入れます。ヘッダーで認証するリモートサーバーの完全な例は 3-8 の GitHub の例です。

OAuth2 認証

MCP の仕様は OAuth 2.1 に対応していますが、SDK はブラウザを開いたり、対話的な OAuth の手続きをしたりしません。設定したサーバーが認可を求めてきて、保存済みのトークンも無い場合、次のようになります。

  • エージェントの実行は、そのサーバーのツールなしで続く
  • サーバーの状態は needs-auth になる
  • ただし init メッセージの mcp_servers 配列では、その時点で pending と表示されることがある

認証が必要かどうかを確かめるには、TypeScript では mcpServerStatus()、Python では get_mcp_status() を繰り返し呼んで状態を確認します。

認証情報を渡すには、アプリの中で OAuth の手続きを済ませ、得られたアクセストークンをサーバーの headers に入れます。

// After completing OAuth flow in your app.
// Implement getAccessTokenFromOAuthFlow for your OAuth provider.
const accessToken = await getAccessTokenFromOAuthFlow();

const options = {
  mcpServers: {
    "oauth-api": {
      type: "http",
      url: "https://api.example.com/mcp",
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    }
  },
  allowedTools: ["mcp__oauth-api__*"]
};
# After completing OAuth flow in your app.
# Implement get_access_token_from_oauth_flow for your OAuth provider.
access_token = await get_access_token_from_oauth_flow()

options = ClaudeAgentOptions(
    mcp_servers={
        "oauth-api": {
            "type": "http",
            "url": "https://api.example.com/mcp",
            "headers": {"Authorization": f"Bearer {access_token}"},
        }
    },
    allowed_tools=["mcp__oauth-api__*"],
)

getAccessTokenFromOAuthFlow/get_access_token_from_oauth_flow は、使う OAuth プロバイダーに合わせて自分で実装する関数です。

ここで大事なのは、認証に失敗してもエラーで止まらず、そのサーバーのツールが無いまま実行が続く点です。エラーを捕まえるだけでは気づけないので、状態を確認する処理が必要になります(3-9)。

3-8. 例

リポジトリのイシューを一覧にする

リモートの GitHub MCP サーバーにつなぎ、最近のイシューを一覧にする例です。MCP の接続とツール呼び出しを確かめるためのデバッグ用の出力が入っています。

実行する前に、調べたいリポジトリの読み取り権限を持つ GitHub の個人用アクセストークンを作り、環境変数に設定します。

export GITHUB_TOKEN=YOUR_GITHUB_PAT
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "List the 3 most recent issues in anthropics/claude-code",
  options: {
    mcpServers: {
      github: {
        type: "http",
        url: "https://api.githubcopilot.com/mcp/",
        headers: {
          Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
        }
      }
    },
    allowedTools: ["mcp__github__list_issues"]
  }
})) {
  // Verify MCP server connected successfully
  if (message.type === "system" && message.subtype === "init") {
    console.log("MCP servers:", message.mcp_servers);
  }

  // Log when Claude calls an MCP tool
  if (message.type === "assistant") {
    for (const block of message.message.content) {
      if (block.type === "tool_use" && block.name.startsWith("mcp__")) {
        console.log("MCP tool called:", block.name);
      }
    }
  }

  // Print the final result
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
import asyncio
import os
from claude_agent_sdk import (
    query,
    ClaudeAgentOptions,
    ResultMessage,
    SystemMessage,
    AssistantMessage,
)


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            "github": {
                "type": "http",
                "url": "https://api.githubcopilot.com/mcp/",
                "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
            }
        },
        allowed_tools=["mcp__github__list_issues"],
    )

    async for message in query(
        prompt="List the 3 most recent issues in anthropics/claude-code",
        options=options,
    ):
        # Verify MCP server connected successfully
        if isinstance(message, SystemMessage) and message.subtype == "init":
            print("MCP servers:", message.data.get("mcp_servers"))

        # Log when Claude calls an MCP tool
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if hasattr(block, "name") and block.name.startswith("mcp__"):
                    print("MCP tool called:", block.name)

        # Print the final result
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

MCP servers: の行で github の status が connected なら、トークンが効いています。Claude Code がこのサーバーのツール一覧をキャッシュしている場合は、状態が pending と表示され、最初のツール呼び出しで接続します。

状態が failed や needs-auth だった場合、結果をそのまま信じてはいけません。 サーバーが使えないとき、Claude は組み込みのツールで代わりに答えようとすることがあるからです。たとえば GitHub のサーバーにつながらないまま、Web 検索などで調べた内容を答えにする可能性があります。見た目はもっともらしい答えが返ってくるので、状態を確認していないと、MCP サーバーから取ったデータなのかどうか区別できません(この具体例は筆者の補足。原文は「ビルトインツールにフォールバックする可能性がある」とだけ書いています)。

言語の違いは次の2点です。

  • ツール呼び出しの取り出し方:TypeScript は message.message.content の中から block.type === "tool_use" を探し(第77回の二重構造)、Python は message.content の中から hasattr(block, "name") で判定しています。
  • mcp_servers の読み方:TypeScript は message.mcp_servers、Python は message.data.get("mcp_servers") です。

データベースに問い合わせる

DBHub を使って Postgres データベースに問い合わせる例です。エージェントはデータベースの構造を自動で調べ、SQL を書き、結果を返します。

DBHub の execute_sql ツールは、エージェントが出した SQL を書き込みも含めてそのまま実行します。 そこで DBHub の設定ファイルで readonly = true を指定します。すると DBHub は INSERT・UPDATE・DELETE と DDL 文(テーブルの作成・変更など)を拒否するので、エージェントが書き込みの SQL を出してもデータは変わりません。また、DBHub は設定を読み込むときに ${DATABASE_URL} をプロセスの環境変数から解決するので、接続文字列(パスワードを含む)はファイルに書かずに済みます。スクリプトと同じ場所に、次の dbhub.toml を作ります。

[[sources]]
id = "production"
dsn = "${DATABASE_URL}"

[[tools]]
name = "execute_sql"
source = "production"
readonly = true

スクリプトは接続文字列を直接渡さず、DBHub にこの設定ファイルを読ませます。実行前に、環境変数 DATABASE_URL に接続文字列を設定します(値は自分のデータベースに合わせて置き換えます)。

export DATABASE_URL=postgresql://user:password@localhost:5432/mydb
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  // Natural language query - Claude writes the SQL
  prompt: "How many users signed up last week? Break it down by day.",
  options: {
    mcpServers: {
      postgres: {
        command: "npx",
        // dbhub.toml sets readonly = true, so execute_sql rejects writes
        args: ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"]
      }
    },
    allowedTools: ["mcp__postgres__execute_sql"]
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            "postgres": {
                "command": "npx",
                # dbhub.toml sets readonly = true, so execute_sql rejects writes
                "args": [
                    "-y",
                    "@bytebase/dbhub",
                    "--config",
                    "dbhub.toml",
                ],
            }
        },
        allowed_tools=["mcp__postgres__execute_sql"],
    )

    # Natural language query - Claude writes the SQL
    async for message in query(
        prompt="How many users signed up last week? Break it down by day.",
        options=options,
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

「先週の新規登録者数を日別に」という自然言語の依頼から、Claude が SQL を書きます。許可しているのは mcp__postgres__execute_sql だけです。

読み取り専用の設定は、この例のように MCP サーバー側で行うのが確実です。Claude への指示(「書き込みはしないで」)は保証にならない、という第72回の原則と同じ考え方です。さらに固くするなら、データベース側でも読み取り権限しか持たないユーザーで接続すると、MCP サーバーの設定を誤ったときの保険になります(筆者の指摘)。

3-9. エラー処理

MCP サーバーの接続は、いろいろな理由で失敗します。サーバーのプログラムが入っていない、認証情報が間違っている、リモートのサーバーに届かない、などです。

Claude Code は各クエリの最初に、サブタイプ init の system メッセージを出します。ここに各 MCP サーバーの接続状態が入っています。

status の値意味
"pending"保留中(失敗とは限らない。下記)
"connected"接続済み
"failed"接続に失敗
"needs-auth"認証が必要
"disabled"無効

init メッセージは 3-3 の最初のターンの待ちが終わってから出るので、待ち時間内につながったサーバーは "connected" になります。

"pending" だけで失敗と判断してはいけません。 次のどれかの可能性があります。

  • まだ接続中である(3-3 の待ちの対象外だった)
  • ツール一覧がキャッシュから出されていて、最初に使うときに接続する
  • 接続の期限が切れた(このようなサーバーは、タイミングによって "pending" か "failed" を報告する)

使えないサーバーを見つけるには、"failed" と "needs-auth" を確認します。

import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({
    prompt: "Process data",
    options: {
      mcpServers: {
        // Replace dataServer with your server configuration
        "data-processor": dataServer
      }
    }
  })) {
    if (message.type === "system" && message.subtype === "init") {
      const unavailableServers = message.mcp_servers.filter(
        (s) => s.status === "failed" || s.status === "needs-auth"
      );

      if (unavailableServers.length > 0) {
        console.warn("Unavailable MCP servers:", unavailableServers);
      }
    }

    if (message.type === "result" && message.subtype === "error_during_execution") {
      console.error("Execution failed");
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, the error subtype branch above has
  // already run; a failure to start or reach the Claude Code process
  // yields no result message. MCP servers that fail to connect don't
  // throw: use the status check above, and note that servers still
  // "pending" at init need a later status check.
  console.log(`Session ended with an error: ${error}`);
}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage, ResultMessage


async def main():
    # Replace data_server with your server configuration
    options = ClaudeAgentOptions(mcp_servers={"data-processor": data_server})

    try:
        async for message in query(prompt="Process data", options=options):
            if isinstance(message, SystemMessage) and message.subtype == "init":
                unavailable_servers = [
                    s
                    for s in message.data.get("mcp_servers", [])
                    if s.get("status") in ("failed", "needs-auth")
                ]

                if unavailable_servers:
                    print(f"Unavailable MCP servers: {unavailable_servers}")

            if (
                isinstance(message, ResultMessage)
                and message.subtype == "error_during_execution"
            ):
                print("Execution failed")
    except Exception as error:
        # A single-shot query() raises after yielding an error result. If the
        # failure was an error result, the error subtype branch above has
        # already run; a failure to start or reach the Claude Code process
        # yields no result message. MCP servers that fail to connect don't
        # raise: use the status check above, and note that servers still
        # "pending" at init need a later status check.
        print(f"Session ended with an error: {error}")


asyncio.run(main())

コードのコメントには、例外についての重要な説明があります。

  • 1回きりの query() は、エラーの結果を出したあとで例外を投げます(第83回と同じ)。エラーの結果だった場合、上の error_during_execution の分岐はすでに実行されています。
  • Claude Code のプロセスを起動できない、あるいはプロセスに届かない場合は、結果メッセージ自体が出ません。
  • MCP サーバーの接続失敗では例外は投げられません。 上の状態確認で見つけるしかありません。また、init の時点で "pending" のサーバーは、後で改めて状態を確認する必要があります。

トラブルシューティングの節にある「failed のサーバー名を表示する」対訳コードは、この例の状態確認の部分とほぼ同じなので引用を省きます(server.status === "failed" のサーバーごとに server.name を出すだけの違いです)。

接続後に状態が変わることもあります。 リモートサーバーは、"connected" と報告したあとでも状態が変わりえます。セッションの途中で接続が切れると、Claude Code はそのサーバーを "pending" に戻して再接続します。このとき TypeScript の mcpServerStatus() や Python の ClaudeSDKClient.get_mcp_status() を呼ぶと、設定を何も変えていなくても、以前つながっていたサーバーが "pending" と報告されることがあります。

再接続を5回試して失敗すると、サーバーは "failed" を、または再認可が必要なら "needs-auth" を報告します。手動で再試行するには、TypeScript では reconnectMcpServer()、Python では ClaudeSDKClient.reconnect_mcp_server() を呼びます。Python では状態確認も再接続も ClaudeSDKClient のメソッドなので、1回きりの query() 関数ではなくクライアントを使う必要があります(第79回で見た「Python の query() は制御メソッドを持たない」と同じ)。

3-10. トラブルシューティング

サーバーが failed になる。 init メッセージでどのサーバーが失敗したかを確認します。"pending" は失敗ではありません。セッションの途中の最新の状態は、TypeScript の mcpServerStatus() か Python の ClaudeSDKClient.get_mcp_status() で取れます。よくある原因は次の4つです。

原因確認すること
環境変数が無い必要なトークンや認証情報が設定されているか。stdio サーバーでは env フィールドがサーバーの期待と合っているか
サーバーが入っていないnpx の場合、パッケージが存在し、Node.js が PATH に入っているか
接続文字列が不正データベースのサーバーでは、接続文字列の形式と、データベースに届くかどうか
ネットワークの問題リモートの HTTP・SSE サーバーでは、URL に届くか、ファイアウォールが接続を許しているか

ツールが呼ばれない。 Claude がツールを認識しているのに使わない場合は、allowedTools で許可しているかを確認します。原文の対訳断片は、allowedTools に "mcp__servername__*" を書いて、そのサーバーからの呼び出しを自動承認する形です(3-4 の書き方と同じ)。

接続がタイムアウトする。 時間に関わる設定は複数あり、名前が似ているので整理しておきます(原文の各節の記述をまとめたもの)。

環境変数・設定何を決めるか既定値
MCP_TIMEOUTMCP サーバーへの接続の上限(ミリ秒)。最初のターンの待ちにも使われる30秒
MCP_TOOL_TIMEOUT実行中のツール呼び出しにかけられる時間原文に記載なし
CLAUDE_CODE_MCP_STARTUP_WAIT_MS最初のターンで保留中のサーバーを待つ時間(ミリ秒、v2.1.274 以降)未設定なら 3-3 の規則
MCP_CONNECTION_NONBLOCKING0 で、起動時に接続の一群全体を待つ原文に記載なし
MCP_CONNECT_TIMEOUT_MS上の起動時の待ちの上限(ミリ秒)5秒
TypeScript の createSdkMcpServer() の timeout1つの SDK MCP サーバーのツール呼び出しの上限原文に記載なし

起動に時間がかかるサーバーでは、MCP_TIMEOUT を引き上げるほか、次も検討します。

  • より軽いサーバーがあればそれを使う
  • エージェントを始める前にサーバーを起動しておく(プリウォーム)
  • 初期化が遅い原因をサーバーのログで調べる

ツールの出力が上限を超える。 SDK には Claude Code と同じ MCP 出力の上限がかかります。画像を含まないツール結果が 25,000 トークンを超えると、Claude Code は出力をファイルに保存し、ツール結果を「そのファイルのパスを示すエラーメッセージ」に置き換えます。エージェントはそのファイルを少しずつ読み戻せます。上限は環境変数 MAX_MCP_OUTPUT_TOKENS で引き上げられます。サーバーが anthropic/maxResultSizeChars という注釈で、ツールごとに高い上限を宣言する方法もあり、詳細は CLI 側の MCP のページに回されています。


4. まとめ

  • 外部の MCP サーバーは、mcpServers オプションか .mcp.json で設定する。.mcp.json は既定で読み込まれるので、他人のリポジトリで動かすときは settingSources に注意する。
  • 方式は、ドキュメントにコマンドがあれば stdio、URL があれば HTTP/SSE、自作なら SDK サーバー。コードで渡す HTTP サーバーの type は "http"("streamable-http" は JSON 設定ファイルでだけ使える別名)。
  • MCP ツールは明示的な許可が必要。allowedTools に mcp__<server>__<tool> か mcp__<server>__* を書く。acceptEdits は MCP ツールを承認せず、bypassPermissions は広すぎる。許可は必要なツールに絞る。
  • 接続には待ちの規則がある。ツール検索が既定で有効なので、大半のサーバーはバックグラウンドで接続される。最初から確実に使いたいサーバーには alwaysLoad: true か CLAUDE_CODE_MCP_STARTUP_WAIT_MS。
  • 認証は env(主に stdio)か headers(HTTP・SSE)。.mcp.json では ${VAR} で環境変数を展開し、秘密をファイルに書かない。SDK は OAuth の手続きをしないので、アプリで済ませてトークンを渡す。
  • 状態は init メッセージの mcp_servers で確認する。pending は失敗ではない。使えないサーバーは failed と needs-auth で見分ける。接続失敗では例外が投げられない。
  • サーバーが使えないと、Claude は組み込みツールで代わりに答えることがある。状態を確認してから結果を信用する。
  • 接続が切れると pending に戻って再接続し、5回失敗すると failed/needs-auth。Python の状態確認・再接続は ClaudeSDKClient のメソッド。
  • 画像以外のツール結果が 25,000 トークンを超えると、ファイルに保存されてパスだけが返る。

次回予告

次回は 「ツール検索」(agent-sdk/tool-search) を取り上げる予定です。

今回、接続の待ち時間の規則が「ツール検索が有効かどうか」で大きく変わることを見ました。第83回の自作ツールも、ツール検索によって必要になるまで読み込まれない仕組みでした。次回は、ツールがたくさんあってもコンテキストを圧迫しないためのこの仕組みを、どう有効・無効にし、どう使いこなすのかを見ていきます。

※テーマは変更になる場合があります。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました