MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.
MCP 클라이언트는 호스트 애플리케이션과 MCP 서버 사이의 프로토콜 브릿지 역할을 합니다. Claude Desktop이나 VS Code 같은 기존 호스트를 사용한다면 클라이언트는 이미 구현되어 있습니다. 하지만 자체 AI 애플리케이션을 구축하거나, 기존 시스템에 MCP 기능을 통합하려면 클라이언트를 직접 구현해야 합니다.
클라이언트의 핵심 책임은 다음과 같습니다.
MCP TypeScript SDK의 Client 클래스를 사용하여 서버에 연결합니다.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function createClient() {
// 전송 계층 설정 (서버를 자식 프로세스로 실행)
const transport = new StdioClientTransport({
command: "node",
args: ["path/to/server.js"],
env: {
...process.env,
API_KEY: "your-api-key",
},
});
// 클라이언트 생성 및 연결
const client = new Client({
name: "my-app",
version: "1.0.0",
}, {
capabilities: {
sampling: {}, // 샘플링 지원 선언
},
});
await client.connect(transport);
return client;
}연결이 수립되면 서버가 제공하는 프리미티브를 조회합니다.
async function exploreServer(client: Client) {
// 도구 목록 조회
const toolsResult = await client.listTools();
console.log("사용 가능한 도구:");
for (const tool of toolsResult.tools) {
console.log(" - " + tool.name + ": " + tool.description);
}
// 리소스 목록 조회
const resourcesResult = await client.listResources();
console.log("사용 가능한 리소스:");
for (const resource of resourcesResult.resources) {
console.log(" - " + resource.uri + ": " + resource.description);
}
// 프롬프트 목록 조회
const promptsResult = await client.listPrompts();
console.log("사용 가능한 프롬프트:");
for (const prompt of promptsResult.prompts) {
console.log(" - " + prompt.name + ": " + prompt.description);
}
}async function callTool(client: Client) {
const result = await client.callTool({
name: "get_current_weather",
arguments: {
city: "서울",
units: "celsius",
},
});
// result.content는 TextContent, ImageContent 등의 배열
for (const item of result.content as Array<{ type: string; text?: string }>) {
if (item.type === "text") {
console.log("결과:", item.text);
}
}
// 에러 여부 확인
if (result.isError) {
console.error("도구 실행 중 에러가 발생했습니다.");
}
}async function readResource(client: Client) {
const result = await client.readResource({
uri: "weather://cities",
});
for (const content of result.contents) {
console.log("URI:", content.uri);
console.log("내용:", content.text);
}
}MCP 클라이언트의 진정한 가치는 LLM과 결합할 때 드러납니다. LLM이 도구 호출을 결정하면, MCP 클라이언트가 이를 실행하고 결과를 다시 LLM에 전달하는 에이전트 루프를 구성합니다.
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
class McpAgent {
private anthropic: Anthropic;
private mcpClient: Client;
constructor() {
this.anthropic = new Anthropic();
this.mcpClient = new Client({ name: "agent", version: "1.0.0" });
}
async connect(command: string, args: string[]) {
const transport = new StdioClientTransport({ command, args });
await this.mcpClient.connect(transport);
}
async run(userMessage: string): Promise<string> {
// 1. MCP 서버에서 사용 가능한 도구 목록을 가져옵니다
const toolsResult = await this.mcpClient.listTools();
// 2. MCP 도구 정의를 Anthropic API 형식으로 변환합니다
const tools: Anthropic.Tool[] = toolsResult.tools.map((tool) => ({
name: tool.name,
description: tool.description || "",
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
}));
// 3. 대화 메시지 초기화
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
// 4. 에이전트 루프
while (true) {
const response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: tools,
messages: messages,
});
// 5. 최종 응답이면 텍스트를 추출하여 반환
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock && "text" in textBlock ? textBlock.text : "";
}
// 6. 도구 호출 처리
if (response.stop_reason === "tool_use") {
// 어시스턴트 응답을 대화에 추가
messages.push({ role: "assistant", content: response.content });
// 각 도구 호출을 MCP를 통해 실행
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const mcpResult = await this.mcpClient.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>,
});
const resultText = (mcpResult.content as Array<{ text?: string }>)
.map((c) => c.text || "")
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: resultText,
is_error: mcpResult.isError === true,
});
}
}
// 도구 결과를 대화에 추가
messages.push({ role: "user", content: toolResults });
}
}
}
async disconnect() {
await this.mcpClient.close();
}
}
// 사용
async function main() {
const agent = new McpAgent();
await agent.connect("node", ["path/to/weather-server.js"]);
const answer = await agent.run("서울과 부산의 날씨를 비교해 주세요.");
console.log(answer);
await agent.disconnect();
}
main().catch(console.error);이 코드의 핵심은 MCP 도구 정의를 LLM API의 도구 정의로 변환하는 부분입니다. MCP의 inputSchema는 JSON Schema 형식이며, Anthropic API의 input_schema도 JSON Schema 형식이므로 직접 매핑이 가능합니다.
이 에이전트 루프는 5장에서 구축한 날씨 서버뿐 아니라, 어떤 MCP 서버와도 동작합니다. connect() 메서드에 다른 서버의 실행 명령을 전달하기만 하면 됩니다. 이것이 표준 프로토콜의 힘입니다.
실전에서는 여러 MCP 서버를 동시에 연결해야 하는 경우가 많습니다. 날씨 서버, 데이터베이스 서버, GitHub 서버를 모두 연결하여 LLM이 필요에 따라 적절한 도구를 선택하도록 합니다.
class MultiServerAgent {
private anthropic: Anthropic;
private clients: Map<string, Client> = new Map();
private allTools: Map<string, { client: Client; tool: Anthropic.Tool }> = new Map();
constructor() {
this.anthropic = new Anthropic();
}
async addServer(name: string, command: string, args: string[]) {
const client = new Client({ name: "agent-" + name, version: "1.0.0" });
const transport = new StdioClientTransport({ command, args });
await client.connect(transport);
this.clients.set(name, client);
// 도구 목록을 가져와 전체 도구 맵에 추가
const toolsResult = await client.listTools();
for (const tool of toolsResult.tools) {
this.allTools.set(tool.name, {
client: client,
tool: {
name: tool.name,
description: tool.description || "",
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
},
});
}
}
async run(userMessage: string): Promise<string> {
// 모든 서버의 도구를 하나의 배열로 결합
const tools = Array.from(this.allTools.values()).map((t) => t.tool);
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
while (true) {
const response = await this.anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: tools,
messages: messages,
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock && "text" in textBlock ? textBlock.text : "";
}
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
// 도구 이름으로 해당 서버의 클라이언트를 찾습니다
const entry = this.allTools.get(block.name);
if (!entry) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: "도구를 찾을 수 없습니다: " + block.name,
is_error: true,
});
continue;
}
const mcpResult = await entry.client.callTool({
name: block.name,
arguments: block.input as Record<string, unknown>,
});
const resultText = (mcpResult.content as Array<{ text?: string }>)
.map((c) => c.text || "")
.join("\n");
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: resultText,
is_error: mcpResult.isError === true,
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
}
async disconnectAll() {
for (const client of this.clients.values()) {
await client.close();
}
}
}
// 사용
async function main() {
const agent = new MultiServerAgent();
await agent.addServer("weather", "node", ["weather-server.js"]);
await agent.addServer("files", "python", ["file-server.py"]);
await agent.addServer("github", "npx", ["-y", "@modelcontextprotocol/server-github"]);
const answer = await agent.run(
"현재 프로젝트의 README를 읽고, 서울 날씨 정보를 추가한 후 커밋해 주세요."
);
console.log(answer);
await agent.disconnectAll();
}여러 MCP 서버를 연결할 때 도구 이름이 충돌할 수 있습니다. 예를 들어 두 서버가 모두 read_file이라는 도구를 제공하면 어떤 서버의 도구를 호출해야 하는지 모호해집니다. 이를 방지하려면 서버별로 도구 이름에 접두사를 추가하거나, 도구 이름의 고유성을 사전에 검증하는 것이 좋습니다.
Python에서도 동일한 패턴으로 MCP 클라이언트를 구현할 수 있습니다.
import asyncio
import json
from contextlib import AsyncExitStack
import anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
class McpAgent:
def __init__(self):
self.client = anthropic.Anthropic()
self.session: ClientSession | None = None
self.exit_stack = AsyncExitStack()
async def connect(self, command: str, args: list[str]):
server_params = StdioServerParameters(command=command, args=args)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
read_stream, write_stream = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(read_stream, write_stream)
)
await self.session.initialize()
async def run(self, user_message: str) -> str:
if not self.session:
raise RuntimeError("서버에 연결되지 않았습니다.")
# MCP 도구를 Anthropic 형식으로 변환
tools_result = await self.session.list_tools()
tools = [
{
"name": tool.name,
"description": tool.description or "",
"input_schema": tool.inputSchema,
}
for tool in tools_result.tools
]
messages = [{"role": "user", "content": user_message}]
while True:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
messages.append({
"role": "assistant",
"content": [b.model_dump() for b in response.content],
})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = await self.session.call_tool(
block.name, arguments=block.input
)
result_text = "\n".join(
c.text for c in result.content if hasattr(c, "text")
)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result_text,
})
messages.append({"role": "user", "content": tool_results})
async def disconnect(self):
await self.exit_stack.aclose()
async def main():
agent = McpAgent()
await agent.connect("python", ["weather_server.py"])
answer = await agent.run("서울 날씨를 알려주세요.")
print(answer)
await agent.disconnect()
if __name__ == "__main__":
asyncio.run(main())원격 MCP 서버에 연결하려면 Streamable HTTP 전송을 사용합니다.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
async function connectToRemoteServer() {
const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.example.com/api/mcp"),
{
requestInit: {
headers: {
Authorization: "Bearer your-access-token",
},
},
}
);
const client = new Client({ name: "remote-client", version: "1.0.0" });
await client.connect(transport);
return client;
}이 장에서는 MCP 클라이언트를 직접 구현하고 LLM과 통합하는 방법을 다루었습니다.
Client 클래스로 서버에 연결하고, 도구 목록 조회, 도구 호출, 리소스 읽기를 수행합니다.8장에서는 기존 시스템과 MCP를 연동하는 실전 패턴을 다룹니다. 데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 구체적인 방법을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.
Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.
MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.