MCP Deep Dive, Part 4: 모든 도구(및 모든 모델)에 연결되는 MCP 클라이언트 구축하기
요약
Model Context Protocol(MCP)을 활용하여 모델의 도구 요청을 실제 실행으로 변환하는 MCP 클라이언트 구축 방법을 다룹니다. 도구 발견, 에이전트 루프 구현, 서버 라우팅 및 안정적인 실행을 위한 재연결 전략을 설명합니다.
핵심 포인트
- 모델은 도구를 직접 호출하는 것이 아니라 요청하며, 클라이언트가 이를 실행하는 런타임 역할을 함
- MCP 클라이언트는 런타임에 도구, 리소스, 프롬프트를 자동으로 발견하여 하드코딩을 방지함
- JSON Schema를 통해 MCP 도구 정의를 모델의 도구 호출 형식으로 효율적으로 변환함
- 재연결, 타임아웃, 재시도 로직을 통해 프로덕션 환경의 에이전트 안정성을 확보함
MCP를 데모할 때 아무도 말해주지 않는 사실이 있습니다. 모델은 실제로 도구를 호출하지 않습니다. 모델은 도구를 _요청_할 뿐입니다. MCP 클라이언트는 이러한 요청을 실제 서버에 대한 실제 호출로 변환하고 결과를 다시 전달하는 런타임 (runtime)입니다. 이 루프 (loop)를 잘 구축하는 것이 "노트북에서 작동하는 것"과 "프로덕션 에이전트 (production agents)를 구동하는 것"을 가르는 차이점입니다.
이 글은 Model Context Protocol (MCP)에 대한 15부작 심층 분석 중 4부입니다. 3부에서는 mattrx-analytics **서버 (server)**를 구축했습니다. 이제 그 소비자인 **클라이언트 (client)**를 구축합니다. 이 클라이언트는 Mattrx 호스트 내에서 도구를 발견하고, 에이전트 루프 (agent loop)를 실행하며, 세 개의 서버 모두에 걸쳐 호출을 라우팅 (routing)합니다.
요약 (TL;DR)
| 고려 사항 | 맞춤형 에이전트 (이전) | MCP 클라이언트 (이후) |
|---|---|---|
| 도구 목록 | 하드코딩됨, 드리프트 발생 | 런타임에 발견됨 |
| ... |
- 클라이언트는 연결하고,
initialize를 실행하며, 도구/리소스/프롬프트를 **발견 (discovers)**합니다. 하드코딩된 목록은 없습니다. - MCP 도구 정의는 JSON Schema입니다. 이는 모델의 도구 호출 (tool-calling) 형식으로의 얇은 번역 과정입니다.
- **에이전트 루프 (agent loop)**가 곧 클라이언트입니다. 모델이 도구 호출을 요청하면, 클라이언트가 이를 실행하고 완료될 때까지 결과를 다시 피드백합니다.
- 하나의 **클라이언트 매니저 (client manager)**가 각 호출을 소유 서버로 라우팅합니다. 충돌 시 네임스페이스 (namespace)를 사용합니다.
- **재연결 (Reconnect) + 호출당 타임아웃 (per-call timeout) + 재시도 (retry)**를 통해 서버 재배포가 에이전트 실행을 중단시키지 않도록 합니다.
단 하나의 사고방식 전환: 모델은 절대 도구를 호출하지 않습니다. 모델은 도구를 _요청_합니다. 클라이언트는 이러한 요청을 실제 호출로 변환하고, 적절한 서버로 라우팅하며, 결과를 다시 전달하는 런타임입니다. 루프, 라우터, 재연결을 구축하면 어떤 모델이든 어떤 도구든 구동할 수 있습니다.
1. 연결 및 초기화 (Connect and initialize)
클라이언트를 생성하고, 전송 방식 (transport)을 선택한 다음, initialize 핸드셰이크 (handshake)를 실행합니다. 클라이언트는 자신의 역량 (capabilities, 서버가 콜백할 수 있는 것)을 선언하고 서버의 역량을 읽습니다.
// Streamable HTTP + SSE (prod) 또는 stdio (dev)를 통해 연결합니다. Bearer 토큰 -> 6부.
var client = await McpClientFactory.CreateAsync(
new HttpClientTransport(new()
...
핸드셰이크(handshake)의 클라이언트 측 단계는 _서버(server)_가 _당신(you)_에게 무엇을 요청할 수 있는지 선언하는 곳입니다. sampling을 광고하면 도구가 당신의 모델로 콜백(callback)할 수 있으므로, 당신이 준수할 준비가 된 기능만 광고하십시오.
2. 도구 발견 및 모델을 위한 변환
tools/list를 통해 도구를 발견하고, 이미 JSON Schema인 MCP 스키마를 모델의 도구 호출(tool-calling) 형식으로 직접 변환합니다. 수동 매핑(hand-mapping)은 필요 없습니다.
// MCP 도구를 발견한 후 모델의 도구 형식으로 변환합니다. 세션당 한 번 수행됩니다.
var mcpTools = await client.ListToolsAsync(ct);
...
이것이 MCP가 모든 모델과 결합할 수 있는 조용한 이유입니다. MCP 도구 정의는 곧 JSON Schema이며, 모든 모델의 도구 호출 API는 JSON Schema를 소비합니다. 클라이언트의 역할은 중복되거나 파편화된 레지스트리를 만드는 것이 아니라, 얇고 기계적인 변환을 수행하는 것입니다.
3. 에이전트 루프 (The agent loop)
모델은 최종 답변 또는 일련의 도구 호출 요청을 반환합니다. 클라이언트는 이를 실행하고, 결과를 다시 전달하며, 모델이 요청을 멈출 때까지 이 과정을 반복합니다. 모델이 무한히 회전하지 않도록 턴(turn) 횟수를 제한하십시오.
public async Task<string> RunAsync(string goal, CancellationToken ct)
{
var messages = new List<ChatMessage> { ChatMessage.User(goal) };
...
이 루프가 곧 클라이언트입니다. 모델은 손이 없습니다. 모델은 구조화된 출력(structured output)으로서 도구 호출 요청을 방출할 뿐이며, 실제로 손을 뻗어 적절한 서버에서 실행하고 모델이 추론할 수 있도록 결과를 반환하는 것은 클라이언트입니다. 단계 제한(step cap)을 둔 절제된 루프는 에이전트의 p95 지연 시간이 4.2s → 1.8s로 단축된 이유 중 하나입니다.
4. 여러 서버에 대한 라우팅
**클라이언트 매니저(client manager)**는 모든 연결을 보유하고 각 도구 호출을 해당 도구를 소유한 서버로 라우팅합니다. 이는 시작 시 한 번 구축되는 이름→클라이언트(name→client) 맵입니다. 이름 충돌이 발생하면 서버별로 네임스페이스(namespace)를 지정하십시오.
public sealed class McpClientManager(IReadOnlyList<IMcpClient> clients)
{
// 각 서버에서 발견된 도구로부터 구축된, 도구 이름 -> 해당 도구를 소유한 클라이언트 맵
...
수십 개의 도구를 노출하는 세 개의 서버가 있는 상황에서, 클라이언트는 근본적으로 라우터 (router) 역할을 합니다. 이름→소유자(name→owner) 맵을 잘못 설정하면 create_report 호출이 분석(analytics) 서버로 전달될 수 있습니다. 따라서 네임스페이스(namespace) 충돌을 방지하여 잘못된 곳으로 조용히 라우팅되는 일이 없도록 해야 합니다.
5. 회복 탄력성 (Resilience) — 서버는 나타났다 사라집니다
모든 호출에 타임아웃(timeout)을 설정하고, 전송(transport) 실패 시 지수 백오프 (backoff)를 적용하여 재연결하며, 재시도(retry)를 수행하고, tools/list_changed 발생 시 도구를 재발견(re-discover)해야 합니다.
public async Task<ToolResult> InvokeResilientAsync(ToolCall call, CancellationToken ct)
{
for (var attempt = 0; ; attempt++)
...
서버는 배포되고, 확장되며, 재시작됩니다. 이것이 서버를 독립적으로 만드는 이유입니다. 연결 끊김을 치명적인 오류로 취급하는 클라이언트는 이러한 이점을 스스로 버리는 것입니다. 재연결(Reconnect)과 재시도(retry)를 결합하면, 에이전트 실행 중에 하부에서 롤링 배포(rolling deploy)가 일어나더라도 이를 견뎌낼 수 있으며, 도구 호출 오류율을 **0.8%**로 유지할 수 있습니다.
6. 클라이언트 측 기능: 샘플링 (sampling) 및 루트 (roots)
만약 샘플링 (sampling) 기능을 광고(advertise)한다면, 서버(도구)가 사용자의 모델에게 하위 추론(sub-reasoning)을 수행하도록 콜백(callback)을 보낼 수 있습니다. 이는 매우 강력한 기능이지만, 서버가 사용자의 토큰을 소비할 수 있음을 의미합니다. 따라서 다른 모든 모델 호출과 마찬가지로 동일한 AI 게이트웨이 (AI gateway) (예산 관리, 레드액션 (redaction), 감사 (audit))를 통해 라우팅해야 합니다.
// 우리가 `sampling`을 광고했으므로, 서버가 US에게 completion을 실행하도록 요청할 수 있습니다.
client.OnSamplingRequest = async (request, ct) =>
{
...
샘플링은 화살표의 방향을 뒤집습니다. 즉, 도구가 사용자의 모델에게 생각을 요청할 수 있습니다. 이를 가공되지 않은(raw) 모델 호출에 직접 연결하지 마십시오. 연결된 서버가 사용자의 토큰 예산을 조용히 소진할 수 있습니다. 서버가 시작하는 호출을 포함하여 모든 모델 호출은 통제된 하나의 게이트웨이를 거쳐야 합니다.
수치 요약
| 지표 (Metric) | 맞춤형 에이전트 (이전) | MCP 클라이언트 (이후) |
|---|---|---|
| 통합 (Integrations) | 14개의 맞춤형 어댑터 | 1개의 클라이언트, 3개의 연결 |
| ... |
앞으로 나아갈 모델
앞으로 나아갈 모델
모델이 질문하면, 클라이언트가 행동합니다. MCP 클라이언트는 세 가지 역할을 수행합니다. 즉, 탐색기 (discoverer) (어떤 도구들이 존재하는지 학습함), 루프 (loop) (도구 호출 요청을 결과로 변환하고 다시 전달함), 그리고 라우터 (router) (각 호출을 해당 도구를 소유한 서버로 전송함)입니다. 여기에 더해, 서버가 나타났다 사라지는 상황에서도 견딜 수 있는 회복 탄력성 (resilience)을 갖추어야 합니다.
- 탐색하되, 절대 하드코딩하지 마십시오. 서버는 자신의 도구에 대한 신뢰할 수 있는 유일한 원천 (source of truth)입니다.
- 모든 호출에 제한을 두고, 모든 루프에 상한을 설정하십시오. 타임아웃 (Timeout)은 시스템 멈춤을 방지하며, 상한 (cap) 설정은 무한 루프를 방지합니다.
- 소유자에 따라 라우팅하고, 충돌 시 네임스페이스 (namespace)를 사용하십시오. 많은 서버가 존재할 때, 클라이언트는 무엇보다 라우터로서의 역할이 우선됩니다.
원문은 PrepStack에 게시되었습니다. MCP 클라이언트나 에이전트 루프 (agent loop)를 구축 중이며, 라우팅이나 회복 탄력성에 대해 검토가 필요하시다면 randhir.jassal[at]gmail.com으로 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기