MCP 심층 분석, 파트 3: 처음부터 프로덕션급 MCP 서버 구축하기
요약
데모 수준을 넘어 실제 서비스 운영이 가능한 프로덕션급 MCP(Model Context Protocol) 서버 구축 방법을 다룹니다. .NET 9 환경에서 SDK 활용, 타입 지정 스키마, 페이지네이션 및 에러 핸들링 등 대규모 호출을 견디기 위한 필수 설계 원칙을 설명합니다.
핵심 포인트
- MCP 서버는 단순 스크립트가 아닌 프로덕션 API 수준의 서비스로 설계해야 함
- JSON-RPC 직접 구현 대신 MCP SDK를 사용하여 프로토콜 복잡성 해결
- 모델의 호출 에러를 줄이기 위해 타입이 지정된 도구 스키마와 에러 핸들링 필수
- 대규모 쿼리 대응을 위한 페이지네이션 및 CancellationToken 준수 필요
단 한나절이면 데이터를 반환하는 MCP 서버를 세울 수 있습니다. 하지만 에이전트가 하루에 85,000번 호출하고, 여러 테넌트(tenants)를 가로지르며, 스택 트레이스(stack traces)를 유출하지 않고, 대규모 쿼리 시 OOM(Out of Memory)이 발생하지 않으며, 배포 시 호출을 드롭하지 않는 서버를 세우는 것 — 그것은 데모가 아니라 서비스입니다. 이번 파트에서는 진짜를 구축합니다.
이 글은 Model Context Protocol (MCP)에 대한 15부작 심층 분석 중 파트 3입니다. 우리는 세 가지 Mattrx 서버 중 하나인 우리의 .NET 9 읽기 서버 mattrx-analytics를 엔드 투 엔드(end to end)로 구축합니다.
요약 (TL;DR)
| 고려 사항 | 데모 서버 (이전) | 프로덕션 서버 (이후) |
|---|---|---|
| 부트스트랩 (Bootstrap) | 직접 구현한 JSON-RPC | MCP SDK + DI + 전송 계층 (transport) |
| ... |
mattrx-analytics는 읽기 p95 120ms에서 하루 약 85k개의 도구 호출(tool calls)을 처리합니다.- 타입이 지정된 도구 스키마(Typed tool schemas)와 타입이 지정된 에러(typed errors)는 에이전트의 도구 호출 에러율이 0.8%인 주요 이유입니다.
- 모든 결과에 캡(Cap) 및 페이지네이션(paginate) 적용 (페이지 ≤ 200, 불투명 커서(opaque cursor));
CancellationToken준수.
단 하나의 사고방식 전환: MCP 서버는 스크립트가 아니라 서비스입니다. 프로덕션 API에 요구되는 모든 것 — 스키마(schemas), 타입이 지정된 에러(typed errors), 페이지네이션(pagination), 취소(cancellation), 상태 확인(health), 텔레메트리(telemetry) — 을 MCP 서버도 필요로 합니다. 왜냐하면 에이전트는 인간보다 더 까다롭고 관용이 적은 클라이언트이기 때문입니다.
1. 부트스트랩: 직접 구현한 JSON-RPC가 아닌 SDK를 사용하세요
MCP SDK는 서버를 제공하며, 여러분은 DI(의존성 주입)를 통해 기능을 등록하고 앱의 나머지 부분과 동일한 도메인 서비스(domain services)를 재사용할 수 있습니다.
// Program.cs — mattrx-analytics MCP 서버.
var builder = WebApplication.CreateBuilder(args);
...
SDK가 프로토콜(프레이밍(framing), 초기화(initialize), 도구 목록(tools/list), 스키마 방출(schema emission))을 담당하므로, 여러분은 오직 여러분의 기능(capabilities)에만 집중하면 됩니다. JSON-RPC를 직접 구현하는 것은 이미 해결된 문제를 새로운 버그와 함께 다시 만드는 데 노력을 낭비하는 것입니다.
2. 제대로 된 도구(Tools) — 타입이 지정된 파라미터, 실제 스키마
정확한 이름과 설명이 포함된 타입이 지정된 파라미터(SDK가 이를 모델이 읽을 수 있는 JSON 스키마로 변환함), 그리고 구조화된 타입을 반환하는 방식:
[McpServerToolType]
public sealed class AnalyticsTools(ICampaignQueries campaigns, AiPrincipal principal)
{
...
모델은 자신의 스키마 (schema)로부터 도구를 호출합니다. 기술된 타입이 지정된 시그니처 (signature) 자체가 곧 스키마이며, 모델은 이를 정확하게 채웁니다. string args 형태의 블롭 (blob)은 모델로 하여금 추측하게 만들며, 모든 추측은 호출 실패로 이어집니다.
3. 에러 핸들링 (Error handling) — 도구 에러 vs 프로토콜 에러
도구 에러 (tool error)는 에이전트 (agent)가 읽고 복구할 수 있는 결과 (isError: true)입니다. 프로토콜 에러 (protocol error)는 잘못된 형식의 요청 (malformed request)에 대한 것입니다. 예기치 않은 예외 (exception)는 서버 측에서 로그로 기록되며, 스택 트레이스 (stack trace)가 아닌 안전하고 일반적인 도구 에러로 반환됩니다.
public async Task<CallToolResult> GetCampaignKpis(GetKpisArgs args, CancellationToken ct)
{
if (!Guid.TryParse(args.CampaignId, out var id))
...
만약 에이전트가 읽을 수 있는 not_found 대신 끊긴 연결 (dead connection)을 받게 되면 적응할 수 없으며, 만약 SQL 예외 (SQL exception) 텍스트를 그대로 받게 된다면 스키마 (schema)가 유출된 것입니다.
4. 리소스 (Resources) — 모든 것이 도구인 것은 아니다
읽기 전용 데이터를 URI 템플릿 (URI template)과 콘텐츠 타입 (content type)을 가진 리소스로 노출하십시오. 호스트 (host)는 URI를 통해 이를 연결하고, 모델은 이를 읽습니다.
[McpServerResource(
UriTemplate = "mattrx://analytics/campaigns/{campaignId}",
MimeType = "application/json")]
...
모델이 제어하는 동작은 도구 (tool)이며, 애플리케이션이 제어하는 데이터는 리소스 (resource)입니다. "사용자가 보고 있는 레코드"를 도구 호출 대신 리소스로 제공하는 것은 컨텍스트 토큰 (context tokens)을 14k에서 3.5k로 낮게 유지하는 방법 중 하나입니다.
5. 페이지네이션 (Pagination), 제한 (caps), 그리고 취소 (cancellation)
페이지 크기를 제한하고, 불투명한 커서 (opaque cursor)를 반환하며 (거대한 테이블에서 절대 OFFSET을 사용하지 마십시오), 취소 (cancellation) 요청을 준수하십시오:
[McpServerTool(Name = "query_events")]
[Description("Query a campaign's events, newest first. Returns one page; pass `cursor` to continue.")]
public async Task<EventPage> QueryEvents(
...
제한 없는 도구 결과는 서버 메모리 문제와 모델 컨텍스트의 비용 증가 및 혼란을 초래하는 이중의 위험 요소 (double foot-gun)입니다. 커서를 사용하여 페이지를 200개로 제한하면, 1억 8천만 행의 Events 테이블에 대해서도 query_events의 읽기 p95를 120ms로 유지할 수 있습니다.
6. 상태 확인 (Health), 준비 상태 (readiness), 그리고 텔레메트리 (telemetry)
/healthz (liveness) 및 /readyz (의존성에 따라 제어되는 readiness)를 노출하고, 도구 호출(tool call)당 하나의 OpenTelemetry span을 생성하세요. Readiness를 통해 Azure Container Apps는 트래픽 중단 없이 롤링 배포(roll deploys)를 수행할 수 있습니다.
using var activity = ActivitySource.StartActivity("mcp.tool_call");
activity?.SetTag("mcp.tool", toolName);
activity?.SetTag("mattrx.tenant", principal.TenantId);
...
에이전트는 에러가 발생하고 있다고 당신에게 말해주지 않습니다. 그저 조용히 성능이 저하될 뿐입니다. 호출별 span을 사용하면 "어시스턴트의 반응이 이상하다"라는 모호한 느낌을 도구별, 테넌트(tenant)별 p95 및 에러율 차트로 변환할 수 있습니다.
전체 빌드가 과할 때 (Overkill)
- 로컬 stdio 개발 도구. stdio를 통한 몇 개의 도구는 HTTP/readiness/scaling 메커니즘이 전혀 필요하지 않습니다.
- 모든 것을 도구로 만들기. 단순히 레코드를 가져오는 읽기 작업은 리소스(resources)입니다.
- 결과 제한(result caps) 생략. 프로덕션 MCP에서 가장 흔히 발생하는 실수입니다. 처음부터 제한(cap)을 두고 페이지네이션(paginate)을 적용하세요.
- 에러 발생 시 내부 정보 유출. 서버 측에 로그를 남기고, 클라이언트에는 안전한 문자열만 반환하세요.
- 프로토콜 직접 구현하기. SDK가 프레이밍(framing), 협상(negotiation), 스키마 생성(schema generation)을 처리합니다.
- 인메모리 세션 상태 (In-memory session state). 서버를 상태가 없는(stateless) 상태로 유지하고, 세션 관리는 전송 계층(transport)이나 게이트웨이(gateway)가 담당하게 하세요.
계속 유지해야 할 모델
MCP 서버는 스크립트가 아니라 서비스입니다. 에이전트는 끊임없이, 문자 그대로, 가차 없는 클라이언트입니다. 에이전트는 당신의 스키마에 따라 호출하고, 당신의 에러에 대해 재시도하며, 기꺼이 10억 개의 행을 요청할 것입니다.
- 도구에 타입을 지정하세요. 설명되고 타입이 지정된 파라미터는 모델이 의존하는 스키마입니다.
- 에러를 예외(exception)가 아닌 데이터로 만드세요. 에이전트가 추론할 수 있는 도구 에러를 반환하세요. 상세 내용은 로그로 남기되, 절대 유출하지 마세요.
- 모든 것에 제한과 페이지네이션을 적용하세요. 모든 도구가 10억 개의 행과 일치할 수 있다고 가정하고 처리하세요.
원문은 PrepStack에 게시되었습니다. MCP 서버를 구축 중이며 도구 계약(tool contracts)이나 에러 핸들링에 대해 검토가 필요하신가요? randhir.jassal[at]gmail.com으로 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기