Files
dap-was-dapms/docs/contracts/tool-service-mcp/tool-list-loading-guide.md
koseokmin cb29b192b4 docs를 저장소로 되돌리고 계약 예제를 복원한다
5cfb8a1이 .gitignore에 docs/를 넣고 68개 파일을 지웠다. 그런데
AgentBuilderContractExampleTest, ToolBundleContractExampleTest,
ArchitectureDocumentContractTest는 docs/ 아래 계약 예제와 architecture
문서를 입력으로 직접 읽는다. 그 결과 clean clone에서 테스트 10건이
입력을 찾지 못해 실패했다.

제외 범위를 원래 의도대로 좁힌다. 에이전트 산출물(AGENTS.md, .agents/,
.codex/, docs/superpowers/)은 계속 제외하고 저장소 문서는 추적한다.

문서는 삭제 직전 상태(3de052a)를 기준으로 복원하고, 그 위에 main 코드와
대조해 어긋난 부분을 고쳤다.

- ADR-0007을 Superseded로 바꾸고 ADR-0013을 새로 쓴다. route당 Tool
  Service N개가 최종안이며, PortalToolRegistryClient가 이미 route별로
  N개를 유지하고 있는데 ADR-0007은 "bundles는 항상 한 항목"을 Accepted
  상태로 주장하고 있었다. ADR-0009 결정 4도 부분 대체한다.
- ADR-0008 파일 헤더가 Accepted였으나 ADR-0009가 이미 대체한 상태였다.
- 6078852의 endpoint 소유권 반전이 반영되지 않은 서술을 계약 v0.2,
  bundle 설정 예제, Tool 적재 안내에서 고친다.
- Portal registry 계약 v0.1과 route key 규약을 새로 문서화한다. 둘 다
  구현은 있는데 계약 문서가 없었다.
- MCP SDK 2.0.0 SBOM을 추가한다.

번호 주석: ADR-0011과 0012는 feature/mcp-integration이 Tool inputSchema
정책에 쓰고 있어 비워 둔다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:27:56 +09:00

8.9 KiB

안내: Tool 목록 최초 적재와 tools/list 노출 흐름

상태: 안내 문서 · 기준: 현재 MCP 서버 구현 · 대상: Tool Service 개발 파트

이 문서는 현재 동작을 이해하기 위한 안내다. 외부 wire 계약의 정본은 Tool Service-MCP Bundle 조회 계약 v0.2다. 규범 내용은 여기에 복제하지 않고 계약을 가리킨다. 복제본은 조용히 낡기 때문이다.

먼저 구분할 것

Tool Service가 MCP 표준 tools/list를 직접 구현하는 구조가 아니다. Tool Service는 아래의 내부 매니페스트 endpoint를 제공하고, MCP Server가 이를 읽어 Agent Builder용 표준 tools/list 응답으로 변환한다.

Tool Service -- GET /tool-manifest --> MCP Server -- JSON-RPC tools/list --> Agent Builder

현재 운영 배포는 MCP 하나가 Tool Service Bundle 하나를 본다. 구현은 호환 목적으로 여러 Bundle의 병합도 지원하지만, Tool 개발 파트는 자기 Bundle 하나의 매니페스트만 제공하면 된다.

1. 최초 적재는 구현되어 있는가?

구현되어 있다. Spring 애플리케이션이 준비되면 ToolRegistryRefreshScheduler.preload()가 실행된다.

ApplicationReadyEvent
  -> 선택 Redis snapshot warm start (있으면 memory에 임시 적재)
  -> Tool Service manifest 즉시 조회
  -> 검증 성공한 전체 Tool 목록으로 memory snapshot 교체
  -> 선택 Redis cache 저장
  -> readiness 판단 가능

Redis는 선택 cache일 뿐이다. Redis가 없거나 실패해도 Tool Service 매니페스트 조회가 성공하면 정상 기동한다. 반대로 최초 조회와 선택 cache 모두 실패하면 애플리케이션 프로세스는 살아 있어도 usable Tool 목록이 없으므로 readiness는 DOWN이다. 다음 주기 조회에서 자동 재시도한다.

tools/list 요청이 기동 preload보다 먼저 들어와 memory snapshot이 비어 있으면, 요청 경로도 원천을 한 번 직접 조회해 cold start 공백을 메운다.

2. Tool Service에서 memory까지의 처리 순서

ToolBundleRegistryClient.fetchTools()
  -> ToolBundleDiscovery.discoverAll()
  -> GET {manifestUrl}
  -> bundleId / tools[] / Tool 필수 필드 검증
  -> ToolMetadata 생성 (endpoint는 Tool Service manifest의 top-level endpoint 또는 _meta.endpoint 사용)
  -> enabled=false Tool 제외
  -> immutable List<ToolMetadata>를 AtomicReference snapshot에 저장

실제 memory 저장소는 ToolRegistryServiceAtomicReference<List<ToolMetadata>>다.

  • 매니페스트 조회·검증에 성공했을 때만 새 immutable 목록으로 통째로 교체한다.
  • HTTP 오류, timeout, JSON 오류, 필수 필드 누락, 이름 규칙 위반은 기존 snapshot을 비우지 않는다.
  • Tool 하나만 걸러서 부분 반영하지 않는다. 매니페스트 하나가 잘못되면 해당 Bundle 전체를 거부한다.
  • 현재 운영은 Bundle 하나지만, 구현상 여러 Bundle이면 모두 사용 가능한 성공본이 있을 때만 하나의 snapshot을 교체한다.
  • 주기 refresh가 겹치면 single-flight로 하나의 원천 조회를 공유한다.

3. Agent Builder의 tools/list 요청은 어떻게 처리되는가?

Agent Builder는 공개 POST https://{mcpHost}{publicPath}로 JSON-RPC 요청을 보낸다. OpenShift Route는 해당 path의 MCP Service만 선택하고, 컨테이너가 같은 POST {publicPath}를 직접 처리한다.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

처리 경로는 다음과 같다.

McpController
  -> McpMethodHandlerRegistry
  -> ToolsListHandler
  -> ToolRegistryService.listTools()
  -> in-memory snapshot 읽기
  -> MCP SDK ListToolsResult 변환
  -> JSON-RPC result.tools 반환

memory snapshot이 이미 있으면 tools/list는 Tool Service나 Redis를 호출하지 않는다. 따라서 Tool Service가 잠시 느리거나 Redis가 장애여도 이미 적재한 목록은 바로 반환한다.

ToolsListHandler는 매니페스트 Tool 정의의 공개 필드만 MCP Tool로 만든다. _meta 안의 version, timeoutMillis, enabled와 MCP 내부의 endpoint는 절대 Agent Builder에 노출하지 않는다.

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "processing.contract.inquiry",
        "title": "계약 조회",
        "description": "계약번호로 계약의 기본 정보를 조회합니다.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "contractNo": { "type": "string" }
          },
          "required": ["contractNo"]
        },
        "annotations": {
          "readOnlyHint": true
        }
      }
    ]
  }
}

정확한 Agent Builder 응답 fixture는 tools-list-response.json을 따른다.

4. Tool Service가 구현할 매니페스트 endpoint

Tool Service는 MCP 배포 설정에 등록된 manifestUrl에 대해 다음을 반환한다.

GET /tool-manifest
Accept: application/json

200 OK
Content-Type: application/json

이 요청은 사용자 Tool 실행이 아니라 MCP의 배경 metadata 갱신이다. 따라서 guid, 사원 식별자, Mcp-Session-Id 같은 요청 상관·사용자 header를 기대하면 안 된다.

현재 구현은 conditional GET을 보내지 않으므로 Tool Service는 우선 항상 200 OK와 전체 JSON을 반환하면 된다. 304 Not Modified와 ETag는 계약상 선택 사항이지만 현재 MCP 구현 범위가 아니다.

응답 규칙

필드별 규칙과 검증 결과는 계약 v0.2 §5가 정본이다. 이 문서에 같은 표를 두지 않는다. 예전에 표와 예제를 복제했다가 endpoint 필수화를 놓쳐, MCP가 거부할 매니페스트를 안내하고 있었다.

실제 응답 예제는 examples/bundle-v0.2/manifest-response.json을 본다. 이 파일은 ToolBundleContractExampleTest가 직접 읽어 구현과 대조하므로, 문서 가운데 유일하게 조용히 어긋날 수 없는 사본이다.

Tool Service가 특히 놓치기 쉬운 세 가지만 짚는다.

  • endpoint는 필수다. top-level endpoint 또는 _meta.endpoint가 없으면 그 Tool 하나가 아니라 Bundle 전체가 거부된다. 상대 경로는 Portal registry의 serviceDomain 뒤에 붙고, 절대 URL은 HTTP(S) scheme과 host를 갖춰야 한다.
  • tools는 전체 상태다. 부분 목록이나 증분 변경을 보내면 안 된다.
  • credential·개인정보·업무 payload는 매니페스트에 넣지 않는다.

5. Tool Service가 알아야 할 실패 동작

조회 결과별 처리는 계약 v0.2 §6§7 병합 규칙이 정본이다.

Tool Service 입장에서 결론은 하나다. 한 번의 200 응답은 그 시점에 노출할 Tool의 완전한 목록이어야 한다. 조회가 실패하면 MCP는 직전 성공 목록을 유지하므로 실패가 Tool 삭제로 해석되지는 않는다. 그러나 성공한 응답에서 Tool이 빠지면 그것은 삭제로 반영된다. 부분 목록을 한 번 보내는 순간 그대로 카탈로그가 된다.

Tool Service 구현 체크리스트

  1. GET /tool-manifest를 MCP Server namespace에서만 접근 가능하게 제공한다.
  2. bundleId가 배포 설정의 Bundle id와 정확히 일치하는지 배포 전에 함께 확인한다.
  3. 모든 Tool에 고유한 표준 name, 비어 있지 않은 description, object 형태의 inputSchema, _meta.version을 넣는다.
  4. Tool을 숨기려면 _meta.enabled: false를 쓰거나 정상 전체 목록에서 제거한다. 둘의 변경 반영 시점은 다음 refresh다.
  5. credential·개인정보·업무 payload를 매니페스트에 넣지 않는다. 실행 주소는 6번을 따른다.
  6. Tool 자체 실행 endpoint는 manifest의 top-level endpoint 또는 _meta.endpoint에 선언한다. 상대 경로는 Portal registry의 serviceDomain 뒤에 붙고, 절대 HTTP(S) URL은 Tool Service가 제공한 실행 주소 원천으로 그대로 사용한다.

확인한 구현·테스트

  • 최초 preload·주기 refresh: ToolRegistryRefreshScheduler
  • in-memory snapshot·실패 fallback: ToolRegistryService
  • HTTP 매니페스트 조회·필드 검증: ToolBundleDiscovery
  • tools/list 공개 필드 변환·_meta 제거: ToolsListHandler
  • 회귀 테스트: ToolRegistryServiceTest, ToolBundleDiscoveryTest, ToolsListHandlerTest

자세한 field 정의와 실행 계약은 v0.2 계약, 실제 manifest 전체 예시는 manifest-response.json을 참고한다.