클라이언트 만들기 (Build a Client)¶
문서 색인 전체는 https://modelcontextprotocol.io/llms.txt 에서 볼 수 있어요.
이 클라이언트 튜토리얼에 들어가기 전에 MCP 서버 만들기 튜토리얼을 먼저 봐 두면 클라이언트와 서버가 어떻게 통신하는지 훨씬 쉽게 이해할 수 있어요. 이 문서는 그 서버 튜토리얼에서 만든 클라이언트를 프로그램으로 직접 띄워 보는 예제예요.
여러 언어로 된 완성 코드가 준비되어 있어요. 각 탭의 튜토리얼 코드는 아래 저장소에서 찾을 수 있습니다.
Python¶
이 튜토리얼의 완성 코드는 여기에서 볼 수 있어요.
프로젝트 설정¶
프로젝트 디렉터리를 만들고 가상 환경을 준비한 뒤 필요한 패키지를 설치합니다.
# Create project directory
uv init mcp-client
cd mcp-client
# Create virtual environment
uv venv
# Activate virtual environment
source .venv/bin/activate
# Install required packages
uv add mcp anthropic python-dotenv
환경 변수와 모델 설정¶
.env 파일의 환경 변수를 로드하고, 사용할 모델과 Anthropic 클라이언트를 준비합니다.
from dotenv import load_dotenv
load_dotenv() # load environment variables from .env
MODEL = "claude-opus-5"
anthropic = Anthropic()
Client는 프로그램이 서버와 대화하는 유일한 창구예요. 도구 목록을 보여 주거나, 하나를 호출하거나, 리소스를 읽는 일 모두 이 객체의 메서드로 이루어져요.
쿼리 처리¶
모델에 메시지와 사용 가능한 도구를 함께 보내고, 응답에서 도구 호출을 처리합니다.
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
final_text = []
tool_results = []
for content in response.content:
if content.type == 'text':
messages.append({"role": "user", "content": tool_results})
# Get next response from Claude
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)
여기서 async with 블록이 연결 생명주기 전체를 담당해요. 블록에 들어가면 서버를 띄우고 프로토콜 버전을 맞추고, 블록을 벗어나면 연결을 끊고 하위 프로세스를 종료해요. 수동으로 닫아야 할 게 없답니다.
클라이언트의 동작은 크게 이렇게 정리할 수 있어요:
- 연결이 열리면 사용 가능한 도구 목록을 가져온다.
- 쿼리 처리: 대화 맥락을 유지하고, Claude의 응답과 도구 호출을 처리하며, Claude와 도구 사이의 메시지 흐름을 관리하고, 결과를 하나의 응답으로 합친다.
- 대화형 인터페이스: 터미널에서 사용자 입력을 받아 주고받는다.
TypeScript¶
이 튜토리얼의 완성 코드는 여기에서 볼 수 있어요.
프로젝트 설정¶
# Create project directory
mkdir mcp-client-typescript
cd mcp-client-typescript
# Initialize npm project
npm init -y
# Install dependencies
npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
# Install dev dependencies
npm install -D @types/node typescript
# Create source file
touch index.ts
Windows에서는 PowerShell로 이렇게 입력하면 됩니다.
# Create project directory
md mcp-client-typescript
cd mcp-client-typescript
# Initialize npm project
npm init -y
# Install dependencies
npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
tsconfig.json의 컴파일 옵션은 프로젝트 구조에 맞춰 이렇게 설정해 둡니다.
"outDir": "./build",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
API 키 설정¶
API 키를 준비하고 클라이언트가 서버 스크립트를 실행할 수 있게 준비합니다. 운영체제에 따라 인터프리터 명령이 달라지는데, 이 예제는 이렇게 처리해요.
? process.platform === "win32"
? "python"
: "python3"
: process.execPath;
this.transport = new StdioClientTransport({
command,
args: [serverScriptPath],
});
await this.mcp.connect(this.transport);
const toolsResult = await this.mcp.listTools();
클라이언트 실행¶
어떤 MCP 서버와든 이렇게 실행할 수 있어요.
# Build TypeScript
npm run build
# Run the client
node build/index.js path/to/server.py # python server
node build/index.js path/to/build/index.js # node server
서버 스크립트 경로는 절대 경로나 Windows 경로 둘 다 잘 동작합니다.
# Absolute path
node build/index.js /Users/username/projects/mcp-server/build/index.js
# Windows path (either format works)
node build/index.js C:/projects/mcp-server/build/index.js
node build/index.js C:\\projects\\mcp-server\\build\\index.js
Java¶
이 예제는 Spring AI의 MCP 자동 설정과 부트 스타터를 쓴 퀵스타트 데모예요. 동기·비동기 MCP 클라이언트를 직접 만드는 방법은 Java SDK Client 문서를 참고하세요.
이 예제는 Spring AI의 모델 컨텍스트 프로토콜(MCP)과 Brave Search MCP Server를 결합해 대화형 챗봇을 만드는 방법을 보여줘요. Anthropic의 Claude AI 모델이 구동하는 대화형 인터페이스를 만들어, 사용자가 자연어로 실시간 웹 데이터를 검색하게 하는 구조예요. 완성 코드는 여기에서 볼 수 있습니다.
프로젝트를 준비합니다.
API 키를 설정하고 빌드합니다.
export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
export BRAVE_API_KEY='your-brave-api-key-here'
MCP 클라이언트 설정¶
먼저 pom.xml에 필요한 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
다음으로 application.yml에 애플리케이션 속성을 설정합니다.
spring:
ai:
mcp:
client:
enabled: true
name: brave-search-client
version: 1.0.0
type: SYNC
request-timeout: 20s
stdio:
root-change-notification: true
servers-configuration: classpath:/mcp-servers-config.json
toolcallback:
enabled: true
anthropic:
api-key: ${ANTHROPIC_API_KEY}
이 설정은 spring-ai-starter-mcp-client가 서버 설정에 따라 하나 이상의 McpClient를 만들도록 해요. spring.ai.mcp.client.toolcallback.enabled=true 속성은 모든 MCP 도구를 자동으로 Spring AI 도구로 등록하는 tool callback 메커니즘을 켜는데, 기본값은 꺼져 있어요.
mcp-servers-config.json에 MCP 서버를 정의합니다.
{
"mcpServers": {
"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
}
}
}
}
챗 구현¶
챗봇은 Spring AI의 ChatClient에 MCP 도구 연동을 붙여 구현합니다.
var chatClient = chatClientBuilder
.defaultSystem("You are useful assistant, expert in AI and Java.")
.defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
.build();
주요 특징을 정리하면 이래요.
- 자연어 이해에 Claude AI 모델을 사용한다
- MCP를 통해 Brave Search를 연동해 실시간 웹 검색을 한다
- InMemoryChatMemory로 대화 맥락을 유지한다
- 대화형 커맨드라인 애플리케이션으로 동작한다
빌드와 실행¶
실행하면 챗봇은 이렇게 동작해요.
- 필요할 때 Brave Search로 웹 검색을 수행한다
- 이전 메시지의 대화 맥락을 기억한다
- 여러 출처의 정보를 합쳐 종합적인 답변을 만든다
고급 설정¶
MCP 클라이언트는 추가 설정 옵션을 더 지원해요.
- 자동 클라이언트 초기화와 생명주기 관리
Streamable HTTP로 원격 MCP 서버에 연결하려면 연결 URL을 설정합니다.
WebFlux 기반 애플리케이션이라면 WebFlux 스타터를 대신 쓸 수 있어요.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
Kotlin¶
이 튜토리얼의 완성 코드는 여기에서 볼 수 있어요.
프로젝트 설정¶
# Create a new directory for our project
mkdir kotlin-mcp-client
cd kotlin-mcp-client
# Initialize a new kotlin project
gradle init
Windows에서는 이렇게 합니다.
# Create a new directory for our project
md kotlin-mcp-client
cd kotlin-mcp-client
# Initialize a new kotlin project
gradle init
프로젝트를 만든 뒤 build.gradle.kts 내용을 아래처럼 교체합니다. 최신 버전은 https://github.com/modelcontextprotocol/kotlin-sdk/releases 에서 확인할 수 있어요.
// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
val mcpVersion = "0.9.0"
val ktorVersion = "3.2.3"
val anthropicVersion = "2.15.0"
기본 클라이언트 구조¶
먼저 기본 클라이언트 클래스를 만듭니다.
class MCPClient(apiKey: String) : AutoCloseable {
private val anthropic = AnthropicOkHttpClient.builder()
.apiKey(apiKey)
.build()
private val mcp: Client = Client(
clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")
)
private var serverProcess: Process? = null
private lateinit var tools: List<ToolUnion>
// methods will go here
override fun close() {
runBlocking {
mcp.close()
}
serverProcess?.destroy()
anthropic.close()
}
}
서버 연결 관리¶
MCP 서버에 연결하는 메서드를 구현합니다.
서버 퀵스타트의 weather 튜토리얼을 이어서 하고 있다면, 명령은 이런 형태가 될 거예요: java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar
클라이언트는 이렇게 실행됩니다.
java -jar build/libs/client.jar ./server/build/libs/server.jar
# Absolute path
java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
# Windows path (either format works)
java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
빌드 이슈¶
- 모든 의존성을 담은 shadow JAR을 만들려면
./gradlew build나./gradlew shadowJar를 써요../gradlew jar는 안 돼요.
C¶
이 튜토리얼의 완성 코드는 여기에서 볼 수 있어요.
먼저 Program.cs 파일에 기본 클라이언트 클래스를 준비합니다.
using Anthropic.SDK;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"]))
.Messages
.AsBuilder()
.UseFunctionInvocation()
.Build();
var options = new ChatOptions
{
MaxOutputTokens = 1000,
ModelId = "claude-opus-5",
Tools = [.. tools]
};
Ruby¶
클라이언트는 stdio 전송으로 서버에 연결하고 도구 목록을 가져옵니다.
@transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
@mcp_client = MCP::Client.new(transport: @transport)
@mcp_client.connect
tool_names = @mcp_client.tools.map(&:name)
puts "\nConnected to server with tools: #{tool_names}"
end
동작을 정리하면 아래와 같아요.
- stdio 전송에
MCP::Client::Stdio를 사용한다 - MCP 클라이언트를 초기화하고 사용 가능한 도구를 나열한다
- 쿼리 처리: MCP 도구를 Anthropic 도구 형식(
name,description,input_schema)으로 매핑하고,Anthropic::Models::TextBlock과Anthropic::Models::ToolUseBlock으로 패턴 매칭한다
클라이언트는 이렇게 실행합니다.
Rust¶
이 예제에서는 reqwest = "0.12.23" 의존성과 함께 Rust MCP SDK와 자식 프로세스 전송을 제공하는 rmcp 크레이트를 써요. 모델 요청과 도구 표현에는 genai 크레이트를 사용해 Claude에 요청을 보냅니다.
먼저 임포트, 모델 상수, 기본 클라이언트 구조를 추가합니다.
use anyhow::{Context, Result, bail};
use genai::Client;
use genai::chat::{
ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
};
use rmcp::model::{CallToolRequestParam, Tool as McpTool};
use rmcp::service::{RoleClient, RunningService, ServiceExt};
use rmcp::transport::TokioChildProcess;
use serde_json::Value;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tokio::process::Command;
const MODEL_ANTHROPIC: &str = "claude-opus-5";
struct MCPClient {
anthropic: Client,
session: Option<RunningService<RoleClient, ()>>,
tools: Vec<GenaiTool>,
}
이 클라이언트는 모델 API 클라이언트, 활성 MCP 세션, 연결된 서버가 알려 준 도구 목록을 함께 보관해요.
클라이언트 초기화¶
모델 클라이언트를 초기화하고, MCP 세션이나 도구 없이 시작합니다.
impl MCPClient {
fn new() -> Result<Self> {
Ok(MCPClient {
anthropic: Client::default(),
session: None,
tools: Vec::new(),
})
}
let process = TokioChildProcess::new(command)
.with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
let session = ().serve(process).await?;
let rmcp_tools = session
.list_all_tools()
.await
.context("Unable to list tools from server")?;
연결 과정은 이렇게 이뤄져요.
- 커맨드라인에 준 명령과 인자로 서버를 자식 프로세스로 띄운다
- stdio로 MCP 세션을 연다
- 서버가 알려 준 모든 도구를 나열한다
- 그 도구들을 모델 요청에 쓰는 형식으로 변환한다
MCP 도구 변환¶
MCP와 모델 API는 비슷한 정보를 서로 다른 Rust 타입으로 표현해요. convert_tools는 각 MCP 도구의 이름·설명·입력 스키마를 genai 도구 정의로 매핑합니다.
모델 요청 보내기¶
impl MCPClient 안에 이 헬퍼 메서드를 추가합니다.
async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
let response = self
.anthropic
.exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
.await
.context("Anthropic chat request failed")?;
Ok(response)
}
이렇게 하면 모델 요청 처리를 한곳에 모으고, API 요청이 실패할 때 유용한 맥락까지 붙일 수 있어요.
쿼리 처리 로직¶
이제 핵심 쿼리 처리 메서드를 impl MCPClient 안에 추가합니다.
async fn process_query(&mut self, query: &str) -> Result<String> {
let session = self
.session
.as_ref()
.context("Client is not connected to any server")?;
let mut messages = vec![ChatMessage::user(query)];
let mut final_text = Vec::new();
// Initial Claude API call with tools
.context("Failed to serialize tool result")?;
tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
tool_call.call_id.clone(),
payload,
)));
}
// Append tool responses to message history
messages.push(ChatMessage::user(tool_results));
// Build the next request and query model
chat_req = ChatRequest::new(messages.clone());
chat_rsp = self.request_model(&chat_req).await?;
// Collect text from response
for text in chat_rsp.texts() {
main 함수에서 서버 인자를 파싱하고 클라이언트를 실행합니다.
dotenvy::dotenv().context("Failed to load env file")?;
let mut args = std::env::args();
let _ = args.next();
let server_args: Vec<String> = args.collect();
if server_args.is_empty() {
eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
구조를 정리하면 이래요.
new,connect_to_server,process_query,request_model,chat_loop,cleanup은 하나의impl MCPClient블록 안의 메서드다main과convert_tools는impl MCPClient블록 밖의 함수다
실행 흐름은 이렇게 됩니다.
- 응답이 터미널에 표시된다
모범 사례¶
- 오류 처리
- 프로세스, MCP, 모델 API, 직렬화 경계에서 오류에 맥락을 붙인다
- 개별 쿼리 오류를 대화형 세션을 종료하지 않고 보고한다
- 서버 명령을 실행하기 전에 검증한다
- 서버가 노출한 도구를 모델 주도 호출을 허용하기 전에 검토한다
- 신뢰할 수 있는 서버와 실행 가능한 명령에만 연결한다
문제 해결¶
서버 명령 이슈¶
cargo run -- 뒤의 인자는 완전한 명령을 이뤄야 해요. 인터프리터로 실행되는 서버 스크립트는 그 런타임이 필요합니다. 명령을 찾지 못하면 절대 경로를 쓰거나 PATH에 있는지 확인하세요.
환경 파일 이슈¶
Failed to load env file 오류가 보이면 클라이언트를 실행한 디렉터리에 .env가 있는지 확인하세요. 모델 요청에서 API 키가 없다고 나오면 .env에 키가 들어 있는지 확인해야 합니다.
Failed to serialize tool result: 서버 응답에 지원되지 않거나 잘못된 형식의 콘텐츠가 없는지 살펴본다
다음 단계¶
예제 서버 문서에서 공식 MCP 서버와 구현 갤러리를 살펴볼 수 있어요.