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:
2026-08-22 23:27:56 +09:00
parent 5a523ab317
commit cb29b192b4
76 changed files with 4626 additions and 1 deletions

View File

@@ -0,0 +1,23 @@
# ADR-0001 Stateless 실행 책임 경계
- 상태: Accepted
- 결정일: 2026-07-10
- 관련 논의: [DISC-20260710-001](../discussions/DISC-20260710-001-agentbuilder-mcp-interface.md)
## 배경
Agent Builder는 사용자 의도와 업무 맥락을 바탕으로 실행할 Tool을 결정하고, MCP Server는 요청된 Tool을 안전하게 검증·실행하는 계층이다.
## 결정
- MCP Server는 서버 측 대화 세션을 보관하지 않는 stateless 실행 계층으로 유지한다.
- Tool 선택, 의도 분류, 대체 Tool 탐색은 Agent Builder 책임이다.
- MCP Server는 요청에 명시된 Tool만 Registry metadata에 따라 검증하고 실행한다.
- `mcp-session-id`가 사용되더라도 대화 상태가 아닌 correlation 값으로 취급한다.
- Business Rule은 Tool Service 책임으로 유지한다.
## 영향
- MCP Server에 LLM inference, intent classification 또는 autonomous tool selection을 추가하지 않는다.
- 인증·권한 책임은 이 결정 이후 [ADR-0006](ADR-0006-no-authentication-in-mcp.md)에서 확정했다. MCP는 인증·인가하지 않는다.
- 현재 구현은 이 결정과 대체로 일치하므로 즉시 코드 변경하지 않는다.

View File

@@ -0,0 +1,32 @@
# ADR-0002 Agent Builder Tool 노출 제한과 단일 Tool 호출
- 상태: Accepted
- 결정일: 2026-07-10
- 관련 논의: [DISC-20260710-001](../discussions/DISC-20260710-001-agentbuilder-mcp-interface.md)
## 배경
많은 Tool을 한 번에 LLM context에 노출하면 Tool 식별과 선택 정확도가 저하될 수 있다. 또한 한 요청에서 여러 Tool을 실행하면 timeout, 부분 성공, 순서 의존성과 오류 계약이 복잡해진다.
## 결정
- Agent Builder가 사용자·Agent·업무 맥락에 맞는 Tool을 선택한다.
- Agent Builder가 한 시점에 LLM에 노출하는 활성 Tool은 최대 50개다.
- 목표 Agent Builder-MCP 호출 모델은 요청 하나당 Tool 하나다.
- MCP Server는 50개 선별 로직이나 LLM 기반 우선순위 판단을 구현하지 않는다.
## 영향
- 50개 선별은 Agent Builder 변경 사항이며 MCP 코드 변경 대상이 아니다.
- 요청 하나당 Tool 하나라는 결정은 그대로 유효하다.
### 결정 당시의 envelope 기록 (현재 구현 아님)
결정 시점에는 최종 field 이름이 미확정이어서 `params.toolCalls[]` 배열 envelope를 유지하고,
원소를 정확히 한 개만 허용해 단일 호출을 강제했다. 빈 배열과 2개 이상은 `-32602`로 거절했다.
이 envelope는 [ADR-0005](ADR-0005-standard-tool-name.md)와
[계약 v0.3](../contracts/agent-builder-mcp/protocol-v0.3-streaming-policy.md)에서
표준 MCP `params.name` + `params.arguments`로 대체되었다.
**현재 코드에 `toolCalls`는 존재하지 않는다.** 배열이 사라졌으므로 "정확히 한 개" 검증도
필요 없어졌고, 단일 호출 결정은 envelope 구조 자체로 만족된다.

View File

@@ -0,0 +1,29 @@
# ADR-0003 Builder Tool UID를 시스템 간 식별 키로 사용
- 상태: Superseded
- 결정일: 2026-07-10
- 관련 논의: [DISC-20260710-001](../discussions/DISC-20260710-001-agentbuilder-mcp-interface.md)
- 대체 결정: [ADR-0005](ADR-0005-standard-tool-name.md)
> 이 문서는 당시 검토 이력을 보존한다. 현재 구현과 신규 연동에는 ADR-0005를 적용한다.
## 배경
Tool name은 중복 또는 변경 가능성이 있으므로 Agent Builder Tool Registry가 부여한 고유 식별자를 시스템 간 mapping key로 사용할 필요가 있다.
## 결정
- Agent Builder Tool Registry가 Custom Tool에 부여한 UID를 Agent Builder-MCP 사이의 Tool 식별 키로 사용한다.
- MCP Server는 UID를 Registry metadata의 Tool name, version, endpoint와 매핑하여 실행한다.
- Tool name과 version은 설명 및 검증 metadata로 유지할 수 있지만 시스템 간 기본 mapping key 역할은 UID가 담당한다.
## 구현 보류 조건
다음 항목은 아직 결정되지 않았으므로 코드 변경 조건이 충족되지 않았다.
- UID 생성 시점과 Agent Builder/MCP 전달 시점
- version 변경, 비활성화, 삭제, 재등록 시 UID 규칙
- 환경별 UID 승격 또는 분리 규칙
- 목표 요청 payload의 UID field 이름
이 보류안은 ADR-0005로 대체되었으며 현재 모델에 UID field를 두지 않는다.

View File

@@ -0,0 +1,30 @@
# ADR-0004 실행 가드레일
- 상태: Accepted
- 결정일: 2026-07-10
- 관련 논의: [DISC-20260710-001](../discussions/DISC-20260710-001-agentbuilder-mcp-interface.md)
## 결정
- Agent Builder-MCP 상호작용에는 300초 hard limit을 둔다.
- MCP Server는 3만 자 기준으로 Tool Service의 원문 응답을 임의 절단하지 않는다.
- write/update 성격의 Tool은 idempotency가 보장되지 않으면 자동 retry하지 않는다.
## 해석 경계
- 300초는 전체 상한 원칙이며 모든 계층의 socket read timeout을 무조건 300초로 설정한다는 의미가 아니다.
- 원문 미절단은 response body를 무제한 허용한다는 의미가 아니다.
- read Tool이 항상 retry 가능하다는 결정은 아니다.
## 구현 보류 조건
다음 세부 계약이 확정되기 전에는 timeout 또는 retry 코드를 변경하지 않는다.
- ~~Agent Builder, ingress, MCP, Tool Service별 timeout budget~~ →
**확정 (2026-08-01).** Agent Builder 300초(호출 시점 기준) > MCP 270초 > Tool 30초.
배분과 근거는 [architecture.md의 요청 시간 예산](../architecture.md#요청-시간-예산)이 정본이다.
- client disconnect와 downstream cancellation 전파
- response body 최대 크기와 pagination/continuation 정책
- Tool별 idempotency/retryable metadata와 오류 코드
현재 구현에는 3만 자 절단 및 자동 retry가 없으므로 두 원칙에 대한 즉시 코드 변경은 없다.

View File

@@ -0,0 +1,25 @@
# ADR-0005 표준 MCP Tool name을 실행 식별자로 사용
- 상태: Accepted
- 결정일: 2026-07-30
- 대체 대상: [ADR-0003](ADR-0003-builder-tool-uid.md)
## 배경
Tool의 원천 정보는 각 Tool Service가 소유하고 MCP Server가 매니페스트를 pull한다.
Agent Builder의 UID는 Agent Builder 내부 관리 개념이므로 MCP와 Tool Service 사이의 계약으로
전파하면 표준 `tools/list``tools/call` 외에 별도 식별자 동기화가 필요해진다.
## 결정
- `tools/list`가 노출하고 `tools/call.params.name`이 전달하는 표준 MCP Tool `name`을 실행 식별자로 사용한다.
- Tool Service가 전체 MCP 범위에서 고유한 namespaced name을 직접 선언한다.
- 허용 형식은 MCP 표준에 맞춘 1~64자의 영문·숫자와 `_`, `-`, `.`, `/`다.
- MCP Server는 이름을 재작성하지 않고 형식, bundle의 `namePrefix`, 전체 중복을 검증한다.
- Agent Builder 내부 UID는 Agent Builder가 자체 관리하며 MCP metadata와 실행 요청에 요구하지 않는다.
## 결과
- 표준 MCP 계약만으로 목록과 실행 대상을 연결한다.
- Tool 이름 변경은 식별자 변경이므로 Tool Service와 Agent Builder의 rolling 호환 기간이 필요하다.
- 이름 충돌이나 전체 Tool 수 상한 초과 시 일부 목록을 노출하지 않고 기존 정상 snapshot을 유지한다.

View File

@@ -0,0 +1,82 @@
# ADR-0006 MCP Server는 인증·인가를 하지 않는다
- 상태: Accepted
- 결정일: 2026-08-01
- 관련 결정: [ADR-0001](ADR-0001-stateless-execution-boundary.md), [ADR-0010](ADR-0010-tool-service-manifest-owns-execution-endpoint.md), [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md)
> 이 ADR은 **inbound** 신뢰 경계를 다룬다. outbound 목적지는
> [ADR-0010](ADR-0010-tool-service-manifest-owns-execution-endpoint.md) 이후 Tool Service 매니페스트가 정하며,
> 도메인 허용목록도 egress NetworkPolicy도 없다. 아래 "책임 소재" 표의 NetworkPolicy 항목은 ingress 통제이므로
> outbound 위험을 덮지 않는다.
## 배경
Tool 실행 권한은 Agent Builder가 Agent를 구성할 때 이미 확인하고 넘어온다.
MCP Server는 Agent Builder가 지정한 Tool을 검증·실행하는 계층이므로 권한을 다시 판단할 근거가 없다.
문제는 MCP가 **판단하지 않는 계층인 동시에 집행 지점**이라는 데 있다.
MCP는 이 배포에 속한 모든 Tool Service에 도달할 수 있는 유일한 경로다.
따라서 "MCP는 아무것도 하지 않는다"는 결정은, 그러면 **누가 하는가**를 함께 적지 않으면
세 계층이 서로 상대가 확인했다고 가정하는 공백을 만든다.
이 문서는 그 공백을 막기 위해 책임 소재를 명시한다.
## 결정
MCP Server는 인증(authentication)과 인가(authorization)를 수행하지 않는다.
- 요청자의 신원을 검증하지 않는다.
- Tool 실행 권한을 판단하지 않는다.
- `employee-no`·`virtual-employee-no`를 복호화·검증·저장하지 않는다.
- Authorization 헤더를 해석하지 않는다. `mcp.tool-client.forward-authorization`이 켜져 있으면
값을 그대로 전달만 한다.
이에 따라 무검증 JWT decode 구현(`JwtClaimExtractor`, `UnverifiedJwtClaimExtractor`)을 삭제한다.
검증하지 않는 인증 코드는 없는 것보다 나쁘다. 이후 누군가 그 claim을 판단 근거로 쓸 여지를 남기고,
코드베이스에 인증이 처리되고 있다는 잘못된 인상을 준다.
## 책임 소재
| 책임 | 주체 | 근거 |
|---|---|---|
| 호출자가 Agent Builder인지 보장 | **플랫폼(NetworkPolicy)** | Helm Chart의 `templates/networkpolicy.yaml`. 환경별 허용 namespace는 `values-{env}.yaml``global.agentBuilderNamespace` |
| 사용자 인증, Tool 실행 권한 판단 | **Agent Builder** | Agent 구성 시점에 확인 |
| 사원 식별자 복호화와 업무 권한 집행 | **Tool Service** | KMS에서 발급받은 키 사용 |
| 요청 형식·schema 검증, 단일 Tool 실행 | **MCP Server** | ADR-0001 |
## 이 결정이 성립하기 위한 전제
**전제 1 — 네트워크가 호출자를 고정한다.**
MCP는 요청자를 확인하지 않으므로, `/mcp`에 도달할 수 있다는 것 자체가 곧 인가다.
NetworkPolicy로 Agent Builder namespace만 8080에 접근하도록 제한한다.
**이 정책 없이 배포하면 클러스터 안의 어떤 Pod이든 Tool을 실행할 수 있다.**
정책은 선택적 강화가 아니라 이 ADR의 성립 조건이다.
그래서 Helm Chart에 비활성화 스위치를 두지 않았고, `HelmDeploymentContractTest`
조건부 렌더링이 들어오는 것까지 막는다. values 한 줄로 인가가 사라지지 않게 하기 위해서다.
**전제 2 — 사원 식별자의 신뢰는 Tool Service가 확보한다.**
`employee-no`·`virtual-employee-no`는 암호화되어 전달되고, 복호화 키는 사내 KMS에서 발급받는다.
MCP는 키를 갖지 않으므로 값을 읽을 수 없고, 따라서 위조 여부도 판별할 수 없다.
Tool 파트가 확인할 항목:
- 복호화는 KMS 키로만 가능하므로 **기밀성**은 확보된다.
**위조 방지**는 암호화 키의 배포 범위에 달려 있다. 암호화 키를 널리 배포하면
임의의 사원번호를 스스로 암호화해 넣을 수 있으므로, 키 배포 범위를 신뢰 경계와 맞춘다.
- 동일한 암호문의 재사용(replay)을 허용할지, 만료·nonce를 둘지 결정한다.
- 두 헤더가 모두 없는 요청의 처리 방침을 정한다. 현재 계약에서 두 값은 선택값이다.
**전제 3 — 감사 추적은 세 계층의 로그를 합쳐야 완성된다.**
MCP 로그에는 `guid``x-request-id`만 남는다.
사원 식별자는 개인 식별자이므로 암호문이라도 기록하지 않는다.
따라서 "누가 조회했는가"는 MCP 로그만으로 답할 수 없다.
`guid`를 Agent Builder·MCP·Tool Service가 공통 상관 키로 사용해 세 로그를 연결한다.
규제 감사 요건이 확정되면 별도 durable sink를 설계한다.
## 영향
- MCP에 인증 코드를 추가하지 않는다. 필요가 생기면 이 ADR을 대체하는 새 ADR을 먼저 쓴다.
- NetworkPolicy는 배포 필수 구성요소다. 누락은 설정 실수가 아니라 보안 결함으로 다룬다.
- 인증 모델이 token 기반으로 바뀌면 이 결정과 전제 1을 함께 재검토한다.
- `forward-authorization` 설정은 유지한다. 검증이 아니라 통과 전달이며,
Tool Service가 자체 인증을 도입할 때 필요한 연결점이다.

View File

@@ -0,0 +1,101 @@
# ADR-0007 MCP 배포 하나는 Tool Service 하나만 본다
- 상태: Superseded
- 결정일: 2026-08-02
- 대체 결정: [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md)
- 관련: [ADR-0001](ADR-0001-stateless-execution-boundary.md), [ADR-0002](ADR-0002-tool-exposure-and-single-call.md), [ADR-0009](ADR-0009-container-handles-public-mcp-path.md), [계약 v0.2](../contracts/tool-service-mcp/protocol-v0.2-bundle-discovery.md)
> 이 문서는 검토 이력을 보존한다. 현재 구현과 신규 연동에는 [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md)을 적용한다.
> route↔Tool Service 매핑의 원천이 Portal로 옮겨지면서, MCP 배포 하나가 N개 route를 보고 route 하나에 N개 Tool Service가 붙는다.
> 아래 격리 논거는 폐기된 것이 아니라 ADR-0013이 무엇을 포기했는지 판단하는 근거로 남는다.
외부에서 여러 MCP를 하나의 host 아래 path로 묶는 방식은 [ADR-0009](ADR-0009-container-handles-public-mcp-path.md)이
소유한다.
## 배경
MCP 설정의 `mcp.bundles`는 여러 Tool Service를 하나의 카탈로그로 병합할 수 있다. 이 능력을 실제로 쓸지,
즉 MCP와 Tool Service를 M:N으로 묶을지는 결정되지 않은 상태였다.
고객 요구는 **Tool의 군집화**다. MCP 자체를 군집화해 달라는 요구가 아니다. 요구의 목적은 가용성이며,
중요한 Tool은 다운이 없어야 한다는 것이다. 분할 기준은 먼저 업무로 나누고, 그 안에서 중단 시 업무
영향도와 가용성 위험도로 다시 나누는 형태다. 예: 처리계-중요, 처리계-비중요, 정보계-중요,
정보계-비중요. 여기서 위험도는 보안·권한 정책이 아니라 **서비스 중단 위험**을 뜻한다. 업무 정책은
Tool Service가 관리하며 이 배포 등급의 범위가 아니다.
Tool 목록이 확정되지 않아 Tool Service가 몇 개가 될지 모르며, 10~20개 이상이 될 수 있다.
## 결정
1. **MCP 배포 하나는 Tool Service를 정확히 하나 본다.** `mcp.bundles`는 항상 한 항목이다.
2. 배포 단위의 분할 축은 **업무 × 등급(tier)** 이다. 등급은 `critical``standard`로 둔다.
3. 등급은 **배포 속성일 뿐 wire 계약에 나타나지 않는다.** Tool 이름·`namePrefix`·매니페스트에 등급을 넣지 않는다.
4. 다중 bundle 병합 코드는 **삭제하지 않고 유지**하되, 배포 설정에서 bundle 1개로 잠근다.
## 근거
### 등급이 다른 Tool Service를 한 MCP가 보면 격리가 깨진다
`ToolBundleRegistryClient.fetchTools()`는 사용 가능한 성공본이 없는 bundle이 하나라도 있으면
카탈로그 전체 교체를 거부한다(계약 v0.2 §1, §7). 한 MCP가 중요·비중요 Tool Service를 함께 보면
**비중요 쪽 조회가 확정되지 않는 동안 중요 Tool의 카탈로그 갱신까지 멈춘다.** 직전 성공본으로
서빙은 계속되지만 변경 반영은 막힌다.
여기에 두 Tool Service 호출이 같은 프로세스의 HTTP connection pool과 스레드를 공유하므로,
비중요 쪽 지연이 중요 쪽 여유를 잠식한다.
**병합은 가용성 요구와 정면으로 충돌한다.** 등급을 나눈 목적을 배포 구조가 되돌려 놓는다.
### 병합해서 얻는 것이 없다
MCP에는 업무 로직이 없다. 여러 Tool Service를 하나로 합치는 일이 MCP 안에서 일어나야 할
기술적 이유가 없다. Agent Builder는 MCP를 개별 등록하면서 하위 Tool 정보를 자기 DB에 저장하고,
사용자 요청을 판단한 뒤 **해당 Tool을 가진 MCP로 호출을 보낸다.** 여러 Tool 묶음을 아우르는 일은
Tool 선택을 이미 수행하는 Agent Builder 계층에서 끝난다.
런타임에 공유되는 공통 Tool Service도 없다. Tool 파트의 `tool-common`은 각 Tool Service 프로젝트가
함께 빌드하는 **빌드 타임 라이브러리**이지 별도로 뜨는 서비스가 아니다. 1:1을 깨야 할 사례가 남지 않는다.
### 1:1이라야 등급별로 다른 비용을 쓸 수 있다
한 MCP가 등급을 섞어 들고 있으면 그 배포 전체에 중요 등급 기준을 적용해야 한다. 나뉘어 있으면
`critical`에만 replica 여유와 PodDisruptionBudget을 주고 `standard`는 최소로 둘 수 있다.
**분할의 실질 이득은 격리 자체보다 여기에 있다.**
다만 배포를 나누는 것만으로 가용성이 생기지는 않는다. 같은 노드 배치, 같은 namespace의 쿼터,
공통 Redis·클러스터 장애는 분할로 막히지 않는다. 등급 분리가 의미를 가지려면 replica 하한,
PodDisruptionBudget, anti-affinity와 usable Tool snapshot 기반 readiness가 함께 가야 한다. Chart의
`tiers` 설정과 `HelmDeploymentContractTest`가 test·prod의 정적 values를 검사하며, 실제 렌더링 결과는
배포 파이프라인의 `helm lint``helm template`이 확인한다.
## 전제
아래가 깨지면 이 결정을 재검토한다.
1. Agent Builder는 MCP를 개별 등록하고, 한 Agent가 여러 MCP의 Tool을 사용할 수 있다.
2. Tool 호출은 한 요청에 하나이며([ADR-0002](ADR-0002-tool-exposure-and-single-call.md)) 그 Tool을 가진 MCP로 직접 간다.
3. 런타임에 공유되는 공통 Tool Service가 없다.
## 영향
- **배포 수 = Tool Service 수**다. 10~20개를 전제로 Helm values는 토폴로지를 한 파일에 모으고
배포 시 `deploymentKey`로 하나를 고른다. 배포가 늘어도 파일 수는 변하지 않는다.
- 계약 v0.2 §7의 병합 규칙 중 bundle 간 이름 충돌(6번)과 `maxToolsTotal`(3번 후단)은 운영에서 발동하지 않는다.
규칙 자체는 계약에 남는다.
- **Tool 이름의 전역 유일성은 Tool Service 책임으로 남는다.** 서로 다른 MCP가 같은 `namePrefix`
쓰는 것을 MCP는 막지 못한다. 등급으로 나뉜 두 배포가 같은 업무 prefix(`processing.`)를 공유하는 것은
의도된 구성이며, 그 안에서 Tool 이름이 겹치지 않아야 한다.
- Tool을 다른 등급으로 옮기면 그 Tool을 제공하는 **MCP endpoint가 바뀐다.** Agent Builder가 Tool 정보를
DB에 보관하므로 반영에는 재등록 또는 다음 `tools/list` 주기가 필요하다. 등급은 자주 바꾸지 않는 값으로 다룬다.
- [ADR-0002](ADR-0002-tool-exposure-and-single-call.md)의 Tool 노출 상한 50개는 한 Agent가 여러 MCP에서
가져온 Tool의 **합계**에 적용된다. MCP를 나눈다고 상한이 늘지 않는다.
## 채택하지 않은 대안
**M:N — 한 MCP가 여러 Tool Service를 본다.** 배포 수는 줄지만 위의 격리 문제가 그대로 남는다.
가용성이 분할의 목적이므로 목적과 수단이 어긋난다.
**코드에서 bundle 1개를 강제한다.** `McpProperties`에 검증을 넣으면 다중 bundle 병합 코드가
도달 불가능해진다. 전제 3이 깨질 때 되돌리는 비용이 커지고, 이미 작성·테스트된 경로를 죽은 코드로
만든다. 1:1은 애플리케이션 불변식이 아니라 **배포 결정**이므로 배포 정의에서 잠그는 편이 맞다.
이 선택은 검증 위치를 옮긴 것이지 검증을 뺀 것이 아니다.

View File

@@ -0,0 +1,58 @@
# ADR-0008 공유 host의 path를 독립 MCP 배포로 연결한다
- 상태: Superseded
- 결정일: 2026-08-05
- 대체 결정: [ADR-0009](ADR-0009-container-handles-public-mcp-path.md)
- 관련: [ADR-0006](ADR-0006-no-authentication-in-mcp.md), [ADR-0007](ADR-0007-one-mcp-per-tool-service.md)
> 이 문서는 검토 이력을 보존한다. 결정 3의 "공개 path를 내부 고정 endpoint `/mcp`로 rewrite한다"는
> [ADR-0009](ADR-0009-container-handles-public-mcp-path.md)이 대체했고, 결정 4의 1:1 매핑은
> [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md)이 대체했다.
## 배경
Agent Builder에는 하나의 HTTPS host를 제공하고 업무별 MCP를 `/mcp/core`, `/mcp/process`,
`/mcp/information` 같은 path로 구분해야 한다. 이를 하나의 애플리케이션 프로세스에서 처리하면
Registry snapshot·Redis cache·readiness·connection 자원을 route별로 다시 나눠야 하고, 한 프로세스의
장애와 배포가 모든 Tool Service에 전파된다.
## 결정
1. 환경마다 공개 MCP host를 하나 둔다.
2. `deployments.<key>.publicPath`는 한 MCP Deployment를 가리키며 topology 전체에서 유일하다.
3. OpenShift Route가 공개 path를 해당 MCP Service로 전달하고 내부 고정 endpoint `/mcp`로 rewrite한다.
4. MCP 애플리케이션과 Tool Service의 1:1 매핑, Registry snapshot, Redis key와 readiness는 배포별로 유지한다.
5. Agent Builder는 각 공개 URL을 독립 MCP endpoint로 등록하고 endpoint별로 initialize한다.
6. 요청 body·header·Tool 이름은 어느 MCP Service로 보낼지 결정하지 못한다. 대상은 Helm topology만 정한다.
```text
https://{host}/mcp/core -> core MCP /mcp -> core Tool Service
https://{host}/mcp/process -> process MCP /mcp -> process Tool Service
https://{host}/mcp/information -> information MCP /mcp -> information Tool Service
```
가용성 등급으로 같은 업무를 둘로 나누면 path도 구분한다. 예를 들어
`/mcp/process-critical``/mcp/process-standard`는 서로 다른 MCP Deployment와 Tool Service를 가리킨다.
## 근거
- 외부 URL은 한 host로 단순화하면서 내부 장애 범위는 업무·가용성 등급별로 유지한다.
- 현재 Java transport·Registry·cache 코드를 route-aware Gateway로 바꾸지 않아도 된다.
- 한 Tool Service의 조회 실패나 부하가 다른 MCP의 readiness와 snapshot 갱신을 막지 않는다.
- 공개 path와 Service 매핑을 한 topology에서 검토하고 계약 테스트로 중복·형식을 막을 수 있다.
## 영향
- 하나의 Helm release는 Deployment·Service·Route를 하나씩 만든다. 여러 release의 Route가 같은 host와 서로 다른 path를 사용한다.
- OpenShift Router가 TLS를 종료하고 path를 rewrite하므로 backend 애플리케이션은 계속 `POST /mcp`만 처리한다.
- 공개 URL 변경 시 Agent Builder 재등록 또는 endpoint 설정 변경이 필요하다.
- Gateway 기능은 OpenShift Route가 소유한다. MCP Java 프로세스는 Tool 선택이나 다른 MCP로의 proxy를 하지 않는다.
- 실제 환경 host와 인증서·TLS 종료 방식은 플랫폼 적용 전에 확정해야 한다.
## 채택하지 않은 대안
**하나의 MCP 프로세스가 모든 path를 처리한다.** 배포 수는 줄지만 전역 장애 범위와 noisy neighbor가 생기고,
route별 Registry·cache·readiness를 새로 구현해야 하므로 채택하지 않았다.
**Tool Service가 MCP protocol을 직접 구현하고 중앙 Gateway가 raw proxy한다.** Tool Service 계약과 책임이
크게 바뀌며 현재의 공통 MCP 검증 계층이 중복되므로 채택하지 않았다.

View File

@@ -0,0 +1,41 @@
# ADR-0009 컨테이너가 공개 MCP path를 직접 처리한다
- 상태: Accepted
- 결정일: 2026-08-05
- 대체: [ADR-0008](ADR-0008-shared-host-path-routing.md)
- 부분 대체됨: 결정 4는 [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md)이 대체한다
- 관련: [ADR-0007](ADR-0007-one-mcp-per-tool-service.md)
> 결정 1·2·3·5는 유효하다. 공개 path를 rewrite하지 않고 컨테이너가 직접 처리한다는 원칙은 그대로다.
> 결정 4의 "MCP 컨테이너 하나는 endpoint와 Tool Service를 각각 하나만 가진다"만 ADR-0013이 대체하며,
> 지금은 `/mcp` + 동적 route key로 한 컨테이너가 N개 route와 route당 N개 Tool Service를 처리한다.
## 배경
Agent Builder는 `/mcp/core`, `/mcp/process`, `/mcp/information`처럼 sub path까지 포함한 URL을 각각의 MCP로 등록한다. 각 URL은 독립 MCP 컨테이너와 Tool Service에 연결된다. 공개 path를 OpenShift Route가 내부 `/mcp`로 바꾸면 Agent Builder가 등록한 경로와 컨테이너가 처리한 경로가 달라져 운영 추적과 설정 검증이 어려워진다.
## 결정
1. `deployments.<key>.publicPath`는 Agent Builder 등록 URL과 컨테이너 endpoint가 함께 사용하는 단일 정본이다.
2. OpenShift Route는 host와 path로 독립 MCP Service만 선택하고 path를 rewrite하지 않는다.
3. Helm ConfigMap이 `publicPath``mcp.endpoint-path`로 주입하고, Controller와 Filter가 그 경로만 처리한다.
4. MCP 컨테이너 하나는 endpoint와 Tool Service를 각각 하나만 가진다. 하나의 Java 프로세스에서 path별 Registry를 선택하지 않는다.
5. 요청 body·header·Tool name은 컨테이너 선택에 관여하지 않는다.
```text
https://{host}/mcp/core -> core MCP /mcp/core -> core Tool Service
https://{host}/mcp/process -> process MCP /mcp/process -> process Tool Service
https://{host}/mcp/information -> information MCP /mcp/information -> information Tool Service
```
## 영향
- 같은 host에서 여러 Service를 사용하므로 OpenShift Route의 path 기반 Service 선택은 유지한다.
- rewrite annotation은 사용하지 않는다. access log와 애플리케이션 log가 같은 path를 본다.
- `publicPath` 변경은 Route와 애플리케이션 endpoint를 함께 바꾸며 Agent Builder 등록 정보도 갱신해야 한다.
- Registry snapshot, Redis namespace, readiness, connection pool과 장애 범위는 배포별로 계속 분리된다.
- JSON-RPC payload와 Tool Service wire 계약은 바뀌지 않는다.
## 대체한 결정
ADR-0008의 공유 host와 독립 Deployment 원칙은 유지한다. 공개 path를 내부 `/mcp`로 rewrite한다는 부분만 이 ADR이 대체한다.

View File

@@ -0,0 +1,47 @@
# ADR-0010 Tool 실행 endpoint를 Tool Service 매니페스트가 선언한다
- 상태: Accepted
- 결정일: 2026-08-19
- 기록: 2026-08-22. 구현 커밋 `6078852`를 기준으로 사후 작성했다.
- 관련: [ADR-0006](ADR-0006-no-authentication-in-mcp.md), [ADR-0007](ADR-0007-one-mcp-per-tool-service.md)
## 배경
이전 설계에서 Tool 실행 주소는 MCP 배포 설정이 단독으로 소유했다. `mcp.bundles[].baseEndpoint` 뒤에 Tool name을 붙여 `POST {baseEndpoint}/{toolName}`을 만들었고, 매니페스트가 endpoint 성격의 값을 담고 있어도 읽지 않았다. **Tool Service가 반환한 어떤 값도 MCP가 요청을 보내는 대상을 바꿀 수 없다**는 것이 이 설계의 핵심이었고, `docs/architecture.md`, Tool Service 계약 v0.2, `ToolBundleDiscovery`의 Javadoc, `application.yml` 주석이 같은 문장을 반복해 고정하고 있었다.
Portal registry를 Tool Server 목록의 원천으로 도입하면서 전제가 무너졌다. 포털은 route별로 Tool Server의 `serviceDomain``manifestPath`만 제공한다. Tool 하나하나의 실행 경로는 포털이 모르고, MCP 배포 설정도 미리 알 수 없다. 기존 구조를 유지하려면 Tool을 추가하거나 경로를 바꿀 때마다 배포 설정의 endpoint 목록을 함께 고쳐야 했고, 이는 Tool Service의 배포 주기와 MCP의 배포 주기를 묶어 버린다.
## 결정
1. Tool 실행 주소는 Tool Service 매니페스트가 선언한다. MCP는 각 Tool의 top-level `endpoint`를 먼저 읽고, 없으면 `_meta.endpoint`를 사용한다.
2. 둘 다 없거나 비어 있으면 그 Bundle 전체를 거부한다. Tool 하나의 누락이 나머지 Tool을 조용히 통과시키지 않는다.
3. 상대 경로는 Portal registry가 제공한 `serviceDomain` 뒤에 붙여 절대 URL로 만든다. 이것이 운영에서 기대하는 형태다.
4. 절대 URL은 Tool Service가 제공한 실행 주소 원천으로 그대로 사용한다. scheme이 HTTP(S)가 아니거나 host가 없으면 거부한다. 프로토콜 상대 주소(`//host/path`)와 개행이 섞인 값도 거부한다.
5. `mcp.bundles[].baseEndpoint`는 더 이상 실행 주소의 정본이 아니다. 상대 경로를 해석하는 기준으로만 남으며, 절대 HTTP(S)여야 한다.
6. `endpoint`는 내부 실행 정보이므로 `_meta`와 함께 제거해 `tools/list` 공개본에 내보내지 않는다.
```text
manifest endpoint = "/mcp/processing.contract.inquiry" (운영 관례)
-> https://tool-cus.devjun.net/mcp/processing.contract.inquiry
manifest endpoint = "https://other.example/tool" (허용되지만 위험)
-> https://other.example/tool
```
## 영향
- Tool을 추가하거나 실행 경로를 바꿀 때 MCP 배포 설정을 함께 바꾸지 않아도 된다. Tool Service가 매니페스트만 갱신하면 다음 refresh에 반영된다.
- **신뢰 경계가 이동한다.** 이전에는 배포 설정이 outbound 대상을 봉인했으나, 이제는 매니페스트가 결정한다. 매니페스트가 절대 URL을 선언하면 MCP는 그 호스트로 요청을 보낸다.
- 검증은 두 지점에 있다. discovery 시점에 `ToolBundleDiscovery`가 scheme·host·프로토콜 상대 주소·개행을 확인하고, 실행 시점에 `ToolRoutingService.validateEndpoint()`가 절대 HTTP(S)인지 다시 확인한다. 둘 다 **형식 검사이며 도메인 허용목록은 없다.** 따라서 매니페스트 원천의 신뢰성이 곧 outbound 대상의 신뢰성이다.
- 네트워크 계층의 완화도 없다. `deploy/helm/mcp-server/templates/networkpolicy.yaml``policyTypes: [Ingress]`만 선언하므로 outbound 목적지를 제한하지 않는다. 이 저장소가 제공하는 allowlist(`route.sourceAllowlist`, NetworkPolicy)는 모두 inbound 통제다.
- [ADR-0006](ADR-0006-no-authentication-in-mcp.md)에 따라 MCP는 인증·인가를 하지 않는다. 그래서 "`GET /tool-manifest`를 NetworkPolicy로 MCP Server namespace에서만 접근 가능하게 한다"는 기존 요구가 선택적 권고가 아니라 이 결정의 전제 조건이 된다.
- endpoint 검증 실패는 Bundle 전체 거부로 처리되고 직전 정상 snapshot이 유지되므로, 잘못된 매니페스트 배포가 기존 Tool 목록을 지우지는 않는다.
- 계약 문서와 예제가 함께 갱신됐다. `docs/contracts/tool-service-mcp/examples/bundle-v0.2/manifest-response.json``endpoint`를 포함하며, `ToolBundleContractExampleTest`가 문서와 구현의 일치를 고정한다.
## 남은 위험
- **도메인 허용목록 부재.** 매니페스트가 임의의 HTTP(S) 호스트를 지정할 수 있고, 애플리케이션 검사도 네트워크 정책도 이를 좁히지 않는다. 내부망 운영 전에 두 방향 중 하나를 정해야 한다.
- `mcp.tool-domains` 형태의 allowlist를 두고 절대 URL을 그에 대조한다.
- 절대 URL을 아예 거부하고 상대 경로만 허용해 목적지를 Portal registry의 `serviceDomain`으로 봉인한다. 운영 예제가 이미 상대 경로만 쓰고 있어 비용이 가장 낮고, 이전 설계의 "Tool Service가 호출 대상을 바꿀 수 없다"는 성질도 회복된다.
- egress NetworkPolicy를 함께 검토한다. 위 두 방안 중 무엇을 택하든 애플리케이션 단독 방어보다 낫다.
- 이 ADR은 기존 ADR을 대체하지 않는다. 뒤집힌 불변식이 ADR이 아니라 architecture 문서와 코드 주석에만 있었기 때문이다. 같은 일이 반복되지 않도록 실행 주소 관련 결정은 앞으로 이 ADR을 갱신하거나 후속 ADR로 남긴다.

View File

@@ -0,0 +1,87 @@
# ADR-0013 Tool Server endpoint 목록과 route 매핑의 원천은 Portal이 소유한다
- 상태: Accepted
- 결정일: 2026-08-22
- 대체 결정: [ADR-0007](ADR-0007-one-mcp-per-tool-service.md) 전체, [ADR-0009](ADR-0009-container-handles-public-mcp-path.md) 결정 4
- 관련: [ADR-0001](ADR-0001-stateless-execution-boundary.md) · [ADR-0005](ADR-0005-standard-tool-name.md) · [ADR-0010](ADR-0010-tool-service-manifest-owns-execution-endpoint.md) · [Portal-MCP 계약 v0.1](../contracts/portal-mcp/protocol-v0.1-registry.md)
- 번호 주석: `feature/mcp-integration`이 ADR-0011·ADR-0012를 Tool inputSchema 정책에 쓰고 있어 그 번호를 비워 둔다. 같은 브랜치의 동명 ADR-0013과 이 문서는 같은 결정이다.
## 배경
[ADR-0007](ADR-0007-one-mcp-per-tool-service.md)은 MCP 배포 하나가 Tool Service 하나만 보게 하고, 어떤 Tool Service를 볼지를 `mcp.bundles`에 배포 시점으로 못박았다. 전제는 **매핑이 배포 시점에 확정된다**는 것이었다.
내부망 운영은 그 전제를 따르지 않는다. route와 Tool Service의 매핑은 Portal이 관리하고, MCP는 기동 preload와 주기 refresh에서 Portal registry를 읽어 매핑을 받는다. 매핑이 바뀌어도 MCP를 다시 배포하지 않아야 한다.
구현은 이미 이 구조였다. `PortalToolRegistryClient`가 registry 응답을 `Map<String, List<Bundle>>`(route → Tool Service 목록)로 유지하고, `McpRequestContextFactory``/mcp/{routeKey}`에서 route를 뽑고, `ToolRegistryService`가 route별 snapshot을 들고 있다. 그런데 이 경로를 정당화하는 결정 문서가 없었고, 그 사이 ADR-0007은 `Accepted` 상태로 남아 코드와 정반대되는 내용을 현재 설계 근거처럼 제시하고 있었다.
## 결정
1. **Tool Server endpoint 목록과 route↔Tool Service 매핑의 원천은 Portal이다.** MCP는 기동 preload와 주기 refresh에서 Portal registry를 조회한다.
2. **MCP 배포 하나가 N개 route를 서비스한다.** route key는 `/mcp/{routeKey}` URI에서만 결정한다.
3. **route 하나에 N개 Tool Service가 붙을 수 있다.** 카탈로그 병합 단위는 route다.
4. Portal은 **주소만** 소유한다. Tool 목록·schema·timeout은 Tool Service 매니페스트가 소유하고, Tool 실행 주소도 매니페스트가 정한다([ADR-0010](ADR-0010-tool-service-manifest-owns-execution-endpoint.md)).
5. 요청 경로(`tools/list`, `tools/call`, route key 검증)는 in-memory snapshot만 읽는다. Portal은 요청 경로에 없다.
6. 응답 모양과 실패 처리는 [Portal-MCP 계약 v0.1](../contracts/portal-mcp/protocol-v0.1-registry.md)이 정본이다.
## 근거
### 매핑이 동적이면 배포 축과 매핑 축을 겹칠 수 없다
ADR-0007은 매핑을 배포 정의에 넣었다. Portal이 매핑을 소유하는 순간 **매핑 변경이 곧 배포 변경**이 되어 Portal을 원천으로 둔 의미가 사라진다. 원천이 Portal이면 배포는 매핑에 대해 중립이어야 하고, 그래서 한 배포가 N route를 서비스한다.
### ADR-0007의 격리 논거는 층위별로 다르게 남는다
ADR-0007이 지키려던 것은 가용성 등급별 격리였다. 이 구조에서 무엇이 남고 무엇이 사라지는지 숨기지 않고 적는다. 아래는 현재 main 코드 기준이다.
| 층위 | 격리 | 근거 |
|---|---|---|
| route별 snapshot 보관 | **유지** | `ToolRegistryService`가 route별 snapshot을 따로 들고, 요청은 자기 route만 읽는다 |
| route 안 N개 Tool Service의 **조회** | **유지** | `ToolBundleDiscovery`가 bundle마다 last-good을 따로 보관한다 |
| route 안 카탈로그 **교체** | **없음** | 사용 가능한 성공본이 없는 Tool Service가 하나라도 있으면 그 route 전체 교체를 거부한다 |
| route 간 **갱신** | **없음** | 아래 "남은 위험" 1번 참고. 한 route의 실패가 그 주기의 전 route 갱신을 막는다 |
| 프로세스 자원(connection pool, thread, heap) | **없음** | 전 route가 공유한다 |
| 배포·재기동·프로세스 장애 | **없음** | 전 route가 동시에 영향을 받는다 |
**ADR-0007이 지키려던 가용성 등급별 물리 분리는 이 구조에서 성립하지 않는다.** 등급 요구가 다시 생기면 이 ADR을 재검토한다(전제 2).
## 전제
아래가 깨지면 이 결정을 재검토한다.
1. route↔Tool Service 매핑의 관리 주체는 Portal이며, 매핑 변경이 MCP 재배포 없이 반영되어야 한다.
2. 가용성 등급별 물리 분리 요구가 없다.
3. 전 route의 Tool 총량과 매니페스트 조회 부하를 한 프로세스가 감당한다.
4. Portal은 신뢰 경계 안에 있고 공개 네트워크에 노출되지 않는다.
## 영향
- route 없는 `/mcp` 호출은 `route key is required`로 거부된다. Agent Builder에는 route별 URL만 등록한다.
- 등록되지 않은 route는 `McpRouteKeyValidator`가 controller 진입 전에 거부한다. 판단은 memory snapshot만 본다.
- Tool 이름 유일성은 **route 안에서만** 검사한다. 서로 다른 route에 같은 이름이 있어도 거부하지 않는다.
- `mcp.discovery.max-tools-total`은 전역이 아니라 **route 단위 상한**으로 동작한다. `merge()`가 route마다 호출되기 때문이다.
- Portal 조회 실패는 목록을 비우지 않는다. memory를 유지하고, cold start일 때만 `mcp.redis.portal-registry-key`의 Redis fallback을 읽는다.
- refresh 실패는 애플리케이션을 죽이지 않는다. `ToolRegistryRefreshScheduler``RuntimeException`을 잡아 warn 로그만 남긴다.
- [ADR-0009](ADR-0009-container-handles-public-mcp-path.md)의 "공개 path를 rewrite하지 않고 컨테이너가 직접 처리한다"는 유지된다. 다만 고정 `publicPath` 대신 `/mcp` + 동적 route로 처리하므로 결정 4만 이 ADR이 대체한다.
- [ADR-0002](ADR-0002-tool-exposure-and-single-call.md)의 Tool 노출 상한 50개는 Agent 기준 합계이므로 바뀌지 않는다.
## 남은 위험
이 결정을 확정하면서 코드가 아직 따라오지 못한 지점이다. 셋 다 이 ADR의 의도와 어긋나므로 기록해 둔다.
1. **route 간 갱신 격리가 없다.** `PortalToolRegistryClient.fetchAllTools()``bundlesByRoute`를 순회하며 route마다 `merge()`를 호출하는데, 한 route가 `TOOL_REGISTRY_UNAVAILABLE`을 던지면 예외가 `fetchAllTools()` 밖으로 나가 그 주기의 갱신이 통째로 중단된다. 기존 snapshot은 남으므로 목록이 비지는 않지만, **Tool Service 하나의 장애가 무관한 route의 Tool 변경 반영까지 막는다.** route 단위로 예외를 격리하고 실패한 route만 결과에서 빼야 한다.
2. **warm start가 Portal 모드에서 동작하지 않는다.** `ToolRegistryService.warmStartFromSharedCache()`는 route `""`의 Redis key만 읽는다. route가 이름을 갖는 이 구성에서는 아무것도 읽지 못해, 기동 직후 빈 목록 구간을 줄이는 효과가 사라진다.
3. **readiness가 route별 상태를 노출하지 않는다.** `ToolCatalogHealthIndicator``usableSnapshot`만 detail로 내보낸다. 어느 route가 준비됐고 어느 route가 비어 있는지 관제가 알 수 없다.
## 남은 판단
- `mcp.portal.route-key`가 선언돼 있으나 어떤 코드도 읽지 않는다. 결정 2에 따라 route는 URI에서만 오므로 이 속성은 제거 대상이다. 지금은 선언만 남아 있다.
- `deploy/helm/`의 배포별 topology와 `HelmDeploymentContractTest``mcp.bundles` 기반 1:1 구성의 계약이다. 코드에 그 경로가 남아 있어 local 검증과 1:1 배포에서는 유효하지만, 내부망 운영 대상인지 여부는 이 ADR이 정하지 않는다.
- readiness를 route 단위로 세분화할지는 운영 관측 이후에 다시 본다.
## 채택하지 않은 대안
**ADR-0007을 유지하고 배포마다 자기 route만 조회한다.** 격리는 지키지만 route 추가가 배포 추가가 된다. Portal이 route 목록의 원천인데 배포 topology가 그 목록을 따라가야 하므로 순환이 생긴다.
**`mcp.bundles`에 매핑을 하드코딩한다.** 매핑 변경마다 재배포가 필요해 전제 1과 충돌한다.
**route별로 프로세스를 나누고 각자 Portal을 조회한다.** 자원 격리는 얻지만 Portal이 route 목록을 소유하는 이상 배포 수를 Portal이 정하게 되어, 운영 중 route 추가가 배포 파이프라인을 건드린다.

28
docs/decisions/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Architecture Decision Records
장기 설계 결정은 ADR로 관리한다.
- `Proposed`: 초안. 검토 후 `Accepted`로 확정한다
- `Accepted`: 확정된 결정
- `Superseded`: 후속 ADR로 대체된 결정
- `Rejected`: 검토했으나 채택하지 않은 결정
결정이 변경되면 기존 ADR을 삭제하거나 의미를 덮어쓰지 않고 새 ADR에서 대체 관계를 기록한다.
## 결정 목록
| ADR | 제목 | 상태 |
|---|---|---|
| [ADR-0001](ADR-0001-stateless-execution-boundary.md) | Stateless 실행 책임 경계 | Accepted |
| [ADR-0002](ADR-0002-tool-exposure-and-single-call.md) | Agent Builder Tool 노출 제한과 단일 Tool 호출 | Accepted |
| [ADR-0003](ADR-0003-builder-tool-uid.md) | Builder Tool UID를 시스템 간 식별 키로 사용 | Superseded |
| [ADR-0004](ADR-0004-execution-guardrails.md) | 300초, Raw Data, unsafe retry 실행 가드레일 | Accepted |
| [ADR-0005](ADR-0005-standard-tool-name.md) | 표준 MCP Tool name을 실행 식별자로 사용 | Accepted |
| [ADR-0006](ADR-0006-no-authentication-in-mcp.md) | MCP Server는 인증·인가를 하지 않는다 | Accepted |
| [ADR-0007](ADR-0007-one-mcp-per-tool-service.md) | MCP 배포 하나는 Tool Service 하나만 본다 | Superseded |
| [ADR-0008](ADR-0008-shared-host-path-routing.md) | 공유 host의 path를 독립 MCP 배포로 연결 | Superseded |
| [ADR-0009](ADR-0009-container-handles-public-mcp-path.md) | 컨테이너가 공개 MCP path를 직접 처리 | Accepted |
| [ADR-0010](ADR-0010-tool-service-manifest-owns-execution-endpoint.md) | Tool 실행 endpoint를 Tool Service 매니페스트가 선언 | Accepted |
| [ADR-0013](ADR-0013-portal-owns-route-and-endpoint-registry.md) | Tool Server endpoint 목록과 route 매핑의 원천은 Portal | Accepted |
ADR-0011과 ADR-0012는 비어 있다. `feature/mcp-integration`이 Tool inputSchema 정책 두 건에 그 번호를 쓰고 있어, 문서를 합칠 때 충돌하지 않도록 예약해 둔 것이다.