MCP 심층 분석, 파트 2: Model Context Protocol 아키텍처 내부 탐구 (Host, Client, Server)
요약
Model Context Protocol(MCP)의 아키텍처를 Host, Client, Server의 세 가지 역할과 도구, 리소스, 프롬프트라는 세 가지 기본 요소로 심층 분석합니다. MCP를 단순한 도구 호출 방식이 아닌 분산 시스템으로 이해함으로써 인증, 확장성, 버전 관리 문제를 해결하는 방법을 제시합니다.
핵심 포인트
- MCP는 Host, Client, Server로 역할을 분리하여 시스템 복잡도를 낮춤
- 도구(Tools), 리소스(Resources), 프롬프트(Prompts)가 핵심 구성 요소
- Capability negotiation을 통해 서버 버전 업데이트 시 호환성 유지 가능
- stdio 및 HTTP/SSE 전송 방식을 지원하여 로컬 및 원격 환경 대응
- N×M 통합 방식을 N+M 방식으로 전환하여 통합 효율성 극대화
대부분의 팀은 MCP를 "모델에게 도구를 제공하는 방법"으로만 접하고 거기서 멈춥니다. 그러한 프레임워크는 손해를 불러올 것입니다. **Model Context Protocol (MCP)**는 세 가지 역할과 세 가지 기본 요소(primitives)를 가진 작은 분산 시스템이며, 일단 아키텍처를 명확히 이해하고 나면 인증(auth), 확장성(scale), 버전 관리(versioning), 스트리밍(streaming)과 같은 이후의 모든 질문에 대한 명확한 해답을 찾을 수 있습니다. 이번 파트에서는 그 지도를 그려봅니다.
이 글은 15부작 심층 분석 중 파트 2입니다. 파트 1에서는 MCP 도입의 필요성을 설명했습니다. MCP는 N×M 방식의 통합 접착제(integration glue)를 N+M 방식으로 전환합니다 (저희의 .NET/Azure SaaS인 Mattrx의 경우, 14개의 맞춤형 클라이언트가 3개의 MCP 서버로 축소되었습니다). 이제 그 내부를 열어보겠습니다.
요약 (TL;DR)
| 개념 | 임시 에이전트 (이전) | MCP 아키텍처 (이후) |
|---|---|---|
| 역할 | 하나의 프로세스가 모든 것을 수행 | Host, Client, Server — 분리됨 |
| ... |
- 세 가지 역할: Host는 에이전트 루프와 그 클라이언트들을 소유하고, Client는 정확히 하나의 Server 연결을 소유하며, Server는 기능(capabilities)을 소유합니다.
- 세 가지 기본 요소 (primitives): 도구(tools, 모델이 제어하는 동작), 리소스(resources, URI에 의해 앱이 제어하는 데이터), 프롬프트(prompts, 사용자가 제어하는 템플릿).
initialize단계에서의 기능 협상(Capability negotiation)을 통해 서버는 클라이언트를 깨뜨리지 않고도 새로운 버전을 출시할 수 있습니다.- 두 가지 전송 방식(transports): 로컬/동일 위치용 stdio, 원격/멀티 테넌트용 Streamable HTTP + SSE.
- 읽기 모델링을 통해 컨텍스트 토큰을 14k에서 3.5k로 유지하며 리소스로 처리합니다. 핸드셰이크(handshake)를 통해 도구 호출(tool-call) 오류율을 0.8%로 유지합니다.
단 하나의 사고 전환: "MCP = 도구 호출(tool calling)"이라고 생각하는 것을 멈추십시오. "MCP = 세 가지 역할과 세 가지 기본 요소"라고 생각하십시오. 역할을 올바르게 정의하면 어려운 부분들(경계에서의 인증, 서버 확장, 계약 버전 관리)은 더 이상 아키텍처 문제가 아닌 설정(configuration)의 문제가 됩니다.
1. 세 가지 역할: Host, Client, Server
이전에는 첫 번째 에이전트가 Host, Client, Server가 하나로 융합된 '갓 오브젝트(god object)'였습니다:
// 이전: 오케스트레이션(orchestration), 도구 로직, 데이터 액세스가 융합됨.
public sealed class InsightsAgent(IChatModel model, AppDbContext db, IReportService reports)
{
...
그 후, MCP는 세 가지 역할을 정의하고 이를 분리하여 유지합니다. 호스트(Host)는 루프(loop)를 소유하며, 서버당 하나의 클라이언트(Client)를 가집니다.
// AFTER: 호스트는 루프와 서버당 하나의 클라이언트를 소유합니다. 그것이 호스트가 소유하는 전부입니다.
public sealed class InsightsHost(IReadOnlyList<IMcpClient> clients, IChatModel model)
{
...
서버(Server)는 이와 대칭되는 구조입니다. 서버는 기능(capabilities)을 선언하며, 자신을 호출하는 대상이 누구인지 알지 못합니다.
// AFTER: 서버는 기능을 소유합니다. 에이전트도, 모델도 없으며, 누가 호출하는지도 알지 못합니다.
builder.Services
.AddMcpServer(o => o.ServerInfo = new() { Name = "mattrx-analytics", Version = "2.4.0" })
...
호스트↔서버 경계는 인증(auth), 속도 제한(rate-limiting), 그리고 감사(audit)가 위치해야 할 정확한 지점이며, 이제 명확한 경계가 존재합니다.
2. 세 가지 기본 요소: 도구(Tools), 리소스(Resources), 프롬프트(Prompts)
MCP는 서버에 세 가지 기본 요소(primitives)를 제공하며, 각 요소는 서로 다른 **제어 주체(controller)**를 가집니다.
- 도구 (Tools) — 모델 제어 (model-controlled). 모델이 언제 도구를 호출할지 결정합니다.
- 리소스 (Resources) — 애플리케이션 제어 (application-controlled). 호스트가 URI로 주소 지정된 관련 데이터를 컨텍스트(context)에 부착합니다. 모델은 이를 읽을 뿐, "호출"하지 않습니다.
- 프롬프트 (Prompts) — 사용자 제어 (user-controlled). 사용자가(또는 UI가) 의도적으로 선택하는 재사용 가능한 템플릿입니다.
// 리소스: 호스트가 컨텍스트에 부착하는 URI 주소 지정 가능 데이터 — 도구 호출이 아님.
[McpServerResource(UriTemplate = "mattrx://analytics/campaigns/{campaignId}")]
[Description("호출자의 테넌트를 위한 캠페인 레코드.")]
...
핵심적인 이점은 구문(syntax)이 아니라 제어 모델(control model)에 있습니다. 리소스를 사용하면 호스트가 사용자가 보고 있는 바로 그 캠페인 레코드를 — URI를 통해 — 부착할 수 있습니다. 이는 모델이 어떤 도구를 호출할지 추측하고 필요한 것보다 더 많은 데이터를 가져오는 대신 이루어집니다. 이러한 방식은 컨텍스트 토큰(context tokens)을 14k에서 3.5k로 유지하는 방법의 일부입니다.
3. 기능 협상: initialize 핸드셰이크 (handshake)
모든 MCP 세션은 initialize로 시작되며, 여기서 양측은 프로토콜 버전과 각자의 기능(capabilities)을 교환합니다.
// initialize 결과 (server -> client)
{
"jsonrpc": "2.0", "id": 1,
...
// 적용 후: 협상된 기능(capabilities)에 적응 — 절대 하드코딩하지 마세요.
var session = await client.InitializeAsync(ct);
if (session.Server.Supports(ServerCapability.Resources))
...
핸드셰이크(handshake)는 규모는 작지만 핵심적인 지지벽 역할을 합니다. 이를 통해 mattrx-analytics는 새로운 도구가 포함된 v2.5 버전을 출시하면서도, v3.0 클라이언트와 v3.1 클라이언트가 모두 계속 작동할 수 있게 합니다. 이 핸드셰이크 이면에 존재하는 독립적인 버전 관리(Independent versioning)는 도구 호출(tool-call) 오류율이 0.8%로 떨어진 주요 원인입니다.
4. 전송 계층 (Transports): stdio vs Streamable HTTP + SSE
전송 계층(Transport)은 서버의 속성이 아니라 **배포(deployment)**의 속성입니다. 동일한 서버 코드가 두 가지 방식으로 실행됩니다:
- stdio — 서버가 호스트(host)의 자식 프로세스(child process)로 실행됩니다. 네트워크가 필요 없고, 인증이 필요 없으며, 지연 시간(latency)이 가장 낮습니다. 로컬 개발 및 동일 위치(co-located)의 도구에 적합합니다.
- Streamable HTTP + SSE — 스트리밍을 위해 Server-Sent Events (SSE)로 업그레이드되는 단일 HTTP 엔드포인트입니다. 네트워크나 신뢰 경계(trust boundary)를 넘나드는 모든 경우에 사용하며, TLS, OAuth 및 수평적 확장(horizontal scale)을 지원합니다.
// 로컬 개발: stdio 자식 프로세스, 인증 없음.
builder.Services.AddMcpServer().WithStdioTransport().WithTools<AnalyticsTools>();
...
신뢰 경계에 따라 전송 계층을 선택하세요. 하나의 프로세스 내부에서는 stdio가 더 간단하고 빠르며 인증이 필요하지 않습니다. 네트워크를 가로지르는 경우 — 테넌트(tenant) 간, 또는 귀하의 서비스와 파트너의 어시스턴트 간 — TLS와 OAuth가 포함된 HTTP가 필요합니다. 동일한 AnalyticsTools를 사용하지만, 연결 방식(wiring)만 다릅니다. (운영 환경에서 당사의 3개 서버는 Azure Container Apps로 실행됩니다: p95 읽기 120ms, p95 보고서 대기열 삽입(report-enqueue) 90ms, p95 첫 번째 토큰 스트리밍 ~300ms.)
전체 아키텍처가 과할 때 (When the full architecture is overkill)
- 단일 공동 배치 도구 (A single co-located tool). stdio와 도구 (tools)만 사용하세요. 하나의 도구만 사용하는 개발 보조 도구에게 리소스 (resources), 프롬프트 (prompts), 협상 (negotiation)은 불필요한 절차일 뿐입니다.
- 어차피 항상 필요한 데이터. 호스트 (host)가 매번 동일한 작은 레코드를 첨부한다면, URI 스킴 (URI scheme)을 사용하는 리소스 (resource)보다 도구 (tool)를 사용하거나 인라이닝 (inlining)하는 것이 더 나을 수 있습니다.
- 준수하지 않을 구독 (Subscriptions) / listChanged. 어떤 클라이언트 (client)도 반응하지 않을 기능 (capabilities)을 광고하지 마세요. 사용되지 않는 기능은 핸드셰이크 (handshake) 과정에서의 거짓말과 같습니다.
- 서버의 과도한 분할. 도메인별로 3개의 서버를 두는 것은 적절하지만, 14개의 마이크로 서버 (micro-servers)를 두는 것은 추가적인 배포 오버헤드와 함께 N×M의 혼란을 재현할 뿐입니다.
- 신뢰 경계 (trust boundary)를 넘나드는 stdio. 프로덕션 환경에서 이는 보통 격리했어야 할 도구를 공동 배치했다는 것을 의미합니다.
계속 가져가야 할 모델
호스트 (Host)는 루프 (loop)를 소유합니다. 클라이언트 (Client)는 연결 (connection)을 소유합니다. 서버 (Server)는 기능 (capability)을 소유합니다. 세 가지 역할, 세 가지 프리미티브 (primitives: tool, resource, prompt), 하나의 핸드셰이크 (handshake). 이 문장이 아키텍처의 전부이며, 우리가 마주한 거의 모든 MCP 버그는 이를 위반한 결과였습니다.
- 코드를 작성하기 전에 호스트/클라이언트/서버의 경계를 그리세요. 대부분의 MCP 혼란은 역할의 혼동에서 비롯됩니다.
- 제어 모델 (control model)에 따라 프리미티브를 선택하세요. "누가 이것을 호출하기로 결정하는가 — 모델 (model), 앱 (app), 또는 사용자 (user)?"라는 질문이 프리미티브를 결정합니다.
- 신뢰 경계 (trust boundary)에 따라 전송 방식 (transport)을 선택하세요. 프로세스 내부에서는 stdio, 네트워크를 가로지르는 경우에는 HTTP+SSE를 사용하고, 인증 (auth)은 정확히 네트워크가 시작되는 지점에 배치하세요.
원문은 PrepStack에 게시되었습니다. 본인만의 호스트/클라이언트/서버 경계를 설정하고 검토가 필요하신가요? randhir.jassal[at]gmail.com으로 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기