29 KiB
AX HUB MCP Server 설계 설명
책임 경계
Agent Builder (판단, Tool 선택)
-> JSON-RPC 2.0 / Streamable HTTP
OpenShift Route (공유 host, 배포별 public path로 Service 선택, rewrite 없음)
-> 배포별 MCP Service
MCP HTTP Transport (transport/http: filter, controller, correlation, 오류 변환)
-> JSON-RPC Parser (envelope/method/params 정규화)
-> Handler Registry (method dispatch)
-> Execute Layer (metadata, validation, routing, flow control)
-> ToolClient (REST, timeout, header propagation)
Tool Service (Business Rule, Legacy/MCI/EAI 연계)
MCP는 Agent Builder가 tools/call에 명시한 단일 Tool을 실행한다. Tool 추천, 사용자 의도 해석, 업무 데이터 재가공, LLM reasoning은 이 경계 밖이다.
인증과 인가도 이 경계 밖이다(ADR-0006). 외부 호출자 제한은 Route IP allowlist가, Pod 직접 접근 제한은 NetworkPolicy가, 사용자 인증과 Tool 권한은 Agent Builder가, 사원 식별자 복호화와 업무 권한은 Tool Service가 담당한다. MCP는 요청 형식과 inputSchema만 검증한다.
전체 실행 흐름
- Agent Builder가 배포별 공개 URL
https://{host}{publicPath}을 호출한다. OpenShift Route는 host와 path로 MCP Service만 선택하고 원래 path를 컨테이너에 전달한다(ADR-0009). 요청 body·header·Tool 이름은 이 선택에 관여하지 않는다. - 컨테이너의
mcp.endpoint-path전용McpExchangeFilter가 header를 검증/추출하고 요청 전체 deadline을 포함한McpRequestContext를 만든다. 호출자 헤더 다섯 개(guid,x-request-id,mcp-session-id,employee-no,virtual-employee-no)는 모두 선택값이며, 응답과 downstream Tool 호출에 그대로 전파한다.guid는 요청 하나의 end-to-end 상관 값,x-request-id는 개별 HTTP 요청 식별자다. - filter는 크기가 제한된 repeatable request body에서
method만 관찰용으로 읽고mcp_http_request_received로그를 남긴다. body, header 값, credential은 로그에 저장하지 않는다. McpProtocolVersionValidator가initialize를 제외한 요청의MCP-Protocol-Version을 supported versions와 대조한다. 누락·불일치는 Controller 진입 전 HTTP 400으로 종료한다.JsonRpcRequestParser가 envelope를 정규화하고 JSON-RPC 2.0, method, id, params shape를 최종 검증한다. JSON-RPC version, 표준 오류 번호와 MCP method 이름은 MCP Java SDK 2.0 상수를 참조한다.McpMethodHandlerRegistry가 method를 명시적 handler에 연결한다.tools/list는ToolRegistryService의 in-memory snapshot에서 실행 metadata를 얻는다. 요청 경로는 Redis를 호출하지 않으므로 Redis 장애·지연이 응답에 영향을 주지 않으며, snapshot이 비어 있는 기동 직후에만 Tool catalog provider를 한 번 조회한다. 이후ToolsListHandler가 MCP SDK의Tool과ListToolsResult로 변환한다. local 기본 구성은 Tool Service 매니페스트를 먼저 조회하고, 최초 실패 시 bundle별 local manifest sample을 cold-start fallback으로 사용한다. 운영은 이 배포가 보는 Tool Service 매니페스트의 사용 가능한 성공본만 원천으로 사용한다.tools/call은ToolsCallHandler가 표준 MCP의params.name과 object인params.arguments를 검증하고 추출한다.ToolExecutionService가 표준 Tool name으로 metadata를 확정하고 argument schema를 검증한다.ToolRoutingService는 snapshot에 저장된 정확한 Tool endpoint와 metadata timeout으로 HTTP 요청을 만든다. Agent Builder가 보낸arguments객체는 JSON raw body로 전달하며 MCP가 Tool을 대체 선택하지 않는다.arguments의 어떤 field도 outbound URL 선택에 사용하지 않는다. Portal registry는 Tool Server의serviceDomain과manifestPath만 제공하고, Tool별 실행 endpoint는 Tool Server manifest의 top-levelendpoint또는_meta.endpoint에서 가져온다. manifest endpoint가 절대 HTTP(S) URL이면 Tool Server가 제공한 실행 주소 원천으로 허용하고, 상대 경로이면 Portal registry의serviceDomain뒤에 붙인다.HttpToolClient가 JDK 공유 HTTP client의 connection pool을 사용해 correlation 헤더와 함께 POST를 실행한다.X-Caller-IP와X-Caller-Host는 기동 시 Downward API의POD_IP·POD_NAME을 우선 사용하고, 값이 없을 때만 로컬 host를 한 번 조회해 프로세스 수명 동안 재사용한다. Portal Registry와 Tool manifest 조회도 별도의 공유 JDK HTTP client를 사용한다. arguments는 JSON body로 전달하며 Tool read timeout은 metadata timeout과 요청 전체 deadline의 남은 시간 이하로 제한한다. Authorization 전달은 설정으로 통제한다.- Tool 응답은 요청 payload와 분리해
response.data만 사용한다. plain text는 그대로, JSON object/array는 compact JSON string으로 MCP SDKCallToolResult/TextContent의result.content[0].text에 넣고 outer JSON serializer가 escaping을 처리한다. 호출 소요 시간(ms)은result.content[0]._meta.searchTime으로 반환하고, 정상 결과에도isError: false를 명시한다. Tool 실행·timeout·권한 오류는 JSON-RPC error가 아니라isError: trueresult로 변환한다. JSON-RPC envelope/params/method 및 서버 구성 오류는 최상위 JSON-RPCerror로 반환한다. - local과 운영 모두 같은
namelookup, endpoint/timeout, inputSchema validation 경로를 사용한다. - Agent Builder가
Accept: application/json, text/event-stream을 보내도 서버는 단일application/jsonJSON-RPC response를 반환한다. filter는 status와 소요 시간을mcp_http_response_completed로그로 남기며 응답 body는 저장하지 않는다. - 모든 예외는
JsonRpcException/McpExceptionHandler에서 표준 error로 변환하고, filter가 ThreadLocal을 반드시 정리한다.
주요 클래스별 책임
| 클래스 | 패키지 | 책임 |
|---|---|---|
McpController |
transport/http |
mcp.endpoint-path의 단일 공개 endpoint, parser/handler 연결, notification 202와 initialize UUID header 선택 |
McpRequestContextFactory |
transport/http |
호출자 헤더 5종 추출. correlation 값 형식 검증, 사원 식별자는 해석하지 않고 주입 위험 문자만 차단 |
McpRequestContextHolder |
context |
요청 수명 ThreadLocal 저장; 세션 저장소가 아님 |
JsonRpcRequestParser |
jsonrpc |
JSON-RPC envelope shape 검증과 내부 request 정규화 |
McpMethodHandlerRegistry |
method |
Handler 전략과 method dispatch를 한 경계에서 관리 |
ToolRegistryService |
registry |
요청 경로(memory 전용)와 배경 갱신 경로(provider 조회 후 memory→Redis 저장) 분리, single-flight refresh, 공유 cache warm start |
ToolsListHandler |
method |
실행 metadata를 MCP 공개 Tool로 변환하고 _meta 제거. 공개 필드 목록은 계약 v0.3이 정본 |
LocalFileToolRegistryClient |
registry |
매니페스트 조회를 끈 local profile에서 legacy JSON/manifest fixture를 읽어 테스트 Tool 목록을 제공 |
ToolBundleDiscovery |
registry |
구현상 N개 Tool Service 매니페스트를 병렬 조회·검증하고 bundle별 last-good 상태를 유지. 최초 원격 조회 실패 시에만 설정된 local manifest fallback을 사용하며, 운영 배포는 1개 Bundle만 사용 |
ToolBundleRegistryClient |
registry |
구현상 모든 bundle의 사용 가능한 성공본을 중복·총량 검증 후 하나의 snapshot으로 병합. 운영 배포에서는 단일 Bundle 결과를 채택 |
RedisToolRegistryCache |
registry |
best-effort Redis snapshot, 실제 read/write 실패를 cache miss로 격리 |
ToolRegistryPreloader |
registry |
기동 preload만 수행; 실패 시 애플리케이션 생존 |
ToolArgumentValidator |
execute |
기존 required/type 오류 계약을 보존하고 MCP SDK JSON Schema 2020-12 검증 적용 |
ToolExecutionService |
execute |
이름 기반 metadata 해석, argument validation, 단일 Tool 실행, HTTP 경계 로그와 오류 mapping |
ToolRoutingService |
execute |
단일 POST endpoint와 timeout 확정, 기본 URI 검증 |
ToolClient |
toolclient |
Tool 호출 port와 해당 경계의 ToolRequest/ToolResponse/실패 타입 소유 |
HttpToolClient |
toolclient |
HTTP 호출, headers, timeout, JSON/text 응답 처리 |
ToolBundleStatusEndpoint |
observability |
management port의 Actuator toolBundles 상태 조회 |
ToolCatalogHealthIndicator |
observability |
readiness group 판정. 첫 조회 시도가 끝나고 usable in-memory snapshot이 있을 때만 UP |
McpExchangeFilter |
transport/http |
설정된 MCP endpoint 전처리, request 크기 제한, correlation, HTTP 요청·응답 경계 로그 |
McpProtocolVersionValidator |
transport/http |
initialize 이후 HTTP MCP-Protocol-Version의 지원 여부 검증; 서버 상태를 저장하지 않음 |
TraceLogger |
observability |
context의 guid/requestId를 직접 포함하는 최소 key=value 경계 로그. 사원 식별자는 기록하지 않음 |
McpExceptionHandler |
transport/http |
JSON parse, JSON-RPC, 예상 밖 오류의 표준 response 변환 |
Spring Boot 3.5가 관리하는 Jackson 2 databind 모델과 annotation API는 com.fasterxml.jackson.* namespace를 사용한다. Registry 응답의 unknown field 무시는 회귀 테스트로 검증한다.
MCP Java SDK는 protocol 상수, 표준 result 모델과 JSON Schema validator에만 사용한다. SDK/Spring AI MCP
Starter와 transport는 활성화하지 않으며 기존 /mcp, 보안, correlation, Registry, Tool 실행 경계를 유지한다.
상세 도입 범위와 업그레이드 검증 기준은 MCP Java SDK 선택적 도입 설계를 따른다.
소스 구성 원칙
- Spring component, 외부 adapter, 교체 가능한 port 구현은 책임별 독립 파일로 유지한다.
- 특정 서비스나 port에서만 의미가 있는 immutable record, enum, 예외는 소유 타입 안에 둔다.
ToolClient가 request/response/failure 타입을,ToolExecutionService가 실행 result를 소유한다.McpMethodHandlerRegistry는 handler 계약을,ToolRegistryClient는 원천 목록 조회 port를 소유한다.- bean이나 동작을 제공하지 않는 빈 configuration class는 두지 않는다. Spring Boot auto-configuration과 application class의 scheduling 설정을 그대로 사용한다.
- 파일 수를 줄이기 위해 서로 다른 서비스 책임을 합치지는 않는다. transport, Registry, execution, Tool client, observability 경계는 계속 분리한다.
패키지 경계
| 패키지 | 책임 |
|---|---|
transport/http |
HTTP로 들어오고 나가는 경계 전부. filter, controller, 본문 wrapper, protocol version 검증, context 생성, 오류 응답 변환 |
context |
요청 수명 동안 공유하는 값과 ThreadLocal holder. 전송 방식을 모른다 |
jsonrpc |
JSON-RPC envelope 모델과 파싱·오류 코드 |
method |
MCP method별 handler와 dispatch |
execute / toolclient |
단일 Tool 실행(응용 서비스)과 outbound port·adapter |
registry |
Tool 목록의 원천 조회, 병합, 캐시 |
observability |
경계 로그, health indicator, Actuator 상태 endpoint |
jakarta.servlet 의존은 transport 패키지 안에서만 허용한다. MCP는 stdio 등 다른 transport를 가질 수 있는
프로토콜이므로, 서블릿 타입이 이 경계 밖으로 새면 전송 방식이 응용 계층에 굳어진다.
이 규칙은 PackageBoundaryContractTest가 강제한다.
Stateless 보장
HttpSession/Spring Session 의존성이나 API를 사용하지 않는다.mcp-session-id는 요청 context와 downstream correlation header에만 사용한다.- initialize 응답의
Mcp-Session-Id는 Agent Builder가 이후 요청에 전달하는 opaque correlation 값이다. 서버는 이를 발급했는지·notification을 받았는지·session readiness를 저장하거나 검증하지 않는다. - Tool metadata의 in-memory cache는 업무/사용자 세션 상태가 아닌 재구성 가능한 read-only snapshot이다.
- replica가 달라져도 동일 요청 계약을 수행할 수 있다.
- HTTP와 Tool 호출 경계 로그는 requestId/guid로 연결하지만 업무·세션 상태를 저장하지 않는다.
HTTP 경계 로그
- 모든 MCP method(
initialize,notifications/initialized,tools/list,tools/call)는 설정된 공개 path의 POST body에 있는 JSON-RPCmethod로 식별된다. Filter는 이 값과 HTTP status, 소요 시간만 요청·응답 경계 로그에 남긴다. guid가 있으면 end-to-end 흐름 전체에 그대로 사용하고, 없으면 UUID를 생성한다.x-request-id도 있으면 그대로 사용하고 없으면 생성해 response header와 downstream Tool header에 전파한다.- MDC는 사용하지 않는다.
TraceLogger가McpRequestContextHolder에서 guid와 requestId를 읽어 각 메시지에 직접 포함한다. - Authorization/Cookie/API key, session 식별자, 요청·응답 body는 logger에 남기지 않는다.
employee-no와virtual-employee-no는 암호문이라도 로그에 남기지 않는다. 개인 식별자이며, 암호화는 저장·전송 보호이지 로그 기록 허가가 아니다.- 수신 request body는
mcp.trace.max-body-bytes로 제한한다. 제한을 넘으면 Controller에 전달하지 않고 JSON-RPCInvalid Request로 종료한다. 이 설정은 로그 capture 크기가 아니라 입력 경계 보호 정책이다. - 현재 구현은 애플리케이션 로그만 제공하며 불변 감사 저장소가 아니다. 보존·위변조 방지·재처리가 필요한 규제 감사 요건이 확정되면 그때 별도 durable sink를 설계한다.
- 응답을 쓰는 중
IOException이 나면mcp_http_response_undeliverableevent로 남긴다. 대개 Agent Builder가 먼저 연결을 끊은 경우이며, 이 로그가 없으면 결과 유실 자체를 관측할 수 없다.
요청 시간 예산
Agent Builder는 응답을 300초까지만 기다리고 연결을 끊는다. MCP의 예산은 그보다 짧아야 한다. 같거나 길면 MCP가 응답을 완성해도 받을 상대가 이미 사라진 뒤다.
| 계층 | 값 | 근거 |
|---|---|---|
| Agent Builder 대기 한도 | 300초 | 외부 제약. Agent가 MCP를 호출한 시점부터 잰다 (ADR-0004) |
MCP 요청 전체 예산 request-deadline-millis |
270초 | 30초 여유. MCP 시계는 요청이 도착한 뒤 출발하므로 그만큼 더 안전하다 |
Tool 개별 timeout 상한 maxToolTimeoutMillis |
30초 | 매니페스트 선언값의 상한. 실질적으로 이 값이 요청 시간을 결정한다 |
Tool 호출 직전마다 remainingMillis()로 남은 예산을 계산해 read timeout을 그 이하로 깎는다.
따라서 Tool 하나가 자기 timeout을 다 써도 요청 전체 예산을 넘지 않는다.
270초는 실제로는 거의 도달하지 않는 backstop이다. 한 요청은 Tool을 정확히 하나만 실행하고,
그 Tool의 timeout은 30초로 상한이 걸려 있다. 따라서 정상 경로의 최대 소요는 연결 1초 + 읽기 30초
수준이다. 270초가 의미를 갖는 것은 maxToolTimeoutMillis를 크게 올릴 때뿐이며,
그때는 이 표 전체를 다시 계산해야 한다.
이 관계 때문에 graceful shutdown 시간도 300초가 아니라 실질 상한(약 31초)에 맞춘다.
spring.lifecycle.timeout-per-shutdown-phase(40초) < terminationGracePeriodSeconds(45초) 순서를 지켜,
진행 중인 Tool 호출이 배포 중에 잘려 부작용만 남는 상황을 줄인다.
연결이 이미 끊긴 뒤 Tool 결과가 도착하는 경우는 완전히 막을 수 없다. MCP는 결과를 저장했다가
나중에 전달하지 않는다(stateless, ADR-0001).
중복 실행 방지는 Tool Service의 책임이다. retry에서 같은 guid를 재사용해 멱등성 키로 삼을지는
미합의 항목이며, 합의 전에는 MCP가 이를 보장한다고 가정하지 않는다.
Protocol version 협상과 검증
- 서버는
mcp.protocol.supported-versions와mcp.protocol.preferred-version으로 지원 버전을 명시적으로 관리한다. preferred version은 반드시 supported versions에 포함되어야 한다. initialize응답은 요청의 JSON-RPCid를 그대로 반환하며, preferred version과serverInfo(name/title/version),capabilities.tools.listChanged=false를 제공한다. Agent routing hint는 선택 정보이므로 Portal 또는 Tool Server 조회가 실패하면_meta.toolServers만 생략하고 기본 initialize 응답은 정상 반환한다. 성공한 routing manifest는 필드 구조를 유지하면서 Jackson 전용 tree가 아닌 Map/List 기반 일반 JSON 값으로 바꿔 HTTP converter 구현과 분리한다.- Agent routing hint는 route/bundle별 in-memory last-good snapshot으로 관리한다. ApplicationReady preload가 Portal endpoint와 Tool manifest를 확보한 뒤 모든 route의
/tool-service-manifest도 미리 적재하므로 최초 initialize는 원격 호출 없이 snapshot을 사용한다.mcp.agent-routing-hints.refresh-ttl-seconds안의 initialize 요청도 Tool Server를 다시 호출하지 않으며, TTL 만료 후 첫 요청 또는 Portal bundle 구성 변경 시에만 갱신한다. 일부 bundle 갱신 실패는 기존 성공본을 유지하고, 한 번도 성공하지 못한 bundle만 응답에서 제외한다. - 이 서버는 stateless이므로 협상 결과를 session에 저장하지 않는다.
initialize이후 Agent Builder는 모든 MCP HTTP 요청에MCP-Protocol-Version: <initialize 응답 protocolVersion>을 포함해야 하며, 서버는 매 요청을 독립적으로 검증한다. - header가 누락되거나 지원하지 않는 값이면 JSON-RPC error가 아닌 HTTP
400 Bad Request를 반환한다. 오류 body는error,message,supportedVersions,guid를 포함해 호출자가 올바른 header를 진단할 수 있게 한다.
Initialize lifecycle correlation
initialize의 JSON-RPC result는 server protocol/capability 정보를 제공하고, HTTP response headerMcp-Session-Id에는 새 UUID를 제공한다.- Agent Builder는 이 값을
notifications/initialized,tools/list,tools/call의Mcp-Session-Idheader에 보낸다. 각 HTTP 요청은 별도x-request-id를 유지한다. - Agent Builder는 MCP 2025-11-25 lifecycle에 따라
notifications/initialized를 보낸다. 서버는 이를 저장하거나 이후 요청의 readiness gate로 사용하지 않는다. InitializedNotificationHandler는 id 없는 notification을 HTTP 202으로 수용한다. 이는 Tool 실행 준비 상태를 메모리에 세우는 동작이 아니므로 replica 간 affinity가 필요 없다.
Tool metadata 갱신 장애 시나리오
요청 경로는 memory만 읽으므로 Redis 상태가 등장하지 않는다.
| Memory | Tool Service aggregate | 결과 |
|---|---|---|
| hit | 무관 | memory 반환. Redis를 호출하지 않는다 |
| miss (기동 직후) | 확정 가능 | provider 직접 조회 후 memory 저장 |
| miss (기동 직후) | 확정 불가 | Redis에 다른 replica의 snapshot이 있으면 채택, 없으면 -32003 Tool registry unavailable |
readiness는 첫 discovery 시도 완료와 usable in-memory snapshot을 모두 요구한다. 원천 조회가 실패해도 기존
memory 또는 Redis last-good을 채택했다면 UP이며, 둘 다 없어 -32003만 반환할 상태라면 DOWN이다. 따라서
rolling update 중 새 Pod이 빈 catalog로 기존 정상 Pod을 대체하지 않는다. 정상 매니페스트가 빈 Tool 목록을
반환한 경우에는 그 빈 목록도 성공적으로 확정된 전체 상태이므로 usable snapshot이다.
배경 갱신 경로의 동작은 다음과 같다.
| Tool Service aggregate | 기존 memory | Redis | 결과 |
|---|---|---|---|
| 확정 가능 | 무관 | 무관 | memory 갱신 후 Redis 저장(best-effort). 성공한 결과만 저장한다 |
| 확정 불가 | hit | 무관 | 현재 memory 유지. 더 오래된 Redis 값으로 덮어쓰지 않는다 |
| 확정 불가 | miss | hit | Redis의 공유 last-good snapshot으로 warm start |
| 확정 불가 | miss | miss/장애 | -32003을 반환하고 다음 TTL 만료 요청에서 재시도 |
각 bundle은 이번 성공본 또는 직전 성공본이 있어야 aggregate를 확정할 수 있다. 조회 실패는 Tool 삭제로 해석하지 않으며, 성공한 매니페스트에서 빠진 경우에만 삭제를 반영한다. 이름 충돌이나 총량 상한 초과도 전체 갱신 실패로 처리한다. 동시에 여러 refresh가 들어오면 single-flight로 하나의 원천 조회 결과를 공유한다.
캐시에서 Tool을 찾지 못하면 stale snapshot 가능성을 고려해 원천을 한 번 더 조회한 뒤 -32001을 결정한다. 반대로 캐시에는 있던 Tool이 실행 시점에 upstream 404 또는 410을 반환하면 삭제된 Tool을 아직 들고 있는 stale snapshot 신호로 보고, 현재 요청은 Tool 실행 실패로 유지한 채 해당 route의 manifest refresh를 best-effort로 즉시 시도한다. 같은 route에서 삭제된 Tool 호출이 몰릴 때 Tool Server manifest 호출이 폭증하지 않도록 짧은 cooldown을 적용한다.
Redis는 요청 경로의 의존성이 아닌 선택적인 warm-start cache다. Tool snapshot은 route별 key(key-prefix:identity:v2:route:{routeToken})로 분리해 서로 다른 route의 Tool 목록이 섞이지 않게 하며, Portal registry fallback key와도 분리한다. key 형식, TTL, 공유 범위, 고가용성·보안 정책은
아직 확정하지 않았으며 extension-points.md에서 합의한다. 현재 구현값은
운영 계약이나 장기 설계 결정이 아니다.
Local Tool manifest fallback
로컬 Agent Builder 연동 검증도 실제 Tool Service와 같은 매니페스트 조회 흐름을 먼저 사용한다. application-local.yml의 bundle URL을 조회하고, 처음 조회가 실패했을 때만 fallback-manifest-file의 manifest sample을 snapshot으로 채택한다. 기본 sample은 프로젝트 루트의 config/local-core-tools-manifest-sample-v1.json이다. 원격 조회가 이후 성공하면 즉시 원격 목록으로 교체하며, 이미 확보한 원격 성공본은 local sample로 덮어쓰지 않는다.
Portal Registry and Tool manifest refresh
로컬 검증에서는 mcp.portal.registry-url을 file:./config/local-toolserver-info-sample-v1.json 같은 Spring resource location으로 지정할 수 있다. 이 경우 MCP는 기동 preload와 요청 시점 TTL refresh에서 Portal HTTP API를 호출하지 않고 프로젝트 안의 registry JSON을 읽는다. 파일에서 확보한 endpoint 목록 이후의 Tool Server tool-manifest TTL 조회, route별 in-memory snapshot 갱신, Redis fallback 규칙은 Portal API를 사용할 때와 동일하다.
Portal Registry를 사용하는 구성에서는 포털을 route별 Tool Server 목록의 원천으로만 사용한다. MCP는 기동 preload 때 포털 registry API를 먼저 호출해 serviceDomain과 manifestPath를 확보한 뒤 Tool Server tool-manifest를 조회한다. 이후에는 scheduler polling 없이 요청 시점에 mcp.portal.refresh-ttl-seconds와 mcp.registry.refresh-ttl-seconds를 확인하고, 만료된 원천만 갱신한다. Portal TTL 만료 시 동시 요청이 들어와도 lock 안에서 원격 I/O를 수행하지 않는 single-flight로 Portal 조회 한 건만 실행하고 나머지 요청은 같은 결과를 공유한다. Portal route는 enabled: "Y"이면 활성 route로 채택하고 "N"이면 요청 대상에서 제외한다. 비활성화되거나 Portal 응답에서 삭제된 route는 Tool memory snapshot과 route별 TTL 상태도 즉시 제거하되, 현재 비활성 정책인 Redis에는 접근하지 않는다. 활성 route의 Tool Service는 enabled: "Y"인 항목만 호출하며, 모두 "N"이어도 route 자체는 유지하고 빈 Tool 목록을 제공한다. 활성 Tool Service의 endpoint 메타데이터가 잘못되면 해당 route의 기존 정상 endpoint snapshot을 유지하고, 최초 등록 route라면 빈 Tool 목록으로 격리한다. routeRevision 또는 endpoint 구성이 바뀐 route는 Tool manifest TTL이 남아 있어도 그 route만 즉시 다시 조회한다. Tool Server 내부 tool/schema/revision/endpoint 변경은 manifest TTL 조회 결과를 route별 in-memory snapshot에 다시 병합하면서 처리한다. 존재하지 않는 Tool 이름이 반복 호출될 때는 route별 5초 cooldown 안에서 manifest 즉시 갱신을 한 번만 허용한다. Portal API 조회가 실패하면 이미 확보한 in-memory Tool Server snapshot을 유지하며, cold start처럼 memory가 비어 있을 때만 mcp.redis.portal-registry-key의 Redis registry JSON을 fallback으로 읽는다. Redis fallback도 실패하면 Tool Server 원천을 확보하지 못한 것으로 처리하고 다음 TTL 만료 요청에서 재시도한다.
노출 대상 Tool은 그 파일이 정의한다. 목록을 이 문서에 옮겨 적지 않는다. 파일의 공개 필드는 그대로 보존하고 _meta와 endpoint 실행 정보만 제거해 tools/list에 내보낸다. fallback도 원격 매니페스트와 같이 top-level endpoint 또는 _meta.endpoint를 내부 실행 endpoint로 사용한다.
이 fixture는 연동 확인용이며 실제 고객·계약·수납·지급 데이터를 담지 않는다.
운영 profile에서는 ToolBundleDiscovery와 ToolBundleRegistryClient만 metadata 원천으로 활성화한다. MCP 배포별 mcp.bundles가 Tool Service의 매니페스트와 실행 주소를 선언한다. 운영 Helm 설정에는 fallback 파일을 넣지 않는다. Tool Service는 표준 name을 소유하고, MCP는 자기 Bundle 안에서 형식·설정된 namePrefix·중복을 검증하되 이름을 재작성하지 않는다. 서로 다른 MCP 배포 간 이름의 전역 유일성은 Tool Service·플랫폼의 변경 절차가 보장한다. Redis는 선택적인 공유 last-good cache일 뿐 Tool 목록의 원천이 아니다.
mcp.bundles는 N개를 지원하지만 운영 배포에서는 항상 한 항목이다. MCP 배포 하나가 Tool Service 하나만 보기로 했기 때문이다(ADR-0007). 대상을 늘리는 방법은 이 목록을 늘리는 것이 아니라 MCP 배포를 하나 더 만드는 것이다. 그래야 등급이 다른 Tool Service의 조회 실패가 서로의 카탈로그 갱신을 막지 않는다. 다중 bundle 병합 코드는 유지하되 Helm Chart가 1개로 잠그고 HelmDeploymentContractTest가 그 사실을 검사한다.
각 배포는 같은 환경 host의 고유 publicPath를 가진 OpenShift Route로 노출된다(ADR-0009). Route는 path로 Service만 선택하고 컨테이너가 같은 값을 mcp.endpoint-path로 직접 처리한다. Java 애플리케이션에는 route table이나 다중 Registry를 추가하지 않는다. Deployment·snapshot·readiness·connection pool은 path별로 분리되고, 공유되는 장애 지점은 OpenShift ingress와 DNS다.
운영 상태는 외부 ingress가 아니라 management port(기본 9090)의 GET /actuator/toolBundles로 확인한다.
변경 시 검증 경계
- MCP envelope/method 변경: adapter → handler registry → handler 직렬화 테스트
tools/call변경: handler params → Registry metadata → argument validator → routing → Tool client → error mapping- Tool metadata 변경: 매니페스트 역직렬화·bundle 검증 → aggregate 확정 → memory/Redis fallback → 요청 시점 TTL refresh
- correlation 변경: header extractor → filter/context 정리 → response/downstream header
- 공개 path/배포 변경: topology의 path 유일성 → Route host/path/Service → ConfigMap endpoint → Controller·Filter → Agent Builder 등록 URL
- 최종 확인:
.\gradlew.bat clean check,bootJar, 실행 JAR의 initialize → notification → tools/list 흐름
Tool list change notification
initialize는 capabilities.tools.listChanged=false를 선언한다(InitializeHandler의 ServerCapabilities.builder().tools(false)). 현재 HTTP 단발 응답 transport가 notification을 push할 수 없으므로, 보내지 못하는 능력을 선언하지 않는 쪽을 택한 것이다.
내부적으로는 배경 Registry refresh가 기존 route snapshot과 다른 Tool 목록을 성공적으로 확보하면 ToolListChangedEvent가 표준 notifications/tools/list_changed JSON-RPC notification envelope를 만든다. 이 이벤트는 아직 소비되지 않는다. SSE/Streamable HTTP 전송 계층이 추가되면 이 이벤트를 route별 Agent 연결에 전달하고 선언을 true로 바꾼다. 그때까지 Agent Builder는 자체 주기로 tools/list를 다시 호출해야 한다.
- 임시 검증에서 Agent↔MCP와 MCP↔Tool Service payload를 확인해야 하면
mcp.trace.payload-logging-enabled=true를 켠다. 이 로그는 JSON 한 줄 형태로 요청·응답 본문을 남기므로 운영 기본값은 false이며, 검증 후 즉시 꺼야 한다.