MCPクライアントは、MCP Serverへ接続し、利用できるToolを確認して実行結果を受け取る側のプログラムです。TypeScriptでは、公式SDKの Client と StdioClientTransport を使うと、ローカル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は後方互換のために残る方式なので、新規実装の既定にしません。
接続できないときの確認順
- Server側の
npm run buildが成功するか node ../note-search-mcp/dist/index.jsでServerが待機するか- Clientの
serverRootとserverPathが実在するか - Serverが標準出力へ通常ログを書いていないか
- ClientとServerで互換性のあるSDK・protocol versionを使っているか
- Serverのstderrにimport、権限、環境変数のエラーがないか
- 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の Client と StdioClientTransport を使い、connect()、listTools()、callTool()、close() の順で最小疎通を確認できます。
実装では、Toolが返す isError と通信例外を分け、結果の型を確認し、必ずタイムアウトと切断処理を設けます。最小Clientが動いた後にResource、Prompt、複数Server、Remote接続へ段階的に広げてください。