はじめに: サーバーから MCP クライアントへ
以前の記事では、ツール、リソース、プロンプトを公開する MCP サーバーを構築しました。でも誰が 消費する これらの機能は?の MCPクライアント そして、あなたが作成するソフトウェアコンポーネント 1 つ以上のサーバーに接続し、利用可能なツールを検出し、言語モデルに代わってそれらを呼び出します。
この記事では、TypeScript でプログラムによる MCP クライアントを構築する方法を検討し、 2 つの主要な輸送メカニズム (スタジオ e ストリーミング可能なHTTP)、 セッション管理を見て、MCP サーバーを Web アプリケーションに統合する方法を見ていきます。 エクスプレス。すべてのコードはリポジトリで利用可能です テックMCP.
この記事で学べること
- 公式 TypeScript SDK を使用して MCP クライアントを作成する方法
- ツールの使用サイクル: クエリ、AI の決定、呼び出し、応答
- トランスポート STDIO: 子プロセス経由のローカル接続
- Transport Streamable HTTP: ネットワーク経由でアクセス可能な独立したサーバー
- セッションIDと再接続によるセッション管理
- テスト用のマルチサーバー パターンと InMemoryTransport
- HTTP トランスポートのセキュリティ: オリジン検証、ベアラー認証
クライアントプロジェクトのセットアップ
MCP クライアントを最初から作成するには、TypeScript プロジェクトのセットアップから始めましょう。 SDK
@modelcontextprotocol/sdk クライアント側に必要なものをすべて提供し、
その間 @anthropic-ai/sdk クロードを意思決定モデルとして統合できます。
mkdir mcp-client && cd mcp-client
npm init -y
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv
npm install -D typescript @types/node
設定する tsconfig.json サーバープロジェクトと同様に、スクリプトを package.json:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
ファイルを作成する .env Anthropic API キーを使用して:
ANTHROPIC_API_KEY=sk-ant-...
トランスポート STDIO を備えた MCP クライアント
輸送 スタジオ クライアントを MCP サーバーに接続するための最も単純なメカニズムです。
クライアントは次のようにサーバーを起動します。 子プロセス 標準入出力を介して通信します。
各メッセージは改行で区切られた JSON-RPC オブジェクトですが、 stderr 引き続き利用可能
ログ記録用。
STDIO クライアント アーキテクチャ
STDIO 接続の流れは次の手順に従います。
- クライアントが作成するのは、
StdioClientTransportコマンドとサーバー引数の指定 - 方法
connect()子プロセスを起動し、ハンドシェイクを実行します。initialize/initialized - クライアントは、使用可能なツールを発見します。
listTools() - ユーザーのクエリごとに、クライアントはツールのリストを指定して Claude を呼び出し、ツール使用ループを処理します。
STDIOクライアントの完全な実装
以下は、Claude を意思決定モデルとして使用する対話型 CLI クライアントの完全なコードです。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import Anthropic from "@anthropic-ai/sdk";
import { config } from "dotenv";
import * as readline from "node:readline";
config(); // Carica variabili da .env
// Tipo per i tool compatibili con l'API Anthropic
interface AnthropicTool {
name: string;
description: string | undefined;
input_schema: Record<string, unknown>;
}
class McpChatClient {
private client: Client;
private anthropic: Anthropic;
private tools: AnthropicTool[] = [];
constructor() {
this.client = new Client({
name: "mcp-chat-client",
version: "1.0.0",
});
this.anthropic = new Anthropic();
}
/**
* Connessione al server MCP via STDIO.
* Il client lancia il server come processo figlio.
*/
async connect(serverCommand: string, serverArgs: string[]): Promise<void> {
const transport = new StdioClientTransport({
command: serverCommand,
args: serverArgs,
});
// connect() esegue il lifecycle: initialize -> initialized
await this.client.connect(transport);
// Scopri i tool disponibili
const result = await this.client.listTools();
this.tools = result.tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema as Record<string, unknown>,
}));
console.error(
`Connesso. Tool disponibili: ${this.tools.map((t) => t.name).join(", ")}`
);
}
/**
* Elabora una query utente con il ciclo tool-use di Claude.
*/
async chat(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
let response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages,
tools: this.tools as Anthropic.Tool[],
});
// Ciclo tool-use: Claude può richiedere più tool in sequenza
while (response.stop_reason === "tool_use") {
const assistantContent = response.content;
messages.push({ role: "assistant", content: assistantContent });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of assistantContent) {
if (block.type === "tool_use") {
console.error(` -> Invocazione: ${block.name}`);
// Chiama il tool sul server MCP
const result = await this.client.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>,
});
const text = (result.content as Array<{ type: string; text: string }>)
.map((c) => c.text)
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: text,
});
}
}
messages.push({ role: "user", content: toolResults });
response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages,
tools: this.tools as Anthropic.Tool[],
});
}
return response.content
.filter((block): block is Anthropic.TextBlock => block.type === "text")
.map((block) => block.text)
.join("\n");
}
async disconnect(): Promise<void> {
await this.client.close();
}
}
ツール利用の流れの詳細
MCP クライアントの中心となるのは、 ツール使用ループ、AI を可能にする反復メカニズム クエリに答えるためにどのツールを呼び出すかを独自に決定します。完全なフローは次のとおりです。
Utente Client Claude API Server MCP
| | | |
| "Aggiungi | | |
| nota X" | | |
| ────────────> | | |
| | ── messages ─────> | |
| | + tools list | |
| | | |
| | <── tool_use ──── | |
| | "add-note" | |
| | | |
| | ── callTool ────────────────────────> |
| | "add-note" | |
| | <── result ───────────────────────── |
| | | |
| | ── tool_result ──> | |
| | | |
| | <── text ──────── | |
| | "Nota salvata" | |
| <──────────── | | |
| "Nota salvata" | | |
サイクルの主要なステップ
- クライアントはユーザークエリをクロードに送信します。 ツールのリストとともに (名前、説明、JSON スキーマ)
- クロードはクエリを分析し、ツールを使用するかどうかを決定します (クロードは次のように応答します)
stop_reason: "tool_use") - クライアントは次のコマンドを使用して MCP サーバー上のツールを呼び出します。
client.callTool() - 結果は次のようにクロードに返されます。
tool_result - クロードは他のツールをリクエストしたり、最終応答を生成したりできます (
stop_reason: "end_turn")
トランスポート STDIO とストリーミング可能な HTTP
MCP SDK は、2 つの根本的に異なるトランスポート メカニズムをサポートしています。この 2 つの間の選択は状況によって決まります ユースケース、システムアーキテクチャ、導入要件から判断します。
STDIO とストリーミング可能な HTTP の比較
| 待ってます | スタジオ | ストリーミング可能なHTTP |
|---|---|---|
| 建築 | クライアントはサーバーを子プロセスとして起動します | サーバーは独立したサービスです |
| 接続 | 1:1 (プロセスごとに 1 つのクライアント) | N:1 (多数のクライアント、1 つのサーバー) |
| 導入 | ローカルのみ、同じホスト | ローカルでもリモートでも、あらゆるネットワーク |
| セッション | 暗黙的 (プロセスの存続期間) | 明示的 (ヘッダー内のセッション ID) |
| 認証 | 不要(同じホスト) | 必須(ベアラートークン、OAuth) |
| スケーラビリティ | 制限付き (クライアントごとに 1 つのプロセス) | 高 (1 つのサーバー、多数のクライアント) |
| 使用事例 | クロード デスクトップ、IDE、ローカル開発 | マイクロサービス、共有API、本番環境 |
| セットアップの複雑さ | 最小限 | HTTP サーバーが必要です (Express、Fastify) |
一般的に、 スタジオ ローカル開発と統合に最適な選択肢です。 Claude Desktop や Cursor などのデスクトップ アプリケーション。 ストリーミング可能なHTTP そして必要なときに サーバーには、複数のクライアント、リモート ホスト、またはコンテナ化された運用環境からアクセスできる必要があります。
トランスポート HTTP (Express) を備えた MCP サーバー
HTTP 経由で MCP サーバーを公開するために、SDK は以下を提供します。 StreamableHTTPServerTransport それはそうです
Express とネイティブに統合されます。サーバーは接続を受け入れる独立したサービスになります
互換性のあるクライアントから。
依存関係のセットアップ
npm install express
npm install -D @types/express
HTTPサーバーの実装
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport }
from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
import { z } from "zod";
// Crea il server MCP (stessa API di STDIO)
const server = new McpServer({
name: "notes-http-server",
version: "1.0.0",
});
// Registra i tool
const notes: Map<string, string> = new Map();
server.tool(
"add-note",
"Aggiunge una nuova nota",
{
title: z.string(),
content: z.string(),
},
async ({ title, content }) => {
notes.set(title, content);
return {
content: [{ type: "text", text: `Nota "${title}" salvata.` }],
};
},
);
// --- Transport HTTP con Express ---
const app = express();
app.use(express.json());
// Crea transport con generatore di sessioni
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
// Connetti il server MCP al transport
await server.connect(transport);
// Endpoint MCP: POST (messaggi client -> server)
app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res, req.body);
});
// Endpoint MCP: GET (SSE stream server -> client)
app.get("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// Endpoint MCP: DELETE (terminazione sessione)
app.delete("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// Health check
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "notes-http-server" });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log(`Server MCP HTTP su http://localhost:${PORT}/mcp`);
});
エンドポイントとHTTPプロトコル
サーバーは単一のパスを公開します /mcp それぞれ特定の機能を持つ 3 つの HTTP メソッドを使用
MCP プロトコルでは次のようになります。
- POST /mcp - クライアントは JSON-RPC (リクエスト、通知、応答) メッセージを送信します。これは、ツールを呼び出すためのメイン チャネルです。
- GET /mcp - クライアントは SSE (Server-Sent Events) ストリームを開き、サーバーからリアルタイムでプッシュ通知を受信します。
- /mcpを削除 - クライアントは明示的にセッションを終了し、サーバー側のリソースを解放します。
STDIOとの根本的な違い
STDIO では、接続ごとに新しいサーバー プロセスが作成されます。 HTTP を使用して、サーバーはトランスポートに接続します。
一度だけ con server.connect(transport) すべてのセッションを管理します
同時に。の sessionIdGenerator 接続する各クライアントに一意の UUID を割り当てます。
トランスポート HTTP を使用する MCP クライアント
HTTP サーバーに接続するために、SDK は以下を提供します。 StreamableHTTPClientTransport。 API
クライアントのは STDIO と同一のままです。接続が確立されると、 listTools(),
callTool() e close() それらはまったく同じように機能します。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport }
from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({
name: "http-client",
version: "1.0.0",
});
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
);
await client.connect(transport);
// Da qui in poi, stessa API di STDIO
const tools = await client.listTools();
console.log("Tool disponibili:", tools.tools.map((t) => t.name));
const result = await client.callTool({
name: "add-note",
arguments: { title: "Test", content: "Contenuto di prova" },
});
console.log("Risultato:", result);
await client.close();
基本的な側面は、 アプリケーションコードは同一です 交通手段に関係なく。 STDIO と HTTP のどちらを選択するかは、アーキテクチャ上の決定ではなく、インフラストラクチャ上の決定です: ビジネス ロジック クライアントの内容は変わりません。
セッション管理
セッション管理は、HTTP トランスポートの最も重要な側面の 1 つです。 STDIO セッション中 HTTP では、暗黙的に (プロセスの存続期間と一致して)、各クライアント接続が セッションの一意のID これは後続のすべてのリクエストに含める必要があります。
セッションの仕組み
- クライアントが最初のリクエストを送信します
initializeセッションIDなし - サーバーは次の UUID を生成します。
sessionIdGeneratorそしてそれをヘッダーに返しますMcp-Session-Id - クライアントには以下が含まれます
Mcp-Session-Id後続のすべてのリクエストで - サーバーは各リクエストを正しいセッションに関連付け、状態を維持します。
- 切断時にクライアントは送信します。
DELETE /mcpセッションを終了するには
StreamableHTTPClientTransport は以下を自動的に処理します。
- ヘッダーの送信
Mcp-Session-Id初期化後 - ヘッダーの送信
MCP-Protocol-Versionすべてのリクエストで - セッションが期限切れになった場合の再接続 (HTTP 404)
ステートフル パターン: セッションごとの状態
運用環境では、各セッションが独自の分離状態を持つことができます。 Here's how to implement it:
import { randomUUID } from "node:crypto";
// Mappa sessioni -> stato isolato
const sessions = new Map<string, { notes: Map<string, string> }>();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => {
const sessionId = randomUUID();
sessions.set(sessionId, { notes: new Map() });
return sessionId;
},
});
複数のサーバーへの接続
MCP クライアントが接続できるのは、 同時に複数のサーバー、ツールを集約する 単一のリストで利用可能です。このパターンは、ツールが使用される複雑なアーキテクチャの基本です。 これらは専用のサーバーに分散されます。
class MultiServerClient {
private clients: Map<string, Client> = new Map();
private allTools: AnthropicTool[] = [];
async addServer(
name: string, command: string, args: string[]
): Promise<void> {
const client = new Client({
name: `client-${name}`,
version: "1.0.0"
});
const transport = new StdioClientTransport({ command, args });
await client.connect(transport);
const result = await client.listTools();
for (const tool of result.tools) {
this.allTools.push({
// Prefisso per evitare collisioni di nomi
name: `${name}__${tool.name}`,
description: `[${name}] ${tool.description}`,
input_schema: tool.inputSchema as Record<string, unknown>,
});
}
this.clients.set(name, client);
}
async callTool(
prefixedName: string, args: Record<string, unknown>
): Promise<unknown> {
const [serverName, toolName] = prefixedName.split("__");
const client = this.clients.get(serverName);
if (!client) throw new Error(`Server ${serverName} non connesso`);
return client.callTool({ name: toolName, arguments: args });
}
}
接頭語 serverName__toolName サーバーが異なる場合の衝突を避けるために不可欠です
同じ名前のツールを公開します。クロードは完全なリストを受け取り、クライアントがルーティングを担当します。
デュアルトランスポート: STDIO と HTTP をサポート
専門的な MCP サーバーがサポートする必要があります。 両方のトランスポート、どちらかを決める 構成に従って使用してください。これにより、両方の Claude Desktop で同じコードを使用できるようになります。 (STDIO) および本番環境 (HTTP)。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport }
from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
function createServer(): McpServer {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
// ... registra tool, resource, prompt ...
return server;
}
// Avvio condizionale basato su variabile d'ambiente
const mode = process.env.MCP_TRANSPORT ?? "stdio";
if (mode === "http") {
const server = createServer();
const app = express();
app.use(express.json());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);
app.post("/mcp", async (req, res) =>
await transport.handleRequest(req, res, req.body));
app.get("/mcp", async (req, res) =>
await transport.handleRequest(req, res));
app.delete("/mcp", async (req, res) =>
await transport.handleRequest(req, res));
app.get("/health", (_, res) => res.json({ status: "ok" }));
const port = process.env.PORT ?? 3000;
app.listen(port, () => console.log(`HTTP su porta ${port}`));
} else {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server avviato su STDIO");
}
トランスポート構成
MCP_TRANSPORT=stdio(デフォルト) - Claude デスクトップおよびローカル IDE の場合MCP_TRANSPORT=http PORT=3000- HTTP デプロイメントとマイクロサービスの場合
テスト用の InMemoryTransport
単体テストの場合、プロセスや HTTP サーバーを起動する必要はありません。 SDKが提供するのは、
InMemoryTransport これはクライアントとサーバーをメモリ内で直接接続します。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
async function testInMemory() {
const server = new McpServer({ name: "test-server", version: "1.0.0" });
server.tool("ping", "Test ping", {}, async () => ({
content: [{ type: "text", text: "pong" }],
}));
// Crea coppia di transport collegati
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// IMPORTANTE: il server si connette PRIMA del client
await server.connect(serverTransport);
const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(clientTransport);
const result = await client.callTool({ name: "ping", arguments: {} });
console.log(result);
// { content: [{ type: "text", text: "pong" }] }
await client.close();
await server.close();
}
クリティカルな接続の順序
Con InMemoryTransport, server.connect() 呼ばれなければなりません
前に di client.connect()。クライアントがメッセージを送信する
initialize すぐに connect()、サーバーはすでに
応答するために耳を傾けてください。
HTTPトランスポートセキュリティ
MCP サーバーが HTTP 経由で公開される場合、セキュリティが重要な側面になります。ここにいます 実行すべき重要な対策。
起源の検証
の攻撃を防ぐには、 DNSの再バインド、ヘッダーを検証します Origin 各リクエストの:
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowed = [
"http://localhost:3000",
"https://myapp.example.com"
];
if (origin && !allowed.includes(origin)) {
res.status(403).json({ error: "Origin non consentito" });
return;
}
next();
});
ローカル バインディングとベアラー認証
ローカルサーバーの場合のみバインドします 127.0.0.1。リモートサーバーの場合、認証を実装します
Bearer トークンの場合:
// Binding solo locale
app.listen(port, "127.0.0.1", () => {
console.log(`Server solo su localhost:${port}`);
});
// Autenticazione per server remoti
app.use("/mcp", (req, res, next) => {
const auth = req.headers.authorization;
if (!auth?.startsWith("Bearer ")) {
res.status(401).json({ error: "Token mancante" });
return;
}
const token = auth.slice(7);
if (!isValidToken(token)) {
res.status(403).json({ error: "Token non valido" });
return;
}
next();
});
まとめ
この記事では、この側面について詳しく調査しました クライアント MCPプロトコルの そして 2 つの基本的な転送メカニズムです。主要な概念は次のとおりです。
- Un MCPクライアント 1 つ以上のサーバーに接続し、ツールを検出し、言語モデルに代わってそれらを呼び出します。
- Il ツール使用ループ クライアントが仲介者として機能し、AI がどのツールを呼び出すかを自律的に決定できるようになります
- スタジオ ローカル開発とデスクトップ統合 (Claude Desktop、Cursor) に最適です。
- ストリーミング可能なHTTP リモートサーバー、複数の接続、実稼働環境に必要
- La セッション管理 セッション ID を使用すると、異なるクライアント間の状態の分離が保証されます。
- パターン デュアルトランスポート 同じコードで両方のモードをサポートできます
- インメモリトランスポート プロセスやHTTPサーバーを使用せずにテストを簡素化します
次の記事ではさらに詳しく掘り下げていきます 共有パッケージ Tech-MCP モノレポの: 共通ユーティリティ、サーバー間通信用の EventBus およびモジュールを備えたコア パッケージ データベースへのアクセス。 TypeScript モノリポジトリで再利用可能なコードを構造化する方法を見ていきます。 プロフェッショナル。
すべての例を含む完全なコードはリポジトリで入手できます。 GitHub 上の Tech-MCP.







