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>
188 lines
8.9 KiB
Markdown
188 lines
8.9 KiB
Markdown
# 안내: 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는 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}`를 직접 처리한다.
|
|
|
|
```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 구현 범위가 아니다.
|
|
|
|
### 응답 규칙
|
|
|
|
필드별 규칙과 검증 결과는 [계약 v0.2 §5](protocol-v0.2-bundle-discovery.md#5-매니페스트-스키마)가
|
|
정본이다. 이 문서에 같은 표를 두지 않는다. 예전에 표와 예제를 복제했다가 `endpoint` 필수화를 놓쳐,
|
|
MCP가 거부할 매니페스트를 안내하고 있었다.
|
|
|
|
실제 응답 예제는 [examples/bundle-v0.2/manifest-response.json](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](protocol-v0.2-bundle-discovery.md#6-mcp의-조회-동작)과
|
|
[§7 병합 규칙](protocol-v0.2-bundle-discovery.md#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 계약](protocol-v0.2-bundle-discovery.md), 실제 manifest 전체 예시는
|
|
[manifest-response.json](examples/bundle-v0.2/manifest-response.json)을 참고한다.
|