Initial commit
This commit is contained in:
227
docs/contracts/tool-service-mcp/TEMP-tool-list-loading-guide.md
Normal file
227
docs/contracts/tool-service-mcp/TEMP-tool-list-loading-guide.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 임시 안내: Tool 목록 최초 적재와 `tools/list` 노출 흐름
|
||||
|
||||
> 상태: **임시 학습 문서** · 기준: 현재 MCP 서버 구현 · 대상: Tool Service 개발 파트
|
||||
>
|
||||
> 이 문서는 현재 동작을 이해하기 위한 안내다. 외부 wire 계약의 정본은
|
||||
> [Tool Service-MCP Bundle 조회 계약 v0.2](protocol-v0.2-bundle-discovery.md)다.
|
||||
|
||||
## 먼저 구분할 것
|
||||
|
||||
Tool Service가 MCP 표준 `tools/list`를 직접 구현하는 구조가 아니다. Tool Service는 아래의 내부
|
||||
**매니페스트 endpoint**를 제공하고, MCP Server가 이를 읽어 Agent Builder용 표준 `tools/list` 응답으로
|
||||
변환한다.
|
||||
|
||||
```text
|
||||
Tool Service -- GET /tool-manifest --> MCP Server -- JSON-RPC tools/list --> Agent Builder
|
||||
```
|
||||
|
||||
현재 운영 배포는 MCP 하나가 Tool Service Bundle 하나를 본다. 구현은 호환 목적으로 여러 Bundle의
|
||||
병합도 지원하지만, Tool 개발 파트는 자기 Bundle 하나의 매니페스트만 제공하면 된다.
|
||||
|
||||
## 1. 최초 적재는 구현되어 있는가?
|
||||
|
||||
**구현되어 있다.** Spring 애플리케이션이 준비되면 `ToolRegistryRefreshScheduler.preload()`가 실행된다.
|
||||
|
||||
```text
|
||||
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까지의 처리 순서
|
||||
|
||||
```text
|
||||
ToolBundleRegistryClient.fetchTools()
|
||||
-> ToolBundleDiscovery.discoverAll()
|
||||
-> GET {manifestUrl}
|
||||
-> bundleId / tools[] / Tool 필수 필드 검증
|
||||
-> ToolMetadata 생성 (endpoint는 MCP 배포 설정의 baseEndpoint 사용)
|
||||
-> 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}`를 직접 처리한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
처리 경로는 다음과 같다.
|
||||
|
||||
```text
|
||||
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에 노출하지 않는다.
|
||||
|
||||
```json
|
||||
{
|
||||
"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](../agent-builder-mcp/examples/agentbuilder-v0.3/tools-list-response.json)을 따른다.
|
||||
|
||||
## 4. Tool Service가 구현할 매니페스트 endpoint
|
||||
|
||||
Tool Service는 MCP 배포 설정에 등록된 `manifestUrl`에 대해 다음을 반환한다.
|
||||
|
||||
```text
|
||||
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 구현 범위가 아니다.
|
||||
|
||||
### 응답 규칙
|
||||
|
||||
```json
|
||||
{
|
||||
"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 구현 체크리스트
|
||||
|
||||
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. Tool 자체 실행 endpoint는 별도로 `POST {baseEndpoint}/{toolName}`을 구현한다. manifest endpoint는 실행 endpoint가 아니다.
|
||||
|
||||
## 확인한 구현·테스트
|
||||
|
||||
- 최초 preload·주기 refresh: `ToolRegistryRefreshScheduler`
|
||||
- in-memory snapshot·실패 fallback: `ToolRegistryService`
|
||||
- HTTP 매니페스트 조회·필드 검증: `ToolBundleDiscovery`
|
||||
- `tools/list` 공개 필드 변환·`_meta` 제거: `ToolsListHandler`
|
||||
- 회귀 테스트: `ToolRegistryServiceTest`, `ToolBundleDiscoveryTest`, `ToolsListHandlerTest`
|
||||
|
||||
자세한 field 정의와 실행 계약은 [v0.2 계약](protocol-v0.2-bundle-discovery.md), 실제 manifest 전체 예시는
|
||||
[manifest-response.json](examples/bundle-v0.2/manifest-response.json)을 참고한다.
|
||||
Reference in New Issue
Block a user