5cfb8a1이 .gitignore에 docs/를 넣고 68개 파일을 지웠다. 그런데
AgentBuilderContractExampleTest, ToolBundleContractExampleTest,
ArchitectureDocumentContractTest는 docs/ 아래 계약 예제와 architecture
문서를 입력으로 직접 읽는다. 그 결과 clean clone에서 테스트 10건이
입력을 찾지 못해 실패했다.
제외 범위를 원래 의도대로 좁힌다. 에이전트 산출물(AGENTS.md, .agents/,
.codex/, docs/superpowers/)은 계속 제외하고 저장소 문서는 추적한다.
문서는 삭제 직전 상태(3de052a)를 기준으로 복원하고, 그 위에 main 코드와
대조해 어긋난 부분을 고쳤다.
- ADR-0007을 Superseded로 바꾸고 ADR-0013을 새로 쓴다. route당 Tool
Service N개가 최종안이며, PortalToolRegistryClient가 이미 route별로
N개를 유지하고 있는데 ADR-0007은 "bundles는 항상 한 항목"을 Accepted
상태로 주장하고 있었다. ADR-0009 결정 4도 부분 대체한다.
- ADR-0008 파일 헤더가 Accepted였으나 ADR-0009가 이미 대체한 상태였다.
- 6078852의 endpoint 소유권 반전이 반영되지 않은 서술을 계약 v0.2,
bundle 설정 예제, Tool 적재 안내에서 고친다.
- Portal registry 계약 v0.1과 route key 규약을 새로 문서화한다. 둘 다
구현은 있는데 계약 문서가 없었다.
- MCP SDK 2.0.0 SBOM을 추가한다.
번호 주석: ADR-0011과 0012는 feature/mcp-integration이 Tool inputSchema
정책에 쓰고 있어 비워 둔다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.5 KiB
5.5 KiB
ADR-0010 Tool 실행 endpoint를 Tool Service 매니페스트가 선언한다
배경
이전 설계에서 Tool 실행 주소는 MCP 배포 설정이 단독으로 소유했다. mcp.bundles[].baseEndpoint 뒤에 Tool name을 붙여 POST {baseEndpoint}/{toolName}을 만들었고, 매니페스트가 endpoint 성격의 값을 담고 있어도 읽지 않았다. Tool Service가 반환한 어떤 값도 MCP가 요청을 보내는 대상을 바꿀 수 없다는 것이 이 설계의 핵심이었고, docs/architecture.md, Tool Service 계약 v0.2, ToolBundleDiscovery의 Javadoc, application.yml 주석이 같은 문장을 반복해 고정하고 있었다.
Portal registry를 Tool Server 목록의 원천으로 도입하면서 전제가 무너졌다. 포털은 route별로 Tool Server의 serviceDomain과 manifestPath만 제공한다. Tool 하나하나의 실행 경로는 포털이 모르고, MCP 배포 설정도 미리 알 수 없다. 기존 구조를 유지하려면 Tool을 추가하거나 경로를 바꿀 때마다 배포 설정의 endpoint 목록을 함께 고쳐야 했고, 이는 Tool Service의 배포 주기와 MCP의 배포 주기를 묶어 버린다.
결정
- Tool 실행 주소는 Tool Service 매니페스트가 선언한다. MCP는 각 Tool의 top-level
endpoint를 먼저 읽고, 없으면_meta.endpoint를 사용한다. - 둘 다 없거나 비어 있으면 그 Bundle 전체를 거부한다. Tool 하나의 누락이 나머지 Tool을 조용히 통과시키지 않는다.
- 상대 경로는 Portal registry가 제공한
serviceDomain뒤에 붙여 절대 URL로 만든다. 이것이 운영에서 기대하는 형태다. - 절대 URL은 Tool Service가 제공한 실행 주소 원천으로 그대로 사용한다. scheme이 HTTP(S)가 아니거나 host가 없으면 거부한다. 프로토콜 상대 주소(
//host/path)와 개행이 섞인 값도 거부한다. mcp.bundles[].baseEndpoint는 더 이상 실행 주소의 정본이 아니다. 상대 경로를 해석하는 기준으로만 남으며, 절대 HTTP(S)여야 한다.endpoint는 내부 실행 정보이므로_meta와 함께 제거해tools/list공개본에 내보내지 않는다.
manifest endpoint = "/mcp/processing.contract.inquiry" (운영 관례)
-> https://tool-cus.devjun.net/mcp/processing.contract.inquiry
manifest endpoint = "https://other.example/tool" (허용되지만 위험)
-> https://other.example/tool
영향
- Tool을 추가하거나 실행 경로를 바꿀 때 MCP 배포 설정을 함께 바꾸지 않아도 된다. Tool Service가 매니페스트만 갱신하면 다음 refresh에 반영된다.
- 신뢰 경계가 이동한다. 이전에는 배포 설정이 outbound 대상을 봉인했으나, 이제는 매니페스트가 결정한다. 매니페스트가 절대 URL을 선언하면 MCP는 그 호스트로 요청을 보낸다.
- 검증은 두 지점에 있다. discovery 시점에
ToolBundleDiscovery가 scheme·host·프로토콜 상대 주소·개행을 확인하고, 실행 시점에ToolRoutingService.validateEndpoint()가 절대 HTTP(S)인지 다시 확인한다. 둘 다 형식 검사이며 도메인 허용목록은 없다. 따라서 매니페스트 원천의 신뢰성이 곧 outbound 대상의 신뢰성이다. - 네트워크 계층의 완화도 없다.
deploy/helm/mcp-server/templates/networkpolicy.yaml은policyTypes: [Ingress]만 선언하므로 outbound 목적지를 제한하지 않는다. 이 저장소가 제공하는 allowlist(route.sourceAllowlist, NetworkPolicy)는 모두 inbound 통제다. - ADR-0006에 따라 MCP는 인증·인가를 하지 않는다. 그래서 "
GET /tool-manifest를 NetworkPolicy로 MCP Server namespace에서만 접근 가능하게 한다"는 기존 요구가 선택적 권고가 아니라 이 결정의 전제 조건이 된다. - endpoint 검증 실패는 Bundle 전체 거부로 처리되고 직전 정상 snapshot이 유지되므로, 잘못된 매니페스트 배포가 기존 Tool 목록을 지우지는 않는다.
- 계약 문서와 예제가 함께 갱신됐다.
docs/contracts/tool-service-mcp/examples/bundle-v0.2/manifest-response.json이endpoint를 포함하며,ToolBundleContractExampleTest가 문서와 구현의 일치를 고정한다.
남은 위험
- 도메인 허용목록 부재. 매니페스트가 임의의 HTTP(S) 호스트를 지정할 수 있고, 애플리케이션 검사도 네트워크 정책도 이를 좁히지 않는다. 내부망 운영 전에 두 방향 중 하나를 정해야 한다.
mcp.tool-domains형태의 allowlist를 두고 절대 URL을 그에 대조한다.- 절대 URL을 아예 거부하고 상대 경로만 허용해 목적지를 Portal registry의
serviceDomain으로 봉인한다. 운영 예제가 이미 상대 경로만 쓰고 있어 비용이 가장 낮고, 이전 설계의 "Tool Service가 호출 대상을 바꿀 수 없다"는 성질도 회복된다.
- egress NetworkPolicy를 함께 검토한다. 위 두 방안 중 무엇을 택하든 애플리케이션 단독 방어보다 낫다.
- 이 ADR은 기존 ADR을 대체하지 않는다. 뒤집힌 불변식이 ADR이 아니라 architecture 문서와 코드 주석에만 있었기 때문이다. 같은 일이 반복되지 않도록 실행 주소 관련 결정은 앞으로 이 ADR을 갱신하거나 후속 ADR로 남긴다.