# ADR-0010 Tool 실행 endpoint를 Tool Service 매니페스트가 선언한다 - 상태: Accepted - 결정일: 2026-08-19 - 기록: 2026-08-22. 구현 커밋 `6078852`를 기준으로 사후 작성했다. - 관련: [ADR-0006](ADR-0006-no-authentication-in-mcp.md), [ADR-0007](ADR-0007-one-mcp-per-tool-service.md) ## 배경 이전 설계에서 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의 배포 주기를 묶어 버린다. ## 결정 1. Tool 실행 주소는 Tool Service 매니페스트가 선언한다. MCP는 각 Tool의 top-level `endpoint`를 먼저 읽고, 없으면 `_meta.endpoint`를 사용한다. 2. 둘 다 없거나 비어 있으면 그 Bundle 전체를 거부한다. Tool 하나의 누락이 나머지 Tool을 조용히 통과시키지 않는다. 3. 상대 경로는 Portal registry가 제공한 `serviceDomain` 뒤에 붙여 절대 URL로 만든다. 이것이 운영에서 기대하는 형태다. 4. 절대 URL은 Tool Service가 제공한 실행 주소 원천으로 그대로 사용한다. scheme이 HTTP(S)가 아니거나 host가 없으면 거부한다. 프로토콜 상대 주소(`//host/path`)와 개행이 섞인 값도 거부한다. 5. `mcp.bundles[].baseEndpoint`는 더 이상 실행 주소의 정본이 아니다. 상대 경로를 해석하는 기준으로만 남으며, 절대 HTTP(S)여야 한다. 6. `endpoint`는 내부 실행 정보이므로 `_meta`와 함께 제거해 `tools/list` 공개본에 내보내지 않는다. ```text 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](ADR-0006-no-authentication-in-mcp.md)에 따라 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로 남긴다.