Portal을 endpoint 원천으로 확정하고 route 단위 실패 격리를 적용한다
내부망 운영은 route↔Tool Service 매핑을 Portal이 소유하고, MCP 배포 하나가 N개 route를 서비스하며, route 하나에 N개 Tool Service가 붙을 수 있다. 이 판단을 ADR-0010으로 남기고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다. 계약 - Portal-MCP registry 조회 계약 v0.1과 예제 JSON을 docs/contracts/portal-mcp/에 신설한다. 지금까지 이 경로에는 정본이 없었다. - 예제를 PortalToolRegistryClient의 실제 파싱 경로에 태우는 계약 테스트를 추가해 문서와 구현이 따로 표류하지 않게 한다. 실패 격리 - fetchAllTools()의 실패 전파를 route 단위로 격리한다. 계약 v0.2의 "aggregate는 전부 아니면 전무"는 카탈로그 하나를 전제한 규칙인데, route가 N개가 되면서 전 route로 확대돼 있었다. Tool Service 하나의 장애가 cold start에서 Pod 전체를 내리고 steady state에서 모든 route의 갱신을 멈추던 동작을 없앤다. - 제거 판단의 원천을 ToolRegistryClient.knownRoutes()로 분리한다. 조회 결과를 기준으로 지우면 이번 주기에 실패한 route의 정상 snapshot까지 사라져 "어떤 실패도 목록을 비우지 않는다" 불변식이 깨진다. - readiness는 최소 1개 route로 UP을 유지한다. 모든 route를 요구하면 정상 route까지 트래픽에서 빠져 위 격리를 되돌리기 때문이다. 대신 routesWithoutSnapshot을 health detail로 노출해 관제가 부분 상태를 감지하게 한다. 설정과 기동 - warm start가 route별 Redis key를 읽도록 확장하고, 읽을 key를 알기 위해 기동 preload 순서를 registry 조회 → warm start → manifest 조회로 바꾼다. - 어떤 코드도 읽지 않던 mcp.portal.route-key를 제거한다. route key는 요청 URI에서만 결정되며, 설정으로 보정하면 잘못된 단일 진입점 호출이 조용히 성공한다. 정리 - ToolRegistryService.java의 이중 인코딩으로 깨져 있던 한글 Javadoc 33줄을 코드 동작에 맞춰 다시 쓰고, replaceSnapshot 위에 겹쳐 있던 고아 Javadoc 블록을 지운다. - Helm chart는 mcp.bundles 구성에서 계속 유효하므로 삭제하지 않고, 내부망 운영 대상이 아니라는 사실을 deploy/README.md와 values.yaml에 명시한다. 검증: 이 환경은 loopback이 막혀 gradlew check를 실행하지 못했다. CodeStyleContract가 보는 항목(줄바꿈, 탭, 행말 공백, 파일 끝 개행, 미사용 import, import 순서)은 변경된 Java 13개 파일에 대해 따로 재현해 확인했다. 컴파일과 테스트 실행은 미확인이다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"registryRevision": 12,
|
||||
"routes": [
|
||||
{
|
||||
"routeKey": "business",
|
||||
"toolServices": [
|
||||
{
|
||||
"serviceKey": "business-tools",
|
||||
"displayName": "Business Tool Server",
|
||||
"serviceDomain": "http://tool-business.ax-hub.svc.cluster.local:8080",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "",
|
||||
"namePrefix": "business.",
|
||||
"toolEndpoints": {
|
||||
"business.customer_search": "/mcp/business.customer_search",
|
||||
"business.order_status": "/mcp/business.order_status"
|
||||
},
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"routeKey": "external",
|
||||
"toolServices": [
|
||||
{
|
||||
"serviceKey": "external-tools",
|
||||
"displayName": "External Tool Server",
|
||||
"serviceDomain": "http://tool-external.ax-hub.svc.cluster.local:8080",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "",
|
||||
"namePrefix": "external.",
|
||||
"toolEndpoints": {
|
||||
"external.exchange_rate": "/mcp/external.exchange_rate",
|
||||
"external.weather_lookup": "/mcp/external.weather_lookup"
|
||||
},
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# MCP Server의 Portal registry 설정 예시 (protocol-v0.1-registry.md 3절)
|
||||
#
|
||||
# 이 파일은 계약 예시이며 실제 적용 설정이 아니다.
|
||||
# 운영에서는 ConfigMap으로 주입한다.
|
||||
#
|
||||
# 키 이름은 구현된 McpProperties와 1:1로 맞춰 두었다. Spring relaxed binding이
|
||||
# camelCase와 kebab-case를 모두 받으므로 이 문서는 application.yml과 같은 kebab-case를 쓴다.
|
||||
|
||||
mcp:
|
||||
portal:
|
||||
# true일 때만 PortalToolRegistryClient가 등록된다.
|
||||
# false면 아래 bundles 목록이 endpoint 원천이 된다.
|
||||
enabled: true
|
||||
|
||||
# 반드시 집계 조회 endpoint를 가리킨다. route별 URL이나 {route} placeholder를 쓰지 않는다.
|
||||
# 이유는 계약 2절에 있다.
|
||||
registry-url: http://portal.ax-hub.svc.cluster.local:8080/api/portal/registry
|
||||
|
||||
# Portal registry 조회 주기. 매니페스트 조회 주기(mcp.registry)와 분리된다.
|
||||
# registryRevision이 바뀌면 이 주기와 별개로 매니페스트 refresh가 즉시 한 번 더 돈다.
|
||||
refresh-interval-seconds: 300
|
||||
|
||||
registry:
|
||||
# 저장된 endpoint의 Tool 매니페스트를 다시 읽는 주기.
|
||||
refresh-interval-seconds: 30
|
||||
refresh-jitter-seconds: 5
|
||||
|
||||
discovery:
|
||||
# Portal 구성에서도 매니페스트 조회·검증 경로는 그대로 사용한다.
|
||||
enabled: true
|
||||
connect-timeout-millis: 1000
|
||||
read-timeout-millis: 3000
|
||||
max-tools-per-bundle: 100
|
||||
max-tools-total: 200
|
||||
max-manifest-bytes: 1048576
|
||||
max-tool-timeout-millis: 30000
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
key-prefix: axhub:mcp
|
||||
# Portal registry 응답 JSON의 fallback key.
|
||||
# route별 Tool snapshot key와 반드시 분리한다(계약 7절).
|
||||
# 운영에서는 Portal이 쓰는 key와 값을 맞춘다.
|
||||
portal-registry-key: axhub:mcp:portal-registry
|
||||
|
||||
# Portal이 endpoint 원천이므로 이 목록은 비운다.
|
||||
# 원천이 둘이 되면 어느 쪽이 이겼는지 로그로 구분할 수 없다.
|
||||
bundles: []
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"routeKey": "external",
|
||||
"registryRevision": 12,
|
||||
"toolServices": [
|
||||
{
|
||||
"serviceKey": "external-tools",
|
||||
"displayName": "External Tool Server",
|
||||
"serviceDomain": "http://tool-external.ax-hub.svc.cluster.local:8080",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "",
|
||||
"namePrefix": "external.",
|
||||
"toolEndpoints": {
|
||||
"external.exchange_rate": "/mcp/external.exchange_rate",
|
||||
"external.weather_lookup": "/mcp/external.weather_lookup"
|
||||
},
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user