10 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 저장소는 ToolRegistryService의 AtomicReference<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 구현 범위가 아니다.
응답 규칙
{
"bundleId": "insurance-processing",
"revision": "2026-08-03T01",
"tools": [
{
"name": "processing.contract.inquiry",
"title": "계약 조회",
"description": "계약번호로 계약의 기본 정보를 조회합니다.",
"inputSchema": {
"type": "object",
"properties": {
"contractNo": {
"type": "string",
"description": "조회할 계약번호입니다.",
"minLength": 1
}
},
"required": ["contractNo"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 3000,
"enabled": true
}
}
]
}
| 항목 | Tool Service 규칙 | MCP 처리 |
|---|---|---|
bundleId |
필수. MCP 배포 설정의 Bundle id와 정확히 일치 | 다르면 Bundle 전체 거부 |
revision |
선택. 변경 식별·운영 진단용 | 현재 호출 대상이나 공개 응답에는 사용하지 않음 |
tools |
필수. 이 Bundle의 전체 상태를 배열로 반환 | 정상 빈 배열은 “노출 Tool 없음”으로 채택 |
name |
필수. [A-Za-z0-9_./-]{1,64} 및 설정 namePrefix로 시작 |
위반 시 Bundle 전체 거부 |
description |
필수. Agent Builder가 Tool 선택에 사용할 설명 | 그대로 tools/list에 공개 |
inputSchema |
필수 JSON Schema object | 그대로 공개하고 tools/call 전에 검증 |
title, annotations |
선택 공개 정보 | 있으면 tools/list에 공개 |
_meta.version |
필수 | 내부 metadata로만 사용, 공개하지 않음 |
_meta.timeoutMillis |
선택 | 설정 상한 이하로 제한, 공개하지 않음 |
_meta.enabled |
선택, 기본 true |
false면 memory snapshot과 tools/list에서 제외 |
outputSchema |
현재 운영에서는 생략 | structuredContent 미지원 상태라 선언하지 않음 |
baseEndpoint, Tool 실행 URL, credential은 매니페스트에 넣지 않는다. MCP가 실제 호출할 주소는
배포 설정의 baseEndpoint에서만 결정한다. 매니페스트 안의 endpoint 성격 필드는 있어도 읽지 않는다.
5. Tool Service가 알아야 할 실패 동작
| Tool Service 매니페스트 결과 | MCP 동작 |
|---|---|
200 + 전체 검증 통과 |
새 목록을 memory에 교체하고 다음 tools/list부터 노출 |
200 + JSON/필수 필드/이름 오류 |
직전 성공 목록 유지. 첫 기동이면 목록을 만들지 못함 |
| timeout, 연결 실패, 4xx/5xx | 직전 성공 목록 유지. 첫 기동이면 readiness DOWN |
정상 tools: [] |
빈 목록을 정상 전체 상태로 채택 |
| Tool 하나만 제거한 정상 전체 manifest | 다음 갱신에 그 Tool도 목록에서 제거 |
따라서 Tool Service는 manifest 응답을 부분 목록이나 증분 변경으로 보내면 안 된다. 한 번의 200 응답은
그 시점에 노출할 Tool의 완전한 목록이어야 한다.
Tool Service 구현 체크리스트
GET /tool-manifest를 MCP Server namespace에서만 접근 가능하게 제공한다.bundleId가 배포 설정의 Bundle id와 정확히 일치하는지 배포 전에 함께 확인한다.- 모든 Tool에 고유한 표준
name, 비어 있지 않은description, object 형태의inputSchema,_meta.version을 넣는다. - Tool을 숨기려면
_meta.enabled: false를 쓰거나 정상 전체 목록에서 제거한다. 둘의 변경 반영 시점은 다음 refresh다. - 실행 주소·credential·개인정보·업무 payload를 매니페스트에 넣지 않는다.
- 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을 참고한다.