AI活用

MCPクライアントの作り方|TypeScriptでServerへ接続してToolを呼ぶ

公式TypeScript SDKでMCPクライアントを作り、stdio接続、Tool一覧取得・実行、結果とエラーの判定、タイムアウト、切断までを実行ログ付きで解説します。

この記事の目次
  1. 結論:connect、listTools、callTool、closeの順で確認する
  2. MCPクライアントの役割を整理する
  3. 事前にMCP Serverをビルドする
  4. TypeScriptプロジェクトを準備する
  5. package.jsonへスクリプトを追加する
  6. tsconfig.jsonを作る
  7. 2つのプロジェクトを並べる
  8. StdioClientTransportでServerを指定する
  9. 接続してTool一覧を取得する
  10. search_notes Toolを実行する
  11. Toolの失敗と通信の失敗を分ける
  12. 完成したClientコード
  13. 型チェック・ビルド・実行する
  14. ResourceとPromptを使う場合
  15. 切断・タイムアウト・再接続を設計する
  16. 再接続は新しいClientとTransportで行う
  17. stdioとStreamable HTTPの選び方
  18. 接続できないときの確認順
  19. 本番クライアントへ広げる前の安全確認
  20. よくある質問
  21. MCPクライアントにもLLM APIが必要ですか?
  22. Toolを呼ぶ前に毎回listToolsが必要ですか?
  23. ブラウザからstdioへ直接接続できますか?
  24. まとめ

MCPクライアントは、MCP Serverへ接続し、利用できるToolを確認して実行結果を受け取る側のプログラムです。TypeScriptでは、公式SDKの ClientStdioClientTransport を使うと、ローカルServerを子プロセスとして起動し、一覧取得から切断までを小さなコードで確認できます。

この記事では、別記事で作った search_notes Toolを持つServerへ接続します。Server側のTool登録は繰り返さず、Client lifecycle(クライアントの接続から終了までの流れ)、結果の型判定、タイムアウト、再接続の考え方に集中します。

2026年7月20日時点で、掲載コードは公式TypeScript SDKの安定版v1、npm版 @modelcontextprotocol/sdk@1.29.0 で型チェックと実行確認を行いました。SDKのメジャーバージョンが変わるとimportやAPIが変わる可能性があるため、導入時は公式v1ドキュメントと利用するServerの対応版を確認してください。

情報確認日:2026年7月20日(日本時間)

結論:connect、listTools、callTool、closeの順で確認する

この記事で作るもの

  • Node.js・TypeScriptで動くMCPクライアント
  • stdioでローカルServerを起動する接続設定
  • tools/list に相当するTool一覧の取得
  • search_notes の実行とテキスト結果の型判定
  • 10秒のrequest timeoutと確実な切断処理
  • 接続から終了までを追える通信ログ
Clientを作る
  ↓
stdioでServerプロセスを起動
  ↓
initialize:対応機能を確認して接続成立
  ↓
tools/list:使えるToolを取得
  ↓
tools/call:名前と引数を指定して実行
  ↓
結果を型判定して利用
  ↓
close:通信と子プロセスを終了

MCPの全体像を先に知りたい場合はMCPとは何か、接続先のサンプルを作る場合はNode.jsでMCPサーバーを作る方法を参照してください。

スポンサーリンク

MCPクライアントの役割を整理する

役割 担当すること この記事の例
Host AIアプリ全体、利用者との対話、許可画面 CLIアプリ全体
Client 1つのServerとの接続、機能確認、request送信 Client インスタンス
Server Tool、Resource、Promptを公開して処理する ノート検索Server
Transport ClientとServerのメッセージを運ぶ stdio

Host(ホスト)はAIアプリ全体、Client(クライアント)は個々のServerへの接続担当、Transport(トランスポート)は通信の運び方です。通常、Hostが複数のServerを使う場合は、ServerごとにClient接続を分けます。

Toolは「外部処理を実行する機能」、Resourceは「URIで参照するデータ」、Promptは「再利用する指示テンプレート」です。3つの使い分けはMCPのTools・Resources・Promptsの違いで詳しく整理しています。

事前にMCP Serverをビルドする

今回のClientは、隣のディレクトリにある note-search-mcp Serverへ接続します。Server側で次を実行し、dist/index.js を作ってください。

cd ../note-search-mcp
npm install
npm run build
node dist/index.js

最後の単体起動ではServerが入力待ちになれば正常です。終了は Ctrl+C です。stdio Serverは標準入力と標準出力をMCP通信に使うため、通常ログを console.log() で出してはいけません。診断ログは標準エラーへ出します。

TypeScriptプロジェクトを準備する

mkdir mcp-client-demo
cd mcp-client-demo
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk@1.29.0
npm install -D typescript @types/node

記事と同じ結果を再現するためSDKを固定しています。検証後はDependabotなどで更新候補を受け取り、型チェックと接続テストを通してから上げます。

package.jsonへスクリプトを追加する

{
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/client.js"
  }
}

tsconfig.jsonを作る

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

2つのプロジェクトを並べる

workspace/
├── note-search-mcp/
│   └── dist/
│       └── index.js
└── mcp-client-demo/
    ├── src/
    │   └── client.ts
    ├── package.json
    └── tsconfig.json

StdioClientTransportでServerを指定する

src/client.tsへ、ClientとTransportを作る処理を書きます。

import { resolve } from "node:path";
import { Client } from
  "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from
  "@modelcontextprotocol/sdk/client/stdio.js";

const serverRoot = resolve(
  process.cwd(),
  "../note-search-mcp"
);
const serverPath = resolve(serverRoot, "dist/index.js");

const transport = new StdioClientTransport({
  command: process.execPath,
  args: [serverPath],
  cwd: serverRoot,
});

const client = new Client({
  name: "note-search-cli",
  version: "1.0.0",
});

command は実行ファイル、args はServerの引数、cwd はServerプロセスの作業ディレクトリです。process.execPath を使うと、今このClientを動かしているNode.jsの実行ファイルを指定できます。

パスの注意:Serverの場所を外部入力から自由に指定させないでください。任意プログラムの起動につながるため、管理者が許可したServerと引数だけを設定します。

接続してTool一覧を取得する

try {
  console.time("mcp-connect");
  await client.connect(transport);
  console.timeEnd("mcp-connect");

  console.log(
    "server:",
    client.getServerVersion()
  );

  const tools = await client.listTools();
  console.log(
    "tools:",
    tools.tools.map((tool) => ({
      name: tool.name,
      description: tool.description,
    }))
  );
} finally {
  await client.close();
  console.log("closed");
}

connect() はTransportを開始するだけでなく、ClientとServerのバージョンや対応機能を確認する初期化も行います。接続前に listTools() を呼ばないよう、起動処理を1か所にまとめます。

search_notes Toolを実行する

listTools() の後に、Tool名と引数を渡します。第3引数の timeout はミリ秒です。

const result = await client.callTool(
  {
    name: "search_notes",
    arguments: {
      query: "stdio",
      limit: 3,
    },
  },
  undefined,
  {
    timeout: 10_000,
  }
);

第2引数は独自の結果スキーマを渡す位置です。今回はServerが返す標準のTool結果を使うため undefined としています。タイムアウトを付けてもServer内の処理が必ず取り消されるとは限りません。書き込み系Toolでは、実行IDを付けて重複実行を防ぐ設計も必要です。

Toolの失敗と通信の失敗を分ける

if (result.isError) {
  throw new Error(
    `Tool error: ${JSON.stringify(result.content)}`
  );
}

if (!Array.isArray(result.content)) {
  throw new Error(
    "Tool response content is not an array"
  );
}

for (const item of result.content) {
  if (item.type === "text") {
    console.log(item.text);
  }
}

Toolが業務上のエラーを返す場合は、request自体が成立していても isError: true になることがあります。一方、Serverプロセスの終了、タイムアウト、プロトコル不正などは例外として処理されます。どちらも同じ「接続失敗」として再試行すると、更新処理を二重実行する危険があります。

完成したClientコード

import { resolve } from "node:path";
import { Client } from
  "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from
  "@modelcontextprotocol/sdk/client/stdio.js";

const serverRoot = resolve(
  process.cwd(),
  "../note-search-mcp"
);

const transport = new StdioClientTransport({
  command: process.execPath,
  args: [resolve(serverRoot, "dist/index.js")],
  cwd: serverRoot,
});

const client = new Client({
  name: "note-search-cli",
  version: "1.0.0",
});

try {
  await client.connect(transport);
  console.log("connected");

  const tools = await client.listTools();
  console.log(
    "tools:",
    tools.tools.map((tool) => tool.name)
  );

  const result = await client.callTool(
    {
      name: "search_notes",
      arguments: {
        query: "stdio",
        limit: 3,
      },
    },
    undefined,
    {
      timeout: 10_000,
    }
  );

  if (result.isError) {
    throw new Error(
      `Tool error: ${JSON.stringify(result.content)}`
    );
  }

  if (!Array.isArray(result.content)) {
    throw new Error(
      "Tool response content is not an array"
    );
  }

  for (const item of result.content) {
    if (item.type === "text") {
      console.log(item.text);
    }
  }
} catch (error: unknown) {
  console.error(
    "MCP client failed:",
    error instanceof Error ? error.message : error
  );
  process.exitCode = 1;
} finally {
  await client.close();
  console.log("closed");
}

型チェック・ビルド・実行する

npm run typecheck
npm run build
npm start

正常に動くと、概ね次の順で表示されます。

note-search MCP server started
connected
tools: [ 'search_notes' ]
{
  "query": "stdio",
  "count": 1,
  "items": [
    {
      "id": "note-2",
      "title": "stdio接続",
      "body": "ローカルではクライアントがServerを子プロセスとして起動します。"
    }
  ]
}
closed

この記事のコードは、SDK v1.29.0で tsc --noEmit、ビルド、Server起動、Tool一覧取得、Tool実行、切断までを確認しています。このログが記事固有の最小疎通テストです。

ResourceとPromptを使う場合

接続先ServerがResourceまたはPromptを公開している場合は、対応機能を確認してから一覧を取得します。

const capabilities =
  client.getServerCapabilities();

if (capabilities?.resources) {
  const resources = await client.listResources();
  console.log(
    resources.resources.map((resource) => ({
      uri: resource.uri,
      name: resource.name,
    }))
  );
}

if (capabilities?.prompts) {
  const prompts = await client.listPrompts();
  console.log(
    prompts.prompts.map((prompt) => prompt.name)
  );
}

Serverが公開していない機能を前提にしないことが重要です。Resource本文を取得する場合は readResource()、Promptを展開する場合は getPrompt() を使いますが、URIや引数も信頼できない外部入力として検証します。

切断・タイムアウト・再接続を設計する

状況 基本対応 自動再実行
Toolが isError 入力・権限・業務エラーを利用者へ示す 原則しない
request timeout 処理が継続していないか確認する 読み取りのみ条件付き
Serverプロセス終了 stderrと終了理由を記録して再接続 回数・間隔に上限
アプリ終了 client.close() で子プロセスも終了 不要

Timeout(タイムアウト)は応答を待つ上限時間、retry(リトライ)は再試行です。タイムアウトは「Serverが処理していない」という証明ではありません。メール送信、削除、購入、更新など副作用のあるToolを無条件で再実行しないでください。

再接続は新しいClientとTransportで行う

接続が閉じた後は、同じオブジェクトを曖昧に再利用せず、新しいTransportとClientを作って初期化し直すと状態を追いやすくなります。常駐アプリでは、指数バックオフ、最大再試行回数、手動停止手段を追加します。

stdioとStreamable HTTPの選び方

接続方式 向いている用途 Client側の主な責任
stdio 同じ端末のローカルServer 実行パス、引数、環境変数、子プロセス終了
Streamable HTTP ネットワーク越しの共有Server HTTPS、認証、Origin、セッション、再接続

stdioはstandard input/output(標準入力・標準出力)の略で、ClientがローカルServerを起動して通信します。Streamable HTTPはHTTPを使う現行のリモート向けTransportです。旧HTTP+SSEは後方互換のために残る方式なので、新規実装の既定にしません。

接続できないときの確認順

  1. Server側の npm run build が成功するか
  2. node ../note-search-mcp/dist/index.js でServerが待機するか
  3. Clientの serverRootserverPath が実在するか
  4. Serverが標準出力へ通常ログを書いていないか
  5. ClientとServerで互換性のあるSDK・protocol versionを使っているか
  6. Serverのstderrにimport、権限、環境変数のエラーがないか
  7. Tool名と引数が listTools() のschemaに一致するか

stdioではServerの通常ログを標準エラーへ出すと、Client側のターミナルで切り分けできます。ただし、APIキー、アクセストークン、個人情報、Toolの全文結果はログへ残しません。

本番クライアントへ広げる前の安全確認

  • 許可したServerのcommand・argsだけを起動する
  • Tool一覧を利用者へ示し、送信・削除・購入は実行前に確認する
  • モデルが作ったTool引数を、ClientとServerの両方で検証する
  • Toolごとにタイムアウト、結果件数、同時実行数を制限する
  • 接続先、Tool名、実行時刻、成否を秘密情報なしで監査する
  • Serverへ渡す環境変数を必要最小限にする
  • Remote Serverでは認証・認可とHTTPSを必須にする

Clientは「モデルが選んだToolをそのまま実行する中継器」ではありません。Host側で利用者の権限、操作リスク、確認要否を判断し、Server側でも最終的な認可を行います。

よくある質問

MCPクライアントにもLLM APIが必要ですか?

今回の疎通用CLIはToolをコードから直接呼ぶため、LLM APIは不要です。自然言語からToolを選ばせるHostを作る場合はLLMとの接続が別途必要ですが、MCP接続とモデル接続は分けてテストします。

Toolを呼ぶ前に毎回listToolsが必要ですか?

毎requestで必須ではありませんが、接続後に一覧とinput schemaを取得し、利用可能なToolを確認する設計が安全です。Server側の一覧が変わり得る場合は、変更通知への対応または再取得方針を決めます。

ブラウザからstdioへ直接接続できますか?

できません。ブラウザはローカル子プロセスを直接起動できないため、Node.jsなどのHostを介するか、認証されたStreamable HTTP Serverへ接続する構成を検討します。

まとめ

TypeScriptのMCPクライアントは、公式SDKの ClientStdioClientTransport を使い、connect()listTools()callTool()close() の順で最小疎通を確認できます。

実装では、Toolが返す isError と通信例外を分け、結果の型を確認し、必ずタイムアウトと切断処理を設けます。最小Clientが動いた後にResource、Prompt、複数Server、Remote接続へ段階的に広げてください。

スポンサーリンク