224 lines
26 KiB
Markdown
224 lines
26 KiB
Markdown
# AX HUB MCP Server 설계 설명
|
|
|
|
## 책임 경계
|
|
|
|
```text
|
|
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](decisions/ADR-0006-no-authentication-in-mcp.md)). 외부 호출자 제한은 Route IP allowlist가, Pod 직접 접근 제한은 NetworkPolicy가, 사용자 인증과 Tool 권한은 Agent Builder가, 사원 식별자 복호화와 업무 권한은 Tool Service가 담당한다. MCP는 요청 형식과 `inputSchema`만 검증한다.
|
|
|
|
## 전체 실행 흐름
|
|
|
|
1. Agent Builder가 배포별 공개 URL `https://{host}{publicPath}`을 호출한다. OpenShift Route는 host와 path로 MCP Service만 선택하고 원래 path를 컨테이너에 전달한다([ADR-0009](decisions/ADR-0009-container-handles-public-mcp-path.md)). 요청 body·header·Tool 이름은 이 선택에 관여하지 않는다.
|
|
2. 컨테이너의 `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 요청 식별자다.
|
|
3. filter는 크기가 제한된 repeatable request body에서 `method`만 관찰용으로 읽고 `mcp_http_request_received` 로그를 남긴다. body, header 값, credential은 로그에 저장하지 않는다.
|
|
4. `McpProtocolVersionValidator`가 `initialize`를 제외한 요청의 `MCP-Protocol-Version`을 supported versions와 대조한다. 누락·불일치는 Controller 진입 전 HTTP 400으로 종료한다.
|
|
5. `JsonRpcRequestParser`가 envelope를 정규화하고 JSON-RPC 2.0, method, id, params shape를 최종 검증한다.
|
|
JSON-RPC version, 표준 오류 번호와 MCP method 이름은 MCP Java SDK 2.0 상수를 참조한다.
|
|
6. `McpMethodHandlerRegistry`가 method를 명시적 handler에 연결한다.
|
|
7. `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 매니페스트의 사용 가능한 성공본만 원천으로 사용한다.
|
|
8. `tools/call`은 `ToolsCallHandler`가 표준 MCP의 `params.name`과 object인 `params.arguments`를 검증하고 추출한다.
|
|
9. `ToolExecutionService`가 표준 Tool name으로 metadata를 확정하고 argument schema를 검증한다. `ToolRoutingService`는 안전한 Tool name을 설정의 base endpoint 뒤에 붙여 `POST {baseEndpoint}/{toolName}` 요청과 metadata timeout을 만든다. Agent Builder가 보낸 `arguments` 객체는 JSON raw body로 전달하며 MCP가 Tool을 대체 선택하지 않는다.
|
|
10. `arguments`의 어떤 field도 outbound URL 선택에 사용하지 않는다. endpoint는 local catalog의 `_meta.endpoint` 또는 운영 배포 설정의 `baseEndpoint`에서만 가져오므로 Agent Builder 입력이나 Tool Service 매니페스트로 outbound 대상이 바뀌지 않는다.
|
|
11. `HttpToolClient`가 JDK 공유 HTTP client의 connection pool을 사용해 correlation 헤더와 함께 POST를 실행한다. arguments는 JSON body로 전달하며 Tool read timeout은 metadata timeout과 요청 전체 deadline의 남은 시간 이하로 제한한다. Authorization 전달은 설정으로 통제한다.
|
|
12. Tool 응답은 요청 payload와 분리해 `response.data`만 사용한다. plain text는 그대로, JSON object/array는 compact JSON string으로 MCP SDK `CallToolResult`/`TextContent`의 `result.content[0].text`에 넣고 outer JSON serializer가 escaping을 처리한다. 호출 소요 시간(ms)은 `result.content[0]._meta.searchTime`으로 반환하고, 정상 결과에도 `isError: false`를 명시한다. Tool 실행·timeout·권한 오류는 JSON-RPC error가 아니라 `isError: true` result로 변환한다. JSON-RPC envelope/params/method 및 서버 구성 오류는 최상위 JSON-RPC `error`로 반환한다.
|
|
13. local과 운영 모두 같은 `name` lookup, endpoint/timeout, inputSchema validation 경로를 사용한다.
|
|
14. Agent Builder가 `Accept: application/json, text/event-stream`을 보내도 서버는 단일 `application/json` JSON-RPC response를 반환한다. filter는 status와 소요 시간을 `mcp_http_response_completed` 로그로 남기며 응답 body는 저장하지 않는다.
|
|
15. 모든 예외는 `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](contracts/agent-builder-mcp/protocol-v0.3-streaming-policy.md#toolslist)이 정본 |
|
|
| `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로 격리 |
|
|
| `ToolRegistryRefreshScheduler` | `registry` | 기동 preload와 주기 refresh; 실패 시 애플리케이션 생존 |
|
|
| `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 선택적 도입 설계](mcp-java-sdk-adoption.md)를 따른다.
|
|
|
|
## 소스 구성 원칙
|
|
|
|
- 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-RPC `method`로 식별된다. 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-RPC `Invalid Request`로 종료한다. 이 설정은 로그 capture 크기가 아니라 입력 경계 보호 정책이다.
|
|
- 현재 구현은 애플리케이션 로그만 제공하며 불변 감사 저장소가 아니다. 보존·위변조 방지·재처리가 필요한 규제 감사 요건이 확정되면 그때 별도 durable sink를 설계한다.
|
|
- 응답을 쓰는 중 `IOException`이 나면 `mcp_http_response_undeliverable` event로 남긴다. 대개 Agent Builder가 먼저 연결을 끊은 경우이며, 이 로그가 없으면 결과 유실 자체를 관측할 수 없다.
|
|
|
|
## 요청 시간 예산
|
|
|
|
Agent Builder는 응답을 **300초**까지만 기다리고 연결을 끊는다. MCP의 예산은 그보다 짧아야 한다.
|
|
같거나 길면 MCP가 응답을 완성해도 받을 상대가 이미 사라진 뒤다.
|
|
|
|
| 계층 | 값 | 근거 |
|
|
|---|---|---|
|
|
| Agent Builder 대기 한도 | 300초 | 외부 제약. Agent가 MCP를 호출한 시점부터 잰다 ([ADR-0004](decisions/ADR-0004-execution-guardrails.md)) |
|
|
| 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](decisions/ADR-0001-stateless-execution-boundary.md)).
|
|
중복 실행 방지는 Tool Service의 책임이다. retry에서 같은 `guid`를 재사용해 멱등성 키로 삼을지는
|
|
[미합의 항목](extension-points.md)이며, 합의 전에는 MCP가 이를 보장한다고 가정하지 않는다.
|
|
|
|
## Protocol version 협상과 검증
|
|
|
|
- 서버는 `mcp.protocol.supported-versions`와 `mcp.protocol.preferred-version`으로 지원 버전을 명시적으로 관리한다. preferred version은 반드시 supported versions에 포함되어야 한다.
|
|
- `initialize` 응답은 요청의 JSON-RPC `id`를 그대로 반환하며, preferred version과 `serverInfo(name/title/version)`, `capabilities.tools.listChanged=true`를 제공한다.
|
|
- 이 서버는 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 header `Mcp-Session-Id`에는 새 UUID를 제공한다.
|
|
- Agent Builder는 이 값을 `notifications/initialized`, `tools/list`, `tools/call`의 `Mcp-Session-Id` header에 보낸다. 각 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`을 반환하고 다음 주기에 재시도 |
|
|
|
|
각 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](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
|
|
|
|
Portal Registry를 사용하는 구성에서는 포털을 route별 Tool Server endpoint 목록의 원천으로만 사용한다. MCP는 기동 preload 때 포털 registry API를 먼저 호출해 endpoint 목록을 확보한 뒤 Tool Server `tool-manifest`를 조회한다. 이후에는 `mcp.registry.refresh-interval-seconds` 주기로 저장된 endpoint 목록에 대해 manifest만 다시 조회하고, `mcp.portal.refresh-interval-seconds` 주기로 포털 registry만 별도로 갱신한다. 포털 `registryRevision`은 포털 응답 JSON 변경 로그와 endpoint 목록 변경 진단에 사용하며, Tool Server 내부 tool/schema/revision 변경 감지는 MCP의 manifest 주기 조회 결과를 route별 in-memory snapshot에 다시 병합하면서 처리한다. 요청 경로의 `tools/list`와 `tools/call`은 계속 in-memory snapshot만 읽는다. Portal API 조회가 실패하면 이미 확보한 in-memory endpoint snapshot을 유지하며, cold start처럼 memory가 비어 있을 때만 `mcp.redis.portal-registry-key`의 Redis registry JSON을 fallback으로 읽는다. 이 Portal registry fallback은 route 목록과 endpoint 목록 확보용이고, route별 Tool snapshot Redis key는 이미 알고 있는 route의 마지막 Tool 목록 fallback에만 사용한다. Redis fallback도 실패하면 endpoint 원천을 확보하지 못한 것으로 처리하고 다음 주기에서 재시도한다.
|
|
|
|
노출 대상 Tool은 그 파일이 정의한다. 목록을 이 문서에 옮겨 적지 않는다. 파일의 공개 필드는 그대로 보존하고 `_meta` 실행 정보만 제거해 `tools/list`에 내보낸다. fallback도 원격 매니페스트와 같이 설정된 `base-endpoint`에 요청 name을 path segment로 붙여 `tools/call`을 POST한다.
|
|
|
|
이 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](decisions/ADR-0007-one-mcp-per-tool-service.md)). 대상을 늘리는 방법은 이 목록을 늘리는 것이 아니라 MCP 배포를 하나 더 만드는 것이다. 그래야 등급이 다른 Tool Service의 조회 실패가 서로의 카탈로그 갱신을 막지 않는다. 다중 bundle 병합 코드는 유지하되 Helm Chart가 1개로 잠그고 `HelmDeploymentContractTest`가 그 사실을 검사한다.
|
|
|
|
각 배포는 같은 환경 host의 고유 `publicPath`를 가진 OpenShift Route로 노출된다([ADR-0009](decisions/ADR-0009-container-handles-public-mcp-path.md)). 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 → refresh scheduler
|
|
- 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=true`를 선언한다. 배경 Registry refresh가 기존 route snapshot과 다른 Tool 목록을 성공적으로 확보하면 `ToolListChangedEvent`가 표준 `notifications/tools/list_changed` JSON-RPC notification envelope를 만든다. 현재 HTTP 단발 응답 transport는 notification을 직접 push하지 않으며, SSE/Streamable HTTP 전송 계층이 추가되면 이 이벤트를 route별 Agent 연결에 전달하고 Agent Builder가 `tools/list`를 다시 호출한다.
|