ADR-0007은 MCP 배포 하나가 Tool Service 하나만 보게 했으나 구현은 이미 Portal이 route와 endpoint를 소유하는 구조다. 결정 문서가 없어 ADR-0007이 Accepted로 남은 채 코드와 정반대되는 내용을 현재 설계 근거처럼 제시하고 있었다. ADR-0013이 그 경로를 확정하고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다. ADR-0010은 Tool 실행 주소의 소유자가 설정이 아니라 Tool Service 매니페스트라는 6078852의 결정을 사후 기록한다. 두 ADR이 정본으로 인용하는 Portal-MCP 계약 v0.1도 함께 넣는다. ADR-0013은 main 현재 코드에 맞춰 세 곳을 고쳤다. route 간 갱신 격리 부재는 6653030이 해소해 격리 표로 옮겼고, 제거된 mcp.portal.route-key 항목은 뺐다. 남은 위험 둘(Portal 모드 warm start 미동작, readiness의 route별 상태 미노출)은 코드에서 유효함을 확인해 남긴다. ADR-0010이 기록하는 대로 endpoint 검증에는 도메인 허용목록이 없고 NetworkPolicy도 Ingress만 선언한다. 매니페스트 원천의 신뢰성이 곧 outbound 대상의 신뢰성이다. McpProperties와 application.yml의 주석이 이 결정과 반대로 남아 있다는 사실도 ADR에 적었다. 코드는 바꾸지 않는다. 192개 테스트 통과. 문서 상대링크 깨짐 없음. 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로 남긴다.