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>
6.2 KiB
Portal-MCP Tool Server Registry 계약 v0.1
- 상태: Implemented (MCP 서버 측 구현 완료, 포털 측 합의 대기)
- 기준일: 2026-08-22
- 조회 위치:
mcp.portal.registry-url— HTTP(S) Portal API 또는file:/classpath:로컬 리소스 - 활성 조건:
mcp.portal.enabled=true - 구현:
PortalToolRegistryClient
1. 계약 범위와 원칙
포털은 route별로 어떤 Tool Server가 있는가만 알려 준다. Tool 목록과 Tool 실행 주소는 포털이 아니라 각 Tool Server의 매니페스트에서 온다(Tool Service-MCP Bundle 조회 계약 v0.2, ADR-0010).
| 원칙 | 내용 |
|---|---|
| MCP가 가져온다 | 포털은 registry를 제공만 한다. MCP에 push하지 않는다 |
| 포털은 route와 Tool Server만 소유 | serviceDomain과 manifestPath까지다. Tool 목록·실행 endpoint는 매니페스트가 정한다 |
| registry는 전체 상태 | 응답은 그 시점 route 전체다. 증분 없음 |
| route 단위 격리 | 한 route의 manifest 조회 실패가 다른 route의 snapshot을 지우지 않는다 |
| 요청 경로는 조회하지 않는다 | tools/list·tools/call과 route key 검증은 in-memory snapshot만 본다 |
2. 응답 형태
두 가지를 모두 받는다. routes가 배열이면 집계형으로, 아니면 단일 route로 해석한다.
집계형 — 한 번의 호출로 모든 route를 받는다. 운영에서 사용한다.
{
"registryRevision": "portal-registry-2026-08-22-01",
"routes": [
{
"routeKey": "cus",
"toolServices": [
{
"serviceKey": "was-cus",
"serviceDomain": "https://tool-cus.devjun.net",
"manifestPath": "/tool-manifest",
"namePrefix": "",
"status": "ACTIVE"
}
]
}
]
}
단일 route형 — routes 없이 최상위에 routeKey와 toolServices를 둔다.
{
"routeKey": "cus",
"toolServices": [ { "serviceKey": "was-cus", "serviceDomain": "https://tool-cus.devjun.net", "manifestPath": "/tool-manifest", "status": "ACTIVE" } ]
}
3. 필드
| 필드 | 위치 | 필수 | MCP 처리 |
|---|---|---|---|
routes[] |
최상위 | 선택 | 배열이면 집계형. 없으면 최상위를 단일 route로 읽는다 |
routeKey |
route | 필수 | 정규화 후 route 식별자. 요청 URL /mcp/{routeKey}와 대조한다 |
toolServices[] |
route | 필수 | 이 route가 보는 Tool Server 목록 |
serviceKey |
service | 필수 | bundle id로 사용. 매니페스트의 bundleId와 일치해야 한다 |
serviceDomain |
service | 필수 | Tool Server 주소. 후행 /는 제거한다. 매니페스트의 상대 endpoint를 절대 URL로 바꾸는 기준이 된다 |
manifestPath |
service | 필수 | 매니페스트 경로. serviceDomain + manifestPath가 조회 주소다 |
status |
service | 선택 | 기본 ACTIVE. 대소문자 무시하고 ACTIVE가 아니면 그 서비스를 건너뛴다 |
namePrefix |
service | 선택 | 기본 "". Tool 이름 접두사 검증에 사용한다 |
registryRevision |
최상위 | 선택 | 변경 진단·로그용. 호출 대상 결정에는 쓰지 않는다 |
MCP가 읽지 않는 필드가 있다. 현재 구현은 executeBasePath, displayName, toolEndpoints를 무시한다. 응답에 있어도 오류가 아니지만 동작에 영향을 주지 않으므로, 포털이 이를 근거로 실행 주소를 통제할 수 있다고 가정하면 안 된다.
4. 실패 동작
| 상황 | MCP 동작 |
|---|---|
| registry 조회 실패 | 이미 확보한 in-memory endpoint 목록 유지. memory가 비어 있으면 mcp.redis.portal-registry-key의 Redis fallback을 읽는다 |
| Redis fallback도 실패 | 원천 미확보로 처리하고 다음 주기에 재시도 |
| route에 ACTIVE 서비스가 하나도 없음 | Portal registry has no active Tool Service로 그 route 조회 실패 |
| 한 Tool Server의 매니페스트에 사용 가능한 성공본이 없음 | 그 route 전체를 실패 처리. 부분 목록을 채택하지 않는다 |
route 간 Tool name 중복 또는 maxToolsTotal 초과 |
같은 이유로 실패 처리 |
| registry에서 사라진 route | 다음 갱신에 in-memory snapshot에서도 제거 |
route 단위 격리와 catalog 교체 규칙은 Tool Service 계약 v0.2 §7과 같은 원칙을 따른다. 조회는 route마다 독립이지만, 한 route 안에서는 전부 아니면 전무다.
5. 갱신 주기
mcp.portal.refresh-interval-seconds(기본 300초): 포털 registry만 다시 읽는다.mcp.registry.refresh-interval-seconds: 이미 확보한 Tool Server 목록의 매니페스트만 다시 읽는다.
기동 preload는 포털 registry를 먼저 호출한 뒤 매니페스트를 조회한다. 두 주기는 독립이다.
6. 로컬 검증
registry-url에 file: 또는 classpath: 리소스를 지정하면 포털 서버 없이 같은 계약으로 읽는다. 이후 매니페스트 조회·route별 snapshot 갱신·Redis fallback 규칙은 HTTP API를 쓸 때와 동일하다.
mcp:
portal:
enabled: true
registry-url: file:./config/local-toolserver-info-sample-v1.json
refresh-interval-seconds: 15
7. 저장소의 샘플 파일
두 샘플이 있고, 현재 서로 다르다. 이 계약을 정본으로 삼고 맞춰야 한다.
| 파일 | 용도 | 이 계약과의 차이 |
|---|---|---|
config/local-toolserver-info-sample-v1.json |
로컬 검증용 | MCP가 읽지 않는 displayName을 포함 |
deploy/portal-registry.json |
배포 참고용 | MCP가 읽지 않는 executeBasePath를 포함하고 registryRevision이 없다 |
8. 열린 항목
- 포털 API의 인증 방식은 이 계약이 정하지 않는다. MCP는 인증·인가를 하지 않으므로(ADR-0006) 네트워크 경계에서 통제한다.
registryRevision의 형식을 문자열로 고정할지 정하지 않았다. 현재 구현은 값을 로그·진단에만 쓰므로 형식에 의존하지 않는다.- 즉시 refresh 알림: 주기 반영으로 부족하다는 운영 근거가 생길 때 검토한다.