Files
dap-was-dapms/docs/contracts/portal-mcp/protocol-v0.1-registry.md
koseokmin 5c069a0ee7 Portal registry ADR의 번호를 0013으로 옮긴다
main이 6078852의 endpoint 소유권 반전을 ADR-0010으로 기록하면서 이
브랜치의 ADR-0010과 번호가 겹쳤다. 두 문서는 다른 결정이므로 나중에
문서를 합칠 때 한쪽을 옮겨야 한다.

이 브랜치가 0012까지 쓰고 있어 0011·0012를 건드리지 않는 첫 번호인
0013을 쓴다. 파일명과 제목, 대체 관계를 가리키는 ADR-0007·ADR-0009,
결정 목록, architecture 문서, Portal 계약 문서, Helm 설명, 그리고
application.yml 주석의 참조를 함께 바꾼다. 결정 내용은 바뀌지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:23:29 +09:00

12 KiB

Portal-MCP Registry 조회 계약 v0.1

  • 상태: MCP 측 구현 완료, Portal 측 미합의
  • 기준일: 2026-08-14
  • 조회 endpoint: GET {mcp.portal.registry-url} — Portal이 제공
  • 구현: PortalToolRegistryClient (@ConditionalOnProperty(mcp.portal.enabled=true))

1. 계약 범위와 원칙

Portal은 route별로 어떤 Tool Server가 있고 그 주소가 무엇인지를 관리한다. MCP는 이 목록을 주기적으로 조회해 Tool Service 매니페스트 조회 대상을 결정한다.

원칙 내용
Portal은 주소만 말한다 Tool 목록·schema·timeout은 Tool Service 매니페스트가 소유한다. Portal 응답에는 Tool 정의가 없다
MCP가 가져온다 Portal은 제공만 한다. MCP에 push하지 않으며 MCP는 쓰기 endpoint를 열지 않는다
응답은 전체 상태 증분이 없다. 응답에 없는 route는 memory에서 제거된다(§6)
조회 주기가 분리된다 Portal registry와 Tool 매니페스트는 서로 다른 주기로 조회한다(§6)
실패는 삭제가 아니다 어떤 실패도 endpoint 목록이나 Tool snapshot을 비우지 않는다(§7)
요청 경로는 Portal을 모른다 tools/list·tools/call은 in-memory snapshot만 읽는다

mcp.bundles를 쓰는 구성과의 차이는 하나뿐이다. Tool Server 주소가 배포 YAML에서 오느냐 Portal에서 오느냐. 주소를 확보한 다음의 매니페스트 조회·검증·병합은 tool-service-mcp v0.2를 그대로 재사용한다.

2. Portal이 제공하는 endpoint

Method Path 용도 MCP가 호출하는가
GET /api/portal/registry 전체 route 집계 조회 예. 유일한 호출 대상
GET /api/portal/registry/{routeKey} 단일 route 조회 아니오 (§5 참고)

MCP는 mcp.portal.registry-url에 설정된 하나의 URL만 호출한다. route별로 나눠 호출하지 않는다. 따라서 registry-url은 집계 endpoint를 가리켜야 한다.

registry-url{route} placeholder를 쓸 수 있게 되어 있으나, 현재 구현은 registry 갱신 시 {route}항상 빈 문자열로 치환한다(registryUrl("")). 즉 /api/portal/registry/{route} 형태로 설정하면 /api/portal/registry/를 호출해 실패한다. placeholder를 쓰지 않는다.

단일 route 조회 endpoint는 Portal 화면과 운영 확인용으로 남아 있으며 MCP 경로가 아니다. 다만 응답 shape는 MCP가 파싱할 수 있는 형태를 유지한다(§4.2). 이유는 §4.3에 있다.

3. MCP 설정 (YAML)

예시는 mcp-portal-config.yaml에 있다.

mcp:
  portal:
    enabled: true
    registry-url: http://portal.ax-hub.svc.cluster.local:8080/api/portal/registry
    refresh-interval-seconds: 300
  discovery:
    enabled: true
  bundles: []
항목 필수 설명
mcp.portal.enabled true일 때만 PortalToolRegistryClient가 등록된다. falsemcp.bundles를 사용한다
mcp.portal.registry-url enabled=true일 때 예 집계 조회 URL. 누락 시 기동이 실패한다(McpProperties.isPortalTargetDeclared)
mcp.portal.refresh-interval-seconds 아니오(기본 300) Portal registry 조회 주기
mcp.redis.portal-registry-key 아니오 Portal registry fallback Redis key. 기본값은 {key-prefix}:portal-registry

mcp.portal.enabled=true이면 mcp.bundles는 비운다. endpoint 원천이 둘이 되지 않게 한다.

route key를 지정하는 설정은 없다. route key는 요청 경로에서만 결정되며(McpRequestContextFactory), 설정 기본값으로 보정하지 않는다(§7). 과거 mcp.portal.route-key가 선언만 되어 있었으나 어떤 코드도 읽지 않아 제거했다(ADR-0013).

4. 응답 계약

4.1 집계 응답 (MCP가 사용하는 형태)

예제: aggregate-registry-response.json

{
  "registryRevision": 12,
  "routes": [
    { "routeKey": "external", "toolServices": [ /* §4.4 */ ] }
  ]
}
필드 필수 타입 의미
registryRevision 아니오 number 또는 string 변경 감지용 판. §6
routes array route 전체 목록. 이 배열이 있으면 집계 응답으로 해석한다
routes[].routeKey string 비어 있으면 registry 오류. 공백은 trim된다
routes[].toolServices array 해당 route의 Tool Server 목록. §4.4

4.2 단일 route 응답

예제: route-registry-response.json

{
  "routeKey": "external",
  "registryRevision": 12,
  "toolServices": [ /* §4.4 */ ]
}
필드 필수 의미
routeKey 최상위에 있어야 한다
toolServices §4.4

4.3 두 형태를 모두 받는 이유와 그 위험

MCP는 응답에 routes 배열이 없으면 단일 route 문서로 해석해 최상위 routeKey를 읽는다. 이 관용은 Redis fallback에 저장된 과거 형태를 읽기 위한 것이다.

두 형태의 삭제 의미가 다르다.

응답 형태 memory 반영
집계(routes 있음) 응답에 없는 route를 제거한다. 전체 상태 교체
단일(routes 없음) 그 route만 덮어쓴다. 다른 route는 남는다

따라서 운영에서 Portal은 항상 집계 형태로 응답한다. 단일 형태를 정기 조회 대상으로 쓰면 Portal에서 삭제한 route가 MCP memory에 영원히 남는다.

4.4 toolServices[] 항목

필드 필수 기본값 MCP가 만드는 값
serviceKey bundle id. 매니페스트의 bundleId와 일치해야 한다
serviceDomain scheme+host+port. 끝 /는 제거된다
manifestPath manifestUrl = serviceDomain + manifestPath. 앞 /가 없으면 붙인다
executeBasePath 아니오 "" baseEndpoint = serviceDomain + executeBasePath. 앞뒤 /가 정규화된다
namePrefix 아니오 "" Tool name 접두사 검증 기준
toolEndpoints 아니오 {} Tool name → 실행 path. 값은 앞 /가 보장되도록 정규화된다
status 아니오 "ACTIVE" ACTIVE가 아니면 조용히 제외한다. 대소문자 무시
displayName 아니오 Portal 화면용. MCP는 무시한다
  • 필수 필드가 없거나 공백이면 registry 오류다. 오류 메시지에는 필드명만 남기고 응답 원문은 넣지 않는다.
  • ACTIVE 서비스가 하나도 없으면 그 응답 전체를 실패로 처리한다. 빈 목록으로 교체하지 않는다.
  • toolEndpoints가 비면 실행 주소는 baseEndpoint에 Tool name을 붙이는 기존 계약을 따른다.

5. Portal이 응답에 넣지 않는 것

넣지 않는 것 이유
Tool 정의(name, schema, timeout) Tool Service 매니페스트가 정본이다
매니페스트 조회용 API key §9의 미확정 항목. 현재 MCP는 자기 설정의 key를 쓴다
MCP 자신의 endpoint 주소 MCP가 자기 주소를 Portal에서 받지 않는다

6. 조회 주기와 변경 감지

주기 대상 설정
기동 preload Portal registry → 각 Tool Service 매니페스트 즉시
mcp.portal.refresh-interval-seconds Portal registry만 기본 300초
mcp.registry.refresh-interval-seconds 저장된 endpoint의 매니페스트만 기본 30초

registryRevision이 직전과 다르면 MCP는 그 응답 전체를 INFO 로그로 남기고, 즉시 매니페스트 refresh를 한 번 더 트리거한다(ToolRegistryRefreshSchedulerportal-change). Portal에서 endpoint를 바꾼 뒤 매니페스트 주기를 기다리지 않게 하기 위한 것이다.

registryRevision변경 감지에만 쓴다. 순서 비교를 하지 않으므로 값이 되돌아가도 "변경됨"으로 처리한다. 단조 증가는 Portal이 보장할 항목이다(README 확정 항목 3).

이 로그는 응답 JSON 전체를 출력한다. registry 응답에는 credential이 없으나 내부 endpoint 주소가 그대로 남는다. 폐쇄망 운영 로그 정책에서 확인이 필요하다.

7. 실패 처리

모든 registry 실패는 JSON-RPC TOOL_REGISTRY_UNAVAILABLE로 변환된다.

상황 동작
Portal 조회 실패 + memory에 endpoint 있음 memory 유지. Redis를 읽지 않는다. WARN 로그
Portal 조회 실패 + memory 비어 있음(cold start) mcp.redis.portal-registry-key의 registry JSON을 fallback으로 읽는다
Portal·Redis 모두 실패 실패로 처리하고 다음 주기에 재시도. 목록은 비우지 않는다
요청 route가 memory에 없음 Portal registry route is not found: {routeKey}
요청 route key가 공백 Portal registry routeKey is required. 설정 기본 route로 보정하지 않는다
ACTIVE 서비스 없음 Portal registry has no active Tool Service
직전 성공본조차 없는 Tool Service가 있음 카탈로그 전체를 교체하지 않는다
Tool name 중복 (서비스 간) 교체하지 않는다
mcp.discovery.max-tools-total 초과 교체하지 않는다

마지막 세 항목은 tool-service-mcp v0.2의 병합 규칙을 그대로 따른다. route key를 보정하지 않는 것은 잘못된 단일 진입점 호출을 조용히 성공시키지 않기 위한 것이다.

Redis fallback은 두 종류이며 key가 분리된다.

key 내용 언제
mcp.redis.portal-registry-key Portal registry 응답 JSON route·endpoint 목록 자체를 모를 때
{key-prefix}:{identity}:v2:route:{routeToken} route별 Tool snapshot 이미 아는 route의 마지막 Tool 목록

8. 보안 요구사항

mcp.bundles 구성에서 AGENTS.md의 불변식은 "outbound 주소는 설정에서만 온다"이다. Portal 구성에서는 그 원천이 Portal로 옮겨간다.

따라서 이 계약은 다음을 요구한다.

  1. Portal registry API는 공개 네트워크에 노출하지 않는다. MCP와 Portal 사이는 NetworkPolicy로 제한한다.
  2. Portal의 쓰기 API(bundle 등록·수정)는 인증을 요구한다. 이 API를 장악하면 MCP의 호출 대상을 바꿀 수 있다.
  3. Tool Service 매니페스트는 여전히 호출 대상을 바꾸지 못한다. 매니페스트는 serviceDomain을 덮어쓸 수 없다.

1·2를 만족하지 못하는 환경에서는 Portal 구성을 쓰지 않고 mcp.bundles를 쓴다.

9. 미확정 항목

항목 현재 확정 필요
Portal API 인증 없음 방식과 credential 관리 주체
매니페스트 조회 credential MCP 설정의 mcp.tool-client.api-key 단일 값 Tool Service별로 다를 때 전달 경로. registry 응답에 담을지 여부
registryRevision 채번 Portal in-memory 카운터 재기동 시 유지 여부, 단조 증가 보장
route 삭제 집계 응답에서 사라지면 즉시 제거 진행 중 요청에 대한 rolling 처리

10. 예제와 검증

파일 용도
aggregate-registry-response.json 운영에서 MCP가 받는 형태
route-registry-response.json 단일 route 형태
mcp-portal-config.yaml MCP 설정 예시

앞의 두 JSON은 PortalRegistryContractExampleTest가 읽어 PortalToolRegistryClient의 실제 파싱 경로에 태운다. serviceDomain만 테스트가 MockWebServer 주소로 치환하며, 나머지 필드는 파일 그대로 사용한다. 예제를 고치면 이 테스트가 함께 깨져야 한다.