Implement project updates and refactor related functionality
All checks were successful
Deploy Gateway / deploy (push) Successful in 2m36s

This commit is contained in:
2026-09-17 20:36:08 +09:00
parent ec517e88ec
commit 87be952fd0
23 changed files with 1207 additions and 134 deletions

View File

@@ -32,7 +32,7 @@ MCP는 Agent Builder가 `tools/call`에 명시한 단일 Tool을 실행한다. T
8. `tools/call``ToolsCallHandler`가 표준 MCP의 `params.name`과 object인 `params.arguments`를 검증하고 추출한다.
9. `ToolExecutionService`가 표준 Tool name으로 metadata를 확정하고 argument schema를 검증한다. `ToolRoutingService`는 snapshot에 저장된 정확한 Tool endpoint와 metadata timeout으로 HTTP 요청을 만든다. Agent Builder가 보낸 `arguments` 객체는 JSON raw body로 전달하며 MCP가 Tool을 대체 선택하지 않는다.
10. `arguments`의 어떤 field도 outbound URL 선택에 사용하지 않는다. Portal registry는 Tool Server의 `serviceDomain``manifestPath`만 제공하고, Tool별 실행 endpoint는 Tool Server manifest의 top-level `endpoint` 또는 `_meta.endpoint`에서 가져온다. manifest endpoint가 절대 HTTP(S) URL이면 Tool Server가 제공한 실행 주소 원천으로 허용하고, 상대 경로이면 Portal registry의 `serviceDomain` 뒤에 붙인다.
11. `HttpToolClient`가 JDK 공유 HTTP client의 connection pool을 사용해 correlation 헤더와 함께 POST를 실행한다. arguments는 JSON body로 전달하며 Tool read timeout은 metadata timeout과 요청 전체 deadline의 남은 시간 이하로 제한한다. Authorization 전달은 설정으로 통제한다.
11. `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 전달은 설정으로 통제한다.
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는 저장하지 않는다.
@@ -53,7 +53,7 @@ MCP는 Agent Builder가 `tools/call`에 명시한 단일 Tool을 실행한다. T
| `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; 실패 시 애플리케이션 생존 |
| `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 검증 |
@@ -149,6 +149,7 @@ Tool 호출 직전마다 `remainingMillis()`로 남은 예산을 계산해 read
- 서버는 `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=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를 진단할 수 있게 한다.
@@ -181,7 +182,7 @@ rolling update 중 새 Pod이 빈 catalog로 기존 정상 Pod을 대체하지
| 확정 가능 | 무관 | 무관 | memory 갱신 후 Redis 저장(best-effort). **성공한 결과만 저장한다** |
| 확정 불가 | hit | 무관 | 현재 memory 유지. 더 오래된 Redis 값으로 덮어쓰지 않는다 |
| 확정 불가 | miss | hit | Redis의 공유 last-good snapshot으로 warm start |
| 확정 불가 | miss | miss/장애 | `-32003`을 반환하고 다음 주기에 재시도 |
| 확정 불가 | miss | miss/장애 | `-32003`을 반환하고 다음 TTL 만료 요청에서 재시도 |
각 bundle은 이번 성공본 또는 직전 성공본이 있어야 aggregate를 확정할 수 있다. 조회 실패는 Tool 삭제로 해석하지 않으며, 성공한 매니페스트에서 빠진 경우에만 삭제를 반영한다. 이름 충돌이나 총량 상한 초과도 전체 갱신 실패로 처리한다. 동시에 여러 refresh가 들어오면 single-flight로 하나의 원천 조회 결과를 공유한다.
@@ -196,9 +197,9 @@ Redis는 요청 경로의 의존성이 아닌 선택적인 warm-start cache다.
## Portal Registry and Tool manifest refresh
로컬 검증에서는 `mcp.portal.registry-url``file:./config/local-toolserver-info-sample-v1.json` 같은 Spring resource location으로 지정할 수 있다. 이 경우 MCP는 기동 preload와 주기 endpoint refresh에서 Portal HTTP API를 호출하지 않고 프로젝트 안의 registry JSON을 읽는다. 파일에서 확보한 endpoint 목록 이후의 Tool Server `tool-manifest` 주기 조회, route별 in-memory snapshot 갱신, Redis fallback 규칙은 Portal API를 사용할 때와 동일하다.
로컬 검증에서는 `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`를 조회한다. 이후에는 `mcp.registry.refresh-interval-seconds` 주기로 저장된 Tool Server 목록에 대해 manifest만 다시 조회하고, `mcp.portal.refresh-interval-seconds` 주기로 포털 registry만 별도로 갱신한다. 포털 `registryRevision`은 포털 응답 JSON 변경 로그와 Tool Server 목록 변경 진단에 사용하며, Tool Server 내부 tool/schema/revision/endpoint 변경 감지는 MCP의 manifest 주기 조회 결과를 route별 in-memory snapshot에 다시 병합하면서 처리한다. 요청 경로의 `tools/list``tools/call`은 계속 in-memory snapshot만 읽는다. Portal API 조회가 실패하면 이미 확보한 in-memory Tool Server snapshot을 유지하며, cold start처럼 memory가 비어 있을 때만 `mcp.redis.portal-registry-key`의 Redis registry JSON을 fallback으로 읽는다. 이 Portal registry fallback은 route 목록과 Tool Server 목록 확보용이고, route별 Tool snapshot Redis key는 이미 알고 있는 route의 마지막 Tool 목록 fallback에만 사용한다. Redis fallback도 실패하면 Tool Server 원천을 확보하지 못한 것으로 처리하고 다음 주기에서 재시도한다.
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로 사용한다.
@@ -216,7 +217,7 @@ Portal Registry를 사용하는 구성에서는 포털을 route별 Tool Server
- 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
- 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 흐름

View File

@@ -20,7 +20,7 @@ Tool Service -- GET /tool-manifest --> MCP Server -- JSON-RPC tools/list --> Age
## 1. 최초 적재는 구현되어 있는가?
**구현되어 있다.** Spring 애플리케이션이 준비되면 `ToolRegistryRefreshScheduler.preload()`가 실행된다.
**구현되어 있다.** Spring 애플리케이션이 준비되면 `ToolRegistryPreloader.preload()`가 실행된다.
```text
ApplicationReadyEvent
@@ -217,7 +217,7 @@ Content-Type: application/json
## 확인한 구현·테스트
- 최초 preload·주기 refresh: `ToolRegistryRefreshScheduler`
- 최초 preload: `ToolRegistryPreloader`
- in-memory snapshot·실패 fallback: `ToolRegistryService`
- HTTP 매니페스트 조회·필드 검증: `ToolBundleDiscovery`
- `tools/list` 공개 필드 변환·`_meta` 제거: `ToolsListHandler`