MCP 클라이언트 만들기 - Node.js
시스템 요구사항
시작하기 전에 아래 요구사항을 만족하는지 확인하세요:
- Mac 또는 Windows 컴퓨터
- Node.js 16 이상
- npm(Node.js에 포함)
환경 설정
먼저 새 Node.js 프로젝트를 만듭니다:
# Create project directory
mkdir mcp-client
cd mcp-client
# Initialize npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv
npm install -D typescript @types/node
# Create TypeScript config
npx tsc --init
# Create necessary files
mkdir src
touch src/client.ts
touch .env필요한 설정을 추가하기 위해 package.json을 업데이트합니다:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node build/client.js"
}
}tsconfig.json도 적절한 설정으로 업데이트합니다:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}API 키 설정
Anthropic Console에서 Anthropic API 키가 필요합니다.
.env 파일을 만듭니다:
ANTHROPIC_API_KEY=your_key_here.gitignore에 .env를 추가합니다:
echo ".env" >> .gitignore클라이언트 만들기
먼저 src/client.ts에서 import를 준비하고 기본 클라이언트 클래스를 만듭니다:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import { Tool } from "@anthropic-ai/sdk/resources/messages.js";
import {
CallToolResultSchema,
ListToolsResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as readline from "node:readline";
dotenv.config();
interface MCPClientConfig {
name?: string;
version?: string;
}
class MCPClient {
private client: Client | null = null;
private anthropic: Anthropic;
private transport: StdioClientTransport | null = null;
constructor(config: MCPClientConfig = {}) {
this.anthropic = new Anthropic();
}
// Methods will go here
}서버 연결 관리
다음으로 MCP 서버에 연결하는 메서드를 구현합니다:
async connectToServer(serverScriptPath: string): Promise<void> {
const isPython = serverScriptPath.endsWith(".py");
const isJs = serverScriptPath.endsWith(".js");
if (!isPython && !isJs) {
throw new Error("Server script must be a .py or .js file");
}
const command = isPython ? "python" : "node";
this.transport = new StdioClientTransport({
command,
args: [serverScriptPath],
});
this.client = new Client(
{
name: "mcp-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
await this.client.connect(this.transport);
// List available tools
const response = await this.client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
console.log(
"\nConnected to server with tools:",
response.tools.map((tool: any) => tool.name)
);
}질의 처리 로직
이제 질의를 처리하고 도구 호출(tool call)을 다루는 핵심 기능을 추가합니다:
async processQuery(query: string): Promise<string> {
if (!this.client) {
throw new Error("Client not connected");
}
// Initialize messages array with user query
let messages: Anthropic.MessageParam[] = [
{
role: "user",
content: query,
},
];
// Get available tools
const toolsResponse = await this.client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
const availableTools: Tool[] = toolsResponse.tools.map((tool: any) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
}));
const finalText: string[] = [];
let currentResponse = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages,
tools: availableTools,
});
// Process the response and any tool calls
while (true) {
// Add Claude's response to final text and messages
for (const content of currentResponse.content) {
if (content.type === "text") {
finalText.push(content.text);
} else if (content.type === "tool_use") {
const toolName = content.name;
const toolArgs = content.input;
// Execute tool call
const result = await this.client.request(
{
method: "tools/call",
params: {
name: toolName,
args: toolArgs,
},
},
CallToolResultSchema
);
finalText.push(
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
);
// Add Claude's response (including tool use) to messages
messages.push({
role: "assistant",
content: currentResponse.content,
});
// Add tool result to messages
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: content.id,
content: [
{ type: "text", text: JSON.stringify(result.content) },
],
},
],
});
// Get next response from Claude with tool results
currentResponse = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages,
tools: availableTools,
});
// Add Claude's interpretation of the tool results to final text
if (currentResponse.content[0]?.type === "text") {
finalText.push(currentResponse.content[0].text);
}
// Continue the loop to process any additional tool calls
continue;
}
}
// If we reach here, there were no tool calls in the response
break;
}
return finalText.join("\n");
}대화형 채팅 인터페이스
채팅 루프와 리소스 정리(cleanup) 기능을 추가합니다:
async chatLoop(): Promise<void> {
console.log("\nMCP Client Started!");
console.log("Type your queries or 'quit' to exit.");
// Using Node's readline for console input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = () => {
rl.question("\nQuery: ", async (query: string) => {
try {
if (query.toLowerCase() === "quit") {
await this.cleanup();
rl.close();
return;
}
const response = await this.processQuery(query);
console.log("\n" + response);
askQuestion();
} catch (error) {
console.error("\nError:", error);
askQuestion();
}
});
};
askQuestion();
}
async cleanup(): Promise<void> {
if (this.transport) {
await this.transport.close();
}
}메인 엔트리 포인트
마지막으로 클래스 바깥에 메인 실행 로직을 추가합니다:
// Main execution
async function main() {
if (process.argv.length < 3) {
console.log("Usage: ts-node client.ts <path_to_server_script>");
process.exit(1);
}
const client = new MCPClient();
try {
await client.connectToServer(process.argv[2]);
await client.chatLoop();
} catch (error) {
console.error("Error:", error);
await client.cleanup();
process.exit(1);
}
}
// Run main if this is the main module
if (import.meta.url === new URL(process.argv[1], "file:").href) {
main();
}
export default MCPClient;클라이언트 실행
임의의 MCP 서버와 함께 클라이언트를 실행하려면:
# Build the TypeScript code. Make sure to rerun this every time you update `client.ts`!
npm run build
# Run the client
node build/client.js path/to/server.py # for Python servers
node build/client.js path/to/server.js # for Node.js servers클라이언트는 다음을 수행합니다:
- 지정한 서버에 연결
- 사용 가능한 도구 목록 출력
- 대화형 채팅 세션 시작(가능한 작업):
- 질의 입력
- 도구 실행 확인
- Claude 응답 확인
핵심 구성 요소 설명
1. 클라이언트 초기화
MCPClient클래스에서 세션 관리와 API 클라이언트를 초기화합니다.- 기본 capability로 MCP 클라이언트를 설정합니다.
- Claude 상호작용을 위해 Anthropic 클라이언트를 구성합니다.
2. 서버 연결
- Python/Node.js 서버를 모두 지원합니다.
- 서버 스크립트 타입을 검증합니다.
- 통신 채널을 올바르게 설정합니다.
- 연결 시 사용 가능한 도구를 나열합니다.
3. 질의 처리
- 대화 컨텍스트를 유지합니다.
- Claude의 응답과 도구 호출을 처리합니다.
- Claude ↔ 도구 간 메시지 흐름을 관리합니다.
- 결과를 자연스러운 응답으로 결합합니다.
4. 대화형 인터페이스
- 간단한 CLI를 제공합니다.
- 사용자 입력을 받고 응답을 출력합니다.
- 기본 오류 처리를 포함합니다.
- 정상 종료(graceful exit)를 지원합니다.
5. 리소스 관리
- 리소스를 올바르게 정리합니다.
- 연결 문제에 대한 오류 처리를 포함합니다.
- 정상 종료 절차를 제공합니다.
자주 커스터마이징하는 지점
도구 처리
- 특정 도구 유형에 맞게
processQuery()를 수정 - 도구 호출에 대한 커스텀 오류 처리 추가
- 도구별 응답 포맷팅 구현
- 특정 도구 유형에 맞게
응답 처리
- 도구 결과를 어떻게 포맷팅할지 커스터마이징
- 응답 필터링/변환 추가
- 커스텀 로깅 구현
사용자 인터페이스
- GUI 또는 웹 인터페이스 추가
- 콘솔 출력 고도화
- 커맨드 히스토리/자동완성 추가
모범 사례
오류 처리
- 도구 호출은 항상 try-catch로 감싸기
- 의미 있는 오류 메시지 제공
- 연결 문제를 정상적으로 처리
리소스 관리
- 적절한 cleanup 메서드 사용
- 작업이 끝나면 연결 종료
- 서버 연결 해제(disconnection) 처리
보안
- API 키는
.env에 안전하게 저장 - 서버 응답 검증
- 도구 권한 설정에 주의
- API 키는
트러블슈팅
서버 경로 문제
- 서버 스크립트 경로를 다시 확인하세요.
- 상대 경로가 동작하지 않으면 절대 경로를 사용하세요.
- Windows에서는 슬래시(/) 또는 이스케이프된 백슬래시(\)를 사용하세요.
- 서버 파일 확장자가 올바른지 확인하세요(.py 또는 .js).
올바른 경로 사용 예시:
# Relative path
node build/client.js ./server/weather.js
# Absolute path
node build/client.js /Users/username/projects/mcp-server/weather.js
# Windows path (either format works)
node build/client.js C:/projects/mcp-server/weather.js
node build/client.js C:\\projects\\mcp-server\\weather.js연결 문제
- 서버 스크립트가 존재하고 권한이 올바른지 확인하세요.
- 서버 스크립트가 실행 가능한지 확인하세요.
- 서버 스크립트 의존성이 설치되어 있는지 확인하세요.
- 서버 스크립트를 직접 실행해 오류가 있는지 확인하세요.
도구 실행 문제
- 서버 로그에서 오류 메시지를 확인하세요.
- 도구 입력 인자가 스키마와 일치하는지 확인하세요.
- 도구 의존성이 사용 가능한지 확인하세요.
- 실행 흐름을 추적하기 위해 디버그 로깅을 추가하세요.