본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 7장: MCP 클라이언트 구현하기
2026년 2월 2일·AI / ML·

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

15분1,267자8개 섹션
mcptypescriptpython
공유
mcp-guide7 / 10
12345678910
이전6장: Python FastMCP로 서버 구축하기다음8장: 기존 시스템과 MCP 연동하기

클라이언트의 역할

MCP 클라이언트는 호스트 애플리케이션과 MCP 서버 사이의 프로토콜 브릿지 역할을 합니다. Claude Desktop이나 VS Code 같은 기존 호스트를 사용한다면 클라이언트는 이미 구현되어 있습니다. 하지만 자체 AI 애플리케이션을 구축하거나, 기존 시스템에 MCP 기능을 통합하려면 클라이언트를 직접 구현해야 합니다.

클라이언트의 핵심 책임은 다음과 같습니다.

  1. MCP 서버와의 연결을 수립하고 초기화 핸드셰이크를 수행합니다.
  2. 서버의 도구, 리소스, 프롬프트 목록을 조회합니다.
  3. LLM의 도구 호출 요청을 MCP 프로토콜 형식으로 변환하여 서버에 전달합니다.
  4. 서버의 응답을 LLM에 다시 전달합니다.

TypeScript 클라이언트 구현

기본 연결

MCP TypeScript SDK의 Client 클래스를 사용하여 서버에 연결합니다.

basic-client.ts
typescript
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;
}

서버 기능 탐색

연결이 수립되면 서버가 제공하는 프리미티브를 조회합니다.

서버 탐색
typescript
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);
  }
}

도구 호출

도구 호출
typescript
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("도구 실행 중 에러가 발생했습니다.");
  }
}

리소스 읽기

리소스 읽기
typescript
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);
  }
}

LLM 통합: 에이전트 루프

MCP 클라이언트의 진정한 가치는 LLM과 결합할 때 드러납니다. LLM이 도구 호출을 결정하면, MCP 클라이언트가 이를 실행하고 결과를 다시 LLM에 전달하는 에이전트 루프를 구성합니다.

TypeScript 에이전트 루프 구현

agent-loop.ts
typescript
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 형식이므로 직접 매핑이 가능합니다.

Info

이 에이전트 루프는 5장에서 구축한 날씨 서버뿐 아니라, 어떤 MCP 서버와도 동작합니다. connect() 메서드에 다른 서버의 실행 명령을 전달하기만 하면 됩니다. 이것이 표준 프로토콜의 힘입니다.

다중 서버 연결

실전에서는 여러 MCP 서버를 동시에 연결해야 하는 경우가 많습니다. 날씨 서버, 데이터베이스 서버, GitHub 서버를 모두 연결하여 LLM이 필요에 따라 적절한 도구를 선택하도록 합니다.

multi-server-agent.ts
typescript
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();
}
Warning

여러 MCP 서버를 연결할 때 도구 이름이 충돌할 수 있습니다. 예를 들어 두 서버가 모두 read_file이라는 도구를 제공하면 어떤 서버의 도구를 호출해야 하는지 모호해집니다. 이를 방지하려면 서버별로 도구 이름에 접두사를 추가하거나, 도구 이름의 고유성을 사전에 검증하는 것이 좋습니다.

Python 클라이언트 구현

Python에서도 동일한 패턴으로 MCP 클라이언트를 구현할 수 있습니다.

python_agent.py
python
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())

Streamable HTTP 클라이언트

원격 MCP 서버에 연결하려면 Streamable HTTP 전송을 사용합니다.

http-client.ts
typescript
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과 통합하는 방법을 다루었습니다.

  • 기본 클라이언트: SDK의 Client 클래스로 서버에 연결하고, 도구 목록 조회, 도구 호출, 리소스 읽기를 수행합니다.
  • 에이전트 루프: MCP 도구 정의를 LLM API 형식으로 변환하고, LLM의 도구 호출 결정을 MCP를 통해 실행하는 루프를 구현합니다.
  • 다중 서버 연결: 여러 MCP 서버를 동시에 연결하여 LLM이 다양한 도구를 활용할 수 있도록 합니다.
  • 원격 연결: Streamable HTTP 전송으로 원격 MCP 서버에 연결합니다.

다음 장 미리보기

8장에서는 기존 시스템과 MCP를 연동하는 실전 패턴을 다룹니다. 데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 구체적인 방법을 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

8장: 기존 시스템과 MCP 연동하기

데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.

2026년 2월 4일·18분
AI / ML

6장: Python FastMCP로 서버 구축하기

Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.

2026년 1월 31일·17분
AI / ML

9장: 보안, 인증, 권한 관리

MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.

2026년 2월 6일·20분
이전 글6장: Python FastMCP로 서버 구축하기
다음 글8장: 기존 시스템과 MCP 연동하기

댓글

목차

약 15분 남음
  • 클라이언트의 역할
  • TypeScript 클라이언트 구현
    • 기본 연결
    • 서버 기능 탐색
    • 도구 호출
    • 리소스 읽기
  • LLM 통합: 에이전트 루프
    • TypeScript 에이전트 루프 구현
  • 다중 서버 연결
  • Python 클라이언트 구현
  • Streamable HTTP 클라이언트
  • 정리
  • 다음 장 미리보기