소개: 서버에서 MCP 클라이언트로
이전 기사에서는 도구, 리소스 및 프롬프트를 표시하는 MCP 서버를 구축했습니다. 그런데 누가 소비하다 이 기능은? 그만큼 MCP 클라이언트 그리고 귀하가 사용하는 소프트웨어 구성요소 하나 이상의 서버에 연결하고 사용 가능한 도구를 검색하여 언어 모델을 대신하여 호출합니다.
이 기사에서는 TypeScript에서 프로그래밍 방식의 MCP 클라이언트를 구축하는 방법을 살펴보고 두 가지 주요 전송 메커니즘(STDIO e 스트리밍 가능한 HTTP), 세션 관리를 살펴보고 MCP 서버를 웹 애플리케이션에 통합하는 방법을 살펴보겠습니다. 익스프레스. 모든 코드는 저장소에서 사용 가능 Tech-MCP.
이 기사에서 배울 내용
- 공식 TypeScript SDK를 사용하여 MCP 클라이언트를 생성하는 방법
- 도구 사용 주기: 쿼리, AI 결정, 호출, 응답
- Transport STDIO: 하위 프로세스를 통한 로컬 연결
- Transport Streamable HTTP: 네트워크를 통해 접근 가능한 독립 서버
- 세션 ID 및 재접속을 통한 세션 관리
- 테스트를 위한 다중 서버 패턴 및 InMemoryTransport
- HTTP 전송 보안: 원본 검증, 베어러 인증
클라이언트 프로젝트 설정
MCP 클라이언트를 처음부터 생성하려면 TypeScript 프로젝트 설정부터 시작하겠습니다. SDK
@modelcontextprotocol/sdk 클라이언트 측에 필요한 모든 것을 제공합니다.
동안 @anthropic-ai/sdk Claude를 의사결정 모델로 통합할 수 있습니다.
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-...
Transport STDIO를 사용하는 MCP 클라이언트
운송 STDIO 클라이언트를 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" | | |
주기의 주요 단계
- 클라이언트는 Claude에게 사용자 쿼리를 보냅니다. 도구 목록과 함께 (이름, 설명, JSON 스키마)
- Claude는 쿼리를 분석하고 도구를 사용할지 여부를 결정합니다.
stop_reason: "tool_use") - 클라이언트는 MCP 서버에서 도구를 호출합니다.
client.callTool() - 결과는 Claude에게 다음과 같이 돌아옵니다.
tool_result - Claude는 다른 도구를 요청하거나 최종 응답을 생성할 수 있습니다(
stop_reason: "end_turn")
전송 STDIO와 스트리밍 가능한 HTTP
MCP SDK는 근본적으로 다른 두 가지 전송 메커니즘을 지원합니다. 둘 사이의 선택은 달라집니다 사용 사례, 시스템 아키텍처 및 배포 요구 사항을 살펴봅니다.
STDIO와 스트리밍 가능한 HTTP 비교
| 나는 기다린다 | STDIO | 스트리밍 가능한 HTTP |
|---|---|---|
| 건축학 | 클라이언트는 서버를 하위 프로세스로 시작합니다. | 서버는 독립적인 서비스입니다. |
| 사이 | 1:1(프로세스당 하나의 클라이언트) | N:1(여러 클라이언트, 하나의 서버) |
| 전개 | 로컬 전용, 동일한 호스트 | 로컬 또는 원격, 모든 네트워크 |
| 세션 | 암시적(프로세스 수명) | 명시적(헤더의 세션 ID) |
| 입증 | 필요하지 않음(동일 호스트) | 필수(전달자 토큰, OAuth) |
| 확장성 | 제한됨(클라이언트당 하나의 프로세스) | 높음(서버 1개, 클라이언트 다수) |
| 사용 사례 | Claude Desktop, IDE, 로컬 개발 | 마이크로서비스, 공유 API, 프로덕션 |
| 설정 복잡성 | 최소한의 | HTTP 서버 필요(Express, Fastify) |
일반적으로, STDIO 지역 발전과 통합을 위한 최선의 선택 Claude Desktop 및 Cursor와 같은 데스크탑 애플리케이션. 스트리밍 가능한 HTTP 그리고 필요할 때 서버는 여러 클라이언트, 원격 호스트 또는 컨테이너화된 프로덕션 환경에서 액세스할 수 있어야 합니다.
전송 HTTP를 사용하는 MCP 서버(Express)
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 세 가지 HTTP 메소드가 있으며 각각 특정 기능을 가지고 있습니다.
MCP 프로토콜에서:
- POST /mcp - 클라이언트는 JSON-RPC(요청, 알림, 응답) 메시지를 보냅니다. 도구를 호출하기 위한 기본 채널입니다.
- GET /mcp - 클라이언트는 SSE(Server-Sent Events) 스트림을 열어 서버로부터 실시간으로 푸시 알림을 받습니다.
- 삭제 /mcp - 클라이언트가 세션을 명시적으로 종료하여 서버 측 리소스를 해제합니다.
STDIO와의 근본적인 차이점
STDIO를 사용하면 각 연결마다 새로운 서버 프로세스가 생성됩니다. HTTP를 사용하면 서버가 전송에 연결됩니다.
한 번만 ~와 함께 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 전송의 가장 중요한 측면 중 하나입니다. STDIO에 있는 동안 세션 암시적(프로세스 수명과 일치), HTTP에서 각 클라이언트 연결은 세션 고유 ID 모든 후속 요청에 포함되어야 합니다.
세션 작동 방식
- 클라이언트가 첫 번째 요청을 보냅니다.
initialize세션 ID 없이 - 서버는 다음을 사용하여 UUID를 생성합니다.
sessionIdGenerator헤더에 반환합니다.Mcp-Session-Id - 클라이언트에는 다음이 포함됩니다.
Mcp-Session-Id모든 후속 요청에서 - 서버는 각 요청을 올바른 세션과 연결하고 상태를 유지합니다.
- 연결이 끊어지면 클라이언트는 다음을 보냅니다.
DELETE /mcp세션을 종료하려면
StreamableHTTPClientTransport는 자동으로 다음을 처리합니다.
- 헤더 보내기
Mcp-Session-Id초기화 후 - 헤더 보내기
MCP-Protocol-Version모든 요청에 - 세션 만료 시 재접속(HTTP 404)
상태 저장 패턴: 세션별 상태
프로덕션에서는 각 세션이 자체 격리 상태를 가질 수 있습니다. 구현 방법은 다음과 같습니다.
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 서로 다른 서버가 충돌할 때 충돌을 피하는 데 필수적입니다.
같은 이름의 도구를 노출합니다. Claude는 전체 목록을 받고 클라이언트는 라우팅을 관리합니다.
이중 전송: 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 Desktop 및 로컬 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();
}
중요한 연결 순서
와 함께 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 프로토콜의 그리고 두 가지 기본 전송 메커니즘. 주요 개념은 다음과 같습니다.
- Un MCP 클라이언트 하나 이상의 서버에 연결하고, 도구를 검색하고, 언어 모델을 대신하여 호출합니다.
- Il 도구 사용 루프 클라이언트가 중개자 역할을 하면서 AI가 호출할 도구를 자율적으로 결정할 수 있습니다.
- STDIO 로컬 개발 및 데스크탑 통합에 이상적입니다(Claude Desktop, Cursor)
- 스트리밍 가능한 HTTP 원격 서버, 다중 연결 및 프로덕션 환경에 필요합니다.
- La 세션 관리 세션 ID를 사용하면 서로 다른 클라이언트 간의 상태 격리가 보장됩니다.
- 패턴 이중 운송 동일한 코드로 두 모드를 모두 지원할 수 있습니다
- 인메모리운송 프로세스나 HTTP 서버 없이 테스트를 단순화합니다.
다음 글에서는 좀 더 자세히 알아보겠습니다. 공유 패키지 Tech-MCP 모노레포의: 공통 유틸리티가 포함된 핵심 패키지, 서버 간 통신을 위한 EventBus 및 모듈 데이터베이스에 대한 액세스. TypeScript 모노레포에서 재사용 가능한 코드를 구성하는 방법을 살펴보겠습니다. 전문.
모든 예제가 포함된 전체 코드는 저장소에서 사용할 수 있습니다. GitHub의 Tech-MCP.







