의존성을 고정하는 방식처럼 MCP 서버 계약(Contracts)을 고정하세요
요약
MCP(Model Context Protocol) 서버의 변경 사항이 에이전트의 동작을 예기치 않게 망가뜨리는 문제를 해결하기 위해, 의존성 고정 방식과 유사한 '계약(Contracts) 고정'의 필요성을 설명합니다. 이를 위해 도구의 설명, 스키마, 파라미터 변경을 감지하고 CI 단계에서 검증할 수 있는 mcpward 도구를 소개합니다.
핵심 포인트
- MCP 서버의 설명(description) 변경은 스키마 변경 없이도 에이전트의 동작을 변화시킴
- 필수 파라미터 추가나 readOnlyHint 변경은 운영 환경에서 심각한 오류를 유발할 수 있음
- mcpward는 도구의 스냅샷을 저장하고 변경 사항(drift)을 감지하는 CI 게이트 역할을 수행함
- npx mcpward 명령어를 통해 baseline 저장 및 diff 검증이 가능함
당신은 npm 의존성(dependencies)을 고정합니다. 락파일(lockfile)을 가지고 있죠. 변경 사항이 생기면 diff를 검토합니다.
이제 당신의 에이전트(agent)가 의존하는 MCP 서버들을 생각해 보세요. 무엇이 그것들을 고정할까요?
tools/list는 이름, 설명, 그리고 JSON 스키마(JSON schemas)를 반환하며, 당신의 에이전트는 이 모든 것을 신뢰합니다. 설명(description)은 문서(documentation)가 아닙니다. 그것은 모델이 도구(tool)가 무엇을 하는지, 언제 호출해야 하는지를 결정하기 위해 읽는 지침(instruction)입니다. 버전 고정(version pin)도, 무결성 검사(integrity check)도, 검토할 diff도 없습니다. 서버가 변경되면, 당신의 에이전트의 동작도 함께 변경됩니다.
MCP 의존성이 당신을 조용히 망가뜨리는 네 가지 방법
1. 설명(description)이 다시 작성됨. 도구 이름과 스키마는 동일하지만 텍스트가 달라집니다. 이제 모델은 다르게 동작하지만 아무런 변경 사항도 감지되지 않습니다. 이것이 가장 중요한 문제인데, 스키마 변경이 전혀 필요하지 않기 때문입니다. 바로 이 점 때문에 스키마 전용 diff 검토로는 이를 놓치게 됩니다.
2. 필수 파라미터(required parameter)가 나타남. 기존의 호출들이 잘못된 파라미터로 인해 실패하기 시작하며, 당신은 이를 운영 트래픽(production traffic)을 통해 알게 됩니다.
3. readOnlyHint가 true에서 false로 바뀜. 자유롭게 호출해도 안전하다고 화이트리스트(allow-listed)에 등록했던 도구가 이제 상태를 변경(mutate)할 수 있게 됩니다.
4. 도구가 사라짐. 최선의 경우는 깔끔한 에러 발생입니다. 최악의 경우는 당신의 에이전트가 다른 무언가로 임기응변을 하는 것입니다.
기존 사례 (Prior art)
Invariant Labs가 이 분야의 기초적인 작업을 수행했습니다. 그들은 도구 포이즈닝(tool poisoning)과 MCP 러그 풀(rug pulls)이라는 용어를 명명했으며, 그들의 mcp-scan은 2025년 4월부터 도구 해싱(tool hashing)을 통해 설명 변경을 감지해 왔습니다. 만약 자신의 컴퓨터에 설치된 MCP 서버를 감사(audit)하고 싶다면 그것이 바로 필요한 도구입니다. 해당 도구는 Claude, Cursor, Windsurf 설정을 스캔하고, 교차 출처 권한 상승(cross-origin escalation, tool shadowing)을 감지하며, 실시간 가드레일(guardrails)이 포함된 프록시 모드(proxy mode)를 제공합니다. 제가 하는 작업은 이 중 어느 것도 아닙니다.
저는 이와 인접한 무언가, 즉 **CI 게이트(CI gate)**가 필요했습니다. "내 노트북이 안전한가"가 아니라, "이 의존성의 계약(contract)이 지난 릴리스 이후로 변경되었는가"를 확인하는 것이 필요했습니다. 그리고 도구 설명을 누구의 API로도 전송하지 않고 내부 서버를 대상으로 실행할 수 있어야 했습니다.
그래서 저는 mcpward를 만들었습니다.
두 가지 명령어
npx mcpward baseline # 서버의 계약(contract)을 lockfile에 스냅샷으로 저장
npx mcpward diff # CI에서: 변경 사항(drift)이 발생하면 빌드 실패 처리
baseline은 모든 도구(tool)의 이름, 설명 해시(description hash), 입력 및 출력 스키마(schema), 그리고 어노테이션(annotations)을 캡처합니다. 그 후, 서버가 업데이트를 배포하면 다음과 같은 결과가 나타납니다.
DRIFT (5 failed)
✗ Tool "echo" description changed (possible rug-pull)
✗ Tool "compute" inputSchema added required property "multiplier"
...
종료 코드(Exit code)는 1이며, 빌드는 실패합니다. 네 가지의 계약(contract) 변경 사항이 발생했는데, 이 중 어느 것도 무언가 고장 나기 전까지는 런타임(runtime)에서 드러나지 않았을 것입니다.
파괴적 변경(Breaking) vs 비파괴적 변경(Non-breaking)
모든 변경 사항이 빌드를 실패시켜야 하는 것은 아닙니다. 선택적 파라미터(optional parameter)를 추가하는 것은 괜찮습니다. 하지만 필수 파라미터(required parameter)를 추가하는 것은 허용되지 않습니다. 분류는 다음과 같습니다:
| 변경 사항 | 클래스 | 기본적으로 실패 처리 |
|---|---|---|
| 도구 삭제 | 파괴적 (breaking) | 예 |
| ... |
이 분류 로직을 정확하게 만드는 것이 이 도구 전체에서 가장 어려운 부분입니다. 이는 철저한 fixture 기반 테스트 스위트가 뒷받침된 순수 함수(pure function)이며, 제가 가장 오류가 있었다는 피드백을 듣고 싶은 부분이기도 합니다.
그 외에 확인하는 사항들
이 도구는 이미 실제 클라이언트로서 프로토콜(protocol)을 사용하고 있기 때문에 다음 사항들을 확인할 수 있습니다:
프로토콜 준수 (Protocol compliance) — 핸드셰이크(handshake), 버전 협상(version negotiation), 기능 일관성(capability consistency), JSON-RPC 정확성.
이중 계층 에러 계약 (The two-layer error contract). 이 부분은 과소평가되어 있습니다. MCP는 프로토콜 에러 (protocol errors) (JSON-RPC error 객체)와 도구 에러 (tool errors) (isError: true를 포함하는 성공적인 결과)를 구분합니다. 파일 없음, 업스트림(upstream) 500 에러와 같이 작업을 수행하는 데 실패한 도구는 첫 번째가 아닌 두 번째 방식을 반환해야 합니다. 서버들은 이를 거꾸로 처리하는 경우가 빈번하며, 이는 모든 클라이언트가 실패를 처리하는 방식을 변화시킵니다. 다른 어떤 도구도 이를 확인하지 않습니다.
도구 오염 휴리스틱 (Tool-poisoning heuristics) — 설명 내의 인젝션(injection) 유사 문구, 숨겨진 유니코드 및 제로 너비(zero-width) 유니코드, API 키나 비밀번호를 요구하는 스키마, 명백히 파괴적인 도구와 모순되는 readOnlyHint. 출력은 SARIF 형식이므로, 탐지된 결과는 GitHub Security 탭에 표시됩니다.
Behavioral suites (행동 스위트) — JSONPath 단언(assertions)을 포함하는 선언적 YAML 케이스:
suites:
- tool: read_file
cases:
...
Latency budgets (지연 시간 예산) — 구성 가능한 임계값(threshold)에 대비한 도구별 p50/p95.
테스트 도구를 신뢰하는 것에 대하여
아무도 신뢰할 수 없는 테스트 하네스(test harness)는 없는 것보다 못하므로, 테스트 접근 방식은 의도적으로 편집증적(paranoid)입니다.
모든 검사는 정답(correctness)이 사전에 정의된 **제어된 픽스처 서버(controlled fixture servers)**를 대상으로 개발됩니다. 즉, 완전히 규격에 맞는 서버, 의도적으로 잘못 구성된 서버, 각 분류 클래스별로 정확히 하나의 차이만 있는 한 쌍의 서버, 느린 서버, 그리고 오염된(poisoned) 서버를 사용합니다. 실제 제3자 서버는 정답(ground truth) 역할을 할 수 없습니다. 왜냐하면 그 서버들이 올바른지 여부를 제어할 수 없기 때문입니다.
또한 모든 검사에는 실패(red)할 수 있음을 증명하는 **부정 테스트(negative test)**가 포함됩니다. 항상 통과하기만 하는 검사는 기능이 아니라 잘못된 확신을 생성하는 도구일 뿐입니다. 깨끗한 픽스처(clean fixture) 역시 모든 릴리스 과정에서 100% 깨끗하게 유지되어야 합니다. 양치기 소년처럼 잘못된 보안 경고를 보내는 검사는 사람들이 이를 무시하도록 훈련시키며, 이는 검사를 출시하지 않는 것보다 더 나쁩니다.
실행해 보기
npx mcpward init
npx mcpward run
블랙박스(Black-box) 방식이므로, 직접 작성하지 않은 서버라도 stdio 또는 Streamable HTTP를 통해 어떤 구현 언어로든 작동합니다. MIT 라이선스입니다. 계정, API 호출, 텔레메트리(telemetry) 없이 완전히 사용자의 로컬 머신에서 실행됩니다.
https://github.com/TsvetanG2/mcpward
실무에서 설명 드리프트(description drift)로 인해 어려움을 겪었거나, 제가 파괴적/비파괴적(breaking/non-breaking) 경계선을 잘못 그었다고 생각하신다면 의견을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기