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>
This commit is contained in:
36
docs/contracts/portal-mcp/README.md
Normal file
36
docs/contracts/portal-mcp/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Portal-MCP 계약 문서
|
||||
|
||||
이 디렉터리는 포털과 MCP Server 사이의 Tool Server registry 조회 계약을 관리한다.
|
||||
|
||||
```text
|
||||
Portal ──[portal-mcp 계약]──▶ MCP Server ──[tool-service-mcp 계약]──▶ Tool Server
|
||||
▲
|
||||
└──[agent-builder-mcp 계약]── Agent Builder
|
||||
```
|
||||
|
||||
| 문서 | 상태 | 용도 |
|
||||
|---|---|---|
|
||||
| [protocol-v0.1-registry.md](protocol-v0.1-registry.md) | Implemented | route별 Tool Server 목록 조회 계약. 응답 형태, 필드, 실패 동작, 갱신 주기 |
|
||||
|
||||
## 현재 원칙
|
||||
|
||||
- 포털은 **route가 무엇이고 그 route에 어떤 Tool Server가 있는가**만 소유한다. `serviceDomain`과 `manifestPath`까지다.
|
||||
- Tool 목록과 Tool 실행 endpoint는 포털이 아니라 Tool Server 매니페스트에서 온다([ADR-0010](../../decisions/ADR-0010-tool-service-manifest-owns-execution-endpoint.md)).
|
||||
- registry 응답은 그 시점의 전체 상태다. 증분은 없다.
|
||||
- 조회 실패는 route 삭제가 아니다. 성공한 registry가 route를 제외했을 때만 제거를 반영한다.
|
||||
- route 조회는 서로 독립이지만, 한 route 안에서는 전부 아니면 전무다. 부분 목록으로 snapshot을 만들지 않는다.
|
||||
- MCP는 요청 경로에서 포털을 호출하지 않는다. route key 검증도 in-memory snapshot만 본다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- 요청 URL의 route key 규약: [Agent Builder 계약 v0.3](../agent-builder-mcp/protocol-v0.3-streaming-policy.md#공개-url과-route-key)
|
||||
- 매니페스트 조회·실행 계약: [Tool Service 계약 v0.2](../tool-service-mcp/protocol-v0.2-bundle-discovery.md)
|
||||
|
||||
## 운영 적용 전 확정할 항목
|
||||
|
||||
1. 포털 API의 인증 방식과 MCP → 포털 방향 NetworkPolicy
|
||||
2. `registryRevision`의 형식과 변경 통지 방식
|
||||
3. route 추가·폐기 시 rolling 호환 기간
|
||||
4. 저장소 샘플(`config/local-toolserver-info-sample-v1.json`, `deploy/portal-registry.json`)을 이 계약에 맞추는 시점
|
||||
|
||||
상세 필드와 장애 처리는 [v0.1 계약](protocol-v0.1-registry.md)을 따른다.
|
||||
117
docs/contracts/portal-mcp/protocol-v0.1-registry.md
Normal file
117
docs/contracts/portal-mcp/protocol-v0.1-registry.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Portal-MCP Tool Server Registry 계약 v0.1
|
||||
|
||||
- 상태: **Implemented** (MCP 서버 측 구현 완료, 포털 측 합의 대기)
|
||||
- 기준일: 2026-08-22
|
||||
- 조회 위치: `mcp.portal.registry-url` — HTTP(S) Portal API 또는 `file:`/`classpath:` 로컬 리소스
|
||||
- 활성 조건: `mcp.portal.enabled=true`
|
||||
- 구현: `PortalToolRegistryClient`
|
||||
|
||||
## 1. 계약 범위와 원칙
|
||||
|
||||
포털은 **route별로 어떤 Tool Server가 있는가**만 알려 준다. Tool 목록과 Tool 실행 주소는 포털이 아니라 각 Tool Server의 매니페스트에서 온다([Tool Service-MCP Bundle 조회 계약 v0.2](../tool-service-mcp/protocol-v0.2-bundle-discovery.md), [ADR-0010](../../decisions/ADR-0010-tool-service-manifest-owns-execution-endpoint.md)).
|
||||
|
||||
| 원칙 | 내용 |
|
||||
|---|---|
|
||||
| MCP가 가져온다 | 포털은 registry를 제공만 한다. MCP에 push하지 않는다 |
|
||||
| 포털은 route와 Tool Server만 소유 | `serviceDomain`과 `manifestPath`까지다. Tool 목록·실행 endpoint는 매니페스트가 정한다 |
|
||||
| registry는 전체 상태 | 응답은 그 시점 route 전체다. 증분 없음 |
|
||||
| route 단위 격리 | 한 route의 manifest 조회 실패가 다른 route의 snapshot을 지우지 않는다 |
|
||||
| 요청 경로는 조회하지 않는다 | `tools/list`·`tools/call`과 route key 검증은 in-memory snapshot만 본다 |
|
||||
|
||||
## 2. 응답 형태
|
||||
|
||||
두 가지를 모두 받는다. `routes`가 배열이면 집계형으로, 아니면 단일 route로 해석한다.
|
||||
|
||||
**집계형** — 한 번의 호출로 모든 route를 받는다. 운영에서 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"registryRevision": "portal-registry-2026-08-22-01",
|
||||
"routes": [
|
||||
{
|
||||
"routeKey": "cus",
|
||||
"toolServices": [
|
||||
{
|
||||
"serviceKey": "was-cus",
|
||||
"serviceDomain": "https://tool-cus.devjun.net",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"namePrefix": "",
|
||||
"status": "ACTIVE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**단일 route형** — `routes` 없이 최상위에 `routeKey`와 `toolServices`를 둔다.
|
||||
|
||||
```json
|
||||
{
|
||||
"routeKey": "cus",
|
||||
"toolServices": [ { "serviceKey": "was-cus", "serviceDomain": "https://tool-cus.devjun.net", "manifestPath": "/tool-manifest", "status": "ACTIVE" } ]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 필드
|
||||
|
||||
| 필드 | 위치 | 필수 | MCP 처리 |
|
||||
|---|---|---|---|
|
||||
| `routes[]` | 최상위 | 선택 | 배열이면 집계형. 없으면 최상위를 단일 route로 읽는다 |
|
||||
| `routeKey` | route | **필수** | 정규화 후 route 식별자. 요청 URL `/mcp/{routeKey}`와 대조한다 |
|
||||
| `toolServices[]` | route | **필수** | 이 route가 보는 Tool Server 목록 |
|
||||
| `serviceKey` | service | **필수** | bundle id로 사용. 매니페스트의 `bundleId`와 일치해야 한다 |
|
||||
| `serviceDomain` | service | **필수** | Tool Server 주소. 후행 `/`는 제거한다. 매니페스트의 **상대** endpoint를 절대 URL로 바꾸는 기준이 된다 |
|
||||
| `manifestPath` | service | **필수** | 매니페스트 경로. `serviceDomain + manifestPath`가 조회 주소다 |
|
||||
| `status` | service | 선택 | 기본 `ACTIVE`. 대소문자 무시하고 `ACTIVE`가 아니면 그 서비스를 건너뛴다 |
|
||||
| `namePrefix` | service | 선택 | 기본 `""`. Tool 이름 접두사 검증에 사용한다 |
|
||||
| `registryRevision` | 최상위 | 선택 | 변경 진단·로그용. 호출 대상 결정에는 쓰지 않는다 |
|
||||
|
||||
**MCP가 읽지 않는 필드가 있다.** 현재 구현은 `executeBasePath`, `displayName`, `toolEndpoints`를 무시한다. 응답에 있어도 오류가 아니지만 동작에 영향을 주지 않으므로, 포털이 이를 근거로 실행 주소를 통제할 수 있다고 가정하면 안 된다.
|
||||
|
||||
## 4. 실패 동작
|
||||
|
||||
| 상황 | MCP 동작 |
|
||||
|---|---|
|
||||
| registry 조회 실패 | 이미 확보한 in-memory endpoint 목록 유지. memory가 비어 있으면 `mcp.redis.portal-registry-key`의 Redis fallback을 읽는다 |
|
||||
| Redis fallback도 실패 | 원천 미확보로 처리하고 다음 주기에 재시도 |
|
||||
| route에 ACTIVE 서비스가 하나도 없음 | `Portal registry has no active Tool Service`로 그 route 조회 실패 |
|
||||
| 한 Tool Server의 매니페스트에 사용 가능한 성공본이 없음 | 그 route 전체를 실패 처리. 부분 목록을 채택하지 않는다 |
|
||||
| route 간 Tool name 중복 또는 `maxToolsTotal` 초과 | 같은 이유로 실패 처리 |
|
||||
| registry에서 사라진 route | 다음 갱신에 in-memory snapshot에서도 제거 |
|
||||
|
||||
route 단위 격리와 catalog 교체 규칙은 Tool Service 계약 v0.2 §7과 같은 원칙을 따른다. 조회는 route마다 독립이지만, 한 route 안에서는 전부 아니면 전무다.
|
||||
|
||||
## 5. 갱신 주기
|
||||
|
||||
- `mcp.portal.refresh-interval-seconds` (기본 300초): 포털 registry만 다시 읽는다.
|
||||
- `mcp.registry.refresh-interval-seconds`: 이미 확보한 Tool Server 목록의 매니페스트만 다시 읽는다.
|
||||
|
||||
기동 preload는 포털 registry를 먼저 호출한 뒤 매니페스트를 조회한다. 두 주기는 독립이다.
|
||||
|
||||
## 6. 로컬 검증
|
||||
|
||||
`registry-url`에 `file:` 또는 `classpath:` 리소스를 지정하면 포털 서버 없이 같은 계약으로 읽는다. 이후 매니페스트 조회·route별 snapshot 갱신·Redis fallback 규칙은 HTTP API를 쓸 때와 동일하다.
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
portal:
|
||||
enabled: true
|
||||
registry-url: file:./config/local-toolserver-info-sample-v1.json
|
||||
refresh-interval-seconds: 15
|
||||
```
|
||||
|
||||
## 7. 저장소의 샘플 파일
|
||||
|
||||
두 샘플이 있고, 현재 서로 다르다. 이 계약을 정본으로 삼고 맞춰야 한다.
|
||||
|
||||
| 파일 | 용도 | 이 계약과의 차이 |
|
||||
|---|---|---|
|
||||
| `config/local-toolserver-info-sample-v1.json` | 로컬 검증용 | MCP가 읽지 않는 `displayName`을 포함 |
|
||||
| `deploy/portal-registry.json` | 배포 참고용 | MCP가 읽지 않는 `executeBasePath`를 포함하고 `registryRevision`이 없다 |
|
||||
|
||||
## 8. 열린 항목
|
||||
|
||||
- 포털 API의 인증 방식은 이 계약이 정하지 않는다. MCP는 인증·인가를 하지 않으므로([ADR-0006](../../decisions/ADR-0006-no-authentication-in-mcp.md)) 네트워크 경계에서 통제한다.
|
||||
- `registryRevision`의 형식을 문자열로 고정할지 정하지 않았다. 현재 구현은 값을 로그·진단에만 쓰므로 형식에 의존하지 않는다.
|
||||
- 즉시 refresh 알림: 주기 반영으로 부족하다는 운영 근거가 생길 때 검토한다.
|
||||
Reference in New Issue
Block a user