내가 다섯 번째 MCP 서버가 아닌 컴파일러를 만든 이유
요약
다양한 AI 에이전트와 프로토콜 변화에 대응하기 위해 MCP 서버를 직접 작성하는 대신, 중간 표현(IR)을 활용한 컴파일러 방식의 접근법을 제안합니다. 비즈니스 로직을 한 번만 정의하면 다양한 SDK와 프로토콜로 자동 생성할 수 있는 구조를 설명합니다.
핵심 포인트
- MCP 서버를 매번 수동으로 작성하는 것은 유지보수 측면에서 비효율적임
- 중간 표현(IR)을 도입하여 프로토콜 변경에 유연하게 대응 가능
- 비즈니스 정의를 한 번만 수행하면 다양한 타겟(SDK, REST 등)으로 방출 가능
- MCP는 도구일 뿐이며, 핵심은 타겟 불가지론적인 IR 설계에 있음
첫 번째 MCP 서버를 작성하는 것은 어렵지 않습니다. 몇 백 줄 정도의 코드면 충분하며, 점심 식사 전에는 실행할 수 있습니다. 저도 직접 작성해 보았기에 잘 알고 있습니다.
문제는 다섯 번째 서버입니다.
작업의 형태
기업이 자사의 기능을 AI 에이전트(AI agents)가 접근할 수 있도록 결정할 때 실제로 일어나는 일은 다음과 같습니다.
당신은 MCP 서버를 작성합니다. 잘 작동합니다. 그러다 ChatGPT가 동일한 기능을 약간 다른 형태로 요구합니다. 그다음은 Gemini입니다. 그다음은 다음 분기에 출시될 무언가입니다. 왜냐하면 다음 분기에는 항상 무언가가 출시되기 때문입니다. 각각은 고유의 SDK, 도구 정의(tool definition)에 대한 각기 다른 개념, 그리고 각기 다른 출시 주기(release cadence)를 가진 별개의 통합 프로젝트가 됩니다.
그리고 이 모든 것의 밑단에서는 당신의 API 자체가 계속 변합니다. 필드 이름이 바뀌거나, 응답이 nullable(null 허용)이 되거나, 엔드포인트(endpoint)가 이동합니다. 이제 당신에게는 각각 독립적으로 잘못된 네 개의 수동 작성 통합 코드가 남게 되며, 이를 알려줄 빌드 단계(build step)도 없습니다.
첫 번째 서버는 오후 한때면 충분하지만, 다섯 번째 서버는 팀 단위의 작업입니다.
그래서 제가 고민했던 질문은 *'어떻게 MCP 서버를 작성할 것인가'*가 아니었습니다. 그것은 바로: **프로토콜이 변경될 때 살아남는 산출물(artifact)은 무엇인가?**였습니다.
하나의 정의, 다수의 방출기(emitters)
그 질문에는 지루하고 잘 알려진 답이 있으며, 이는 1970년대부터 컴파일러(compilers)가 제시해 온 것과 동일한 답입니다. 바로 중간 표현(intermediate representation, IR)을 중간에 두는 것입니다.
capabilities.yaml → semantic model → IR → emitter → MCP tools
↘ emitter → SDK / REST / GraphQL / …
당신은 비즈니스 용어로 기능을 한 번만 기술합니다. 즉, 그것이 무엇을 하는지, 무엇이 필요한지, 무엇을 반환하는지, 그리고 호출 시 재시도(retry)가 안전한지 등을 정의합니다. HTTP도, JSON Schema도, SDK도 없습니다. 이는 타겟 불가지론적(target-agnostic)인 IR로 낮아지며, 방출기(emitters)가 이 IR을 소비합니다.
데모에 나오는 tourism.search 기능 전체는 단 12줄입니다:
capability:
id: tourism.search
description: Find places to stay matching a traveler's intent.
...
프로토콜을 변경하면 재생성(regenerate)하면 됩니다. 백엔드를 변경해도 정의(definition)는 전혀 움직이지 않습니다. 오직 바인딩(binding)만 변경될 뿐이며, 이는 비즈니스 설명(business description)에 절대 나타나지 않는 별도의 파일입니다.
IR이 제품이지, MCP가 제품이 아니라는 증거
이 부분은 만약 다른 누군가가 이 주장을 펼친다면 제가 꼭 확인하고 싶었을 내용이며, 여기 그 내용이 있습니다.
실제 운영 환경에서 Archstone에 가장 많이 의존하는 시스템은 MCP를 전혀 사용하지 않습니다.
ArtVinci는 커스텀 프레임 제작 워크숍입니다. 이 사이트는 고객의 질문에 답하는 어시스턴트를 실행하며, 실제 카탈로그와 자체 백엔드에 의해 실시간으로 계산된 실제 가격을 제공합니다. 이 시스템은 MCP 서버 프로세스를 실행하지 않습니다. 대신 컴파일된 IR을 직접 로드합니다:
const result = await archstone.execute("framing.estimate-frame-price", input);
MCP 경로와 동일한 IR, 동일한 검증(validation), 동일한 fail-closed 응답 매핑(response mapping)을 사용하지만, 배포해야 할 별도의 프로세스가 없을 뿐입니다. MCP는 하나의 에미터(emitter)일 뿐입니다. 만약 MCP가 제품이었다면, 그러한 배포 방식은 불가능했을 것입니다.
실제로 어려운 부분
도구 정의(tool definition)를 생성하는 것은 쉬운 절반에 불과합니다. 에이전트가 고객에게 잘못된 답변을 줄지 여부를 결정하는 나머지 절반은 응답 매핑(response mapping)입니다.
바인딩(binding)은 선언된 리소스(resource)의 각 필드가 제공자(provider)의 페이로드(payload) 중 어디에서 오는지, 그리고 그중 어떤 것이 필수(required)인지를 명시합니다. 필수 필드가 누락되면 호출은 fail-closed 됩니다. 즉, 백엔드가 무엇을 반환하든 상관없이 조용히 통과시키는 것이 아니라 구조화된 에러(structured error)를 발생시킵니다. 반면 선택적(optional) 필드가 누락되면 성능 저하(degrade)가 발생합니다. DEGRADED와 VIOLATION이라는 이 두 단어는, 에이전트가 "그 정보가 없습니다"라고 말하는 것과 에이전트가 가격을 지어내는 것 사이의 차이를 만듭니다.
그리고 제공자는 결국 당신에게 알리지 않고 페이로드를 변경할 것이기 때문에, 각 바인딩은 기록된 픽스처(fixture)와 지문(fingerprint)을 보유합니다. archstone verify는 요청 시점에 기록된 요청을 라이브 백엔드에 대해 재현(replay)하고 바인딩당 상태(health status)를 보고합니다. 따라서 계약 드리프트(contract drift)는 고객의 불만 사항이 아닌, CI에서의 빨간 선(red line)으로 나타나게 됩니다.
"하지만 그냥 Claude에 내 OpenAPI 스펙을 붙여넣으면 되잖아요"
맞습니다. 그렇게 할 수 있고, 90초면 충분하며, 비용도 들지 않습니다. 이것은 제가 온보딩(onboarding) 명령어를 만드는 동안 사람들이 제기한 가장 날카로운 반대 의견이었으며, 그 자체로는 타당한 지적이라고 생각합니다.
그래서 archstone init은 의도적으로 코드 생성기(code generator)가 아닙니다. 이것은 하나의 루프(loop)입니다:
- OpenAPI 문서를 읽고 후보군을 제안합니다.
- 문서가 답할 수 없는 질문들, 즉 호출이
read(읽기),write(쓰기), 또는irreversible(되돌릴 수 없음)인지 묻습니다. 이는 "가격을 조회하는 것"과 "카드로 결제하는 것"의 차이와 같습니다. - **실제 컴파일러(the real compiler)**를 실행합니다. 초안으로 작성한 매니페스트(manifest)가 컴파일되지 않으면, 아무것도 작성하지 않습니다. "대체로 작동하니 에러는 직접 수정하세요"와 같은 모드는 없습니다.
- 선택적으로, **사용자의 실제 백엔드(your real backend)**를 단 한 번, 읽기 전용으로 호출하여 실제 피스처(fixture)를 기록합니다. 그런 다음 배포된 매퍼(mapper)를 통해 이를 다시 재생(replay)하여, 자체 매핑이 실제로 유효한지 확인합니다.
언어 모델(language model)은 그럴듯한 YAML을 작성할 수 있습니다. 하지만 사용자의 컴파일러를 실행하거나 API를 호출할 수는 없습니다. 이것이 결정적인 차이이며, 제가 더 멋진 필드 이름(field names)을 만드는 대신 모든 노력을 이 루프를 구축하는 데 쏟은 이유입니다.
틀린 사례를 보여주는 것이 더 유용하기에
제가 처음으로 init을 실제 스펙에 적용했을 때, 이 도구는 출력값 전체가 권고 경고(advisory warnings) 목록으로만 구성된 가격 추정 기능을 만들어냈습니다. 가격은 없었습니다.
규칙은 설계된 대로 정확하게 작동하고 있었습니다: 응답에서 객체 배열(array of objects)을 찾아 이를 리소스(resource)로 만드는 것이었습니다. 해당 응답에서 유일한 객체 배열은 warnings였습니다. 페이로드(payload) — 즉 가격, 통화, 기하학적 정보(geometry) — 는 그 옆의 스칼라 필드(scalar fields)에 있었고 그대로 누락되었습니다.
컴파일도 되었고, 검증(validation)도 통과했으며, 파일도 작성되었습니다. 이 도구를 호출하는 에이전트(agent)는 경고 목록만 받고 가격은 받지 못했을 것이며, 그 어디에서도 이를 감지할 수 있는 신호는 없었을 것입니다.
해결책은 더 영리한 휴리스틱 (heuristic)이 아니었습니다. "배열이 페이로드(payload)이고 스칼라(scalars)가 페이지네이션(pagination)인 경우"와 "배열이 진단(diagnostics)이고 스칼라가 페이로드인 경우"를 구분하는 것은 문서를 통해서는 불가능합니다. 두 형태는 구조적으로 동일하기 때문입니다. 따라서 이제 init은 후보군을 열거하고, 필드 이름이 표시된 상태에서 단 한 번 질문을 던집니다. 후보는 단 하나뿐이므로 의문의 여지가 없습니다.
이것이 일반적인 태도이며, 이러한 도구가 정직함을 유지할 수 있게 하는 유일한 방법입니다:
모호함은 질문이나 거절이어야 하며, 결코 추측이어서는 안 됩니다.
비용 (What it costs you)
당신은 비즈니스를 설명하는 YAML을 작성합니다. 그것이 거래의 대가입니다. 만약 당신의 기능(capabilities)이 진정으로 단일 엔드포인트(endpoint)뿐이고 두 번째 프로토콜을 기대할 일이 전혀 없다면, MCP 서버를 직접 작성하는 것이 올바른 선택이며 그렇게 해야 합니다.
컴파일러는 기능이 여러 개이고, 소비자가 한 명 이상이며, 백엔드(backend)가 변경되는 시점, 즉 직접 작성한 버전이 더 이상 오후 한나절 만에 끝날 수 없는 시점에서 제값을 합니다.
Archstone는 Apache-2.0 라이선스입니다: github.com/Archstone-Romania/archstone. 이 컴파일러로 컴파일된 기능은 현재 작동 중입니다 — Claude의 커스텀 커넥터(custom connectors)에 https://demo.archstone.dev/mcp를 붙여넣고 여행에 대해 물어보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기