# 설계서: PoC_4 MCP Discovery UI — KB VPD Streamable HTTP 연동 정비 (#620) > **상태**: Draft · source snapshot imported > **작성**: [AI] Architect · **최종수정**: 2026-07-14 > **추적성** — Redmine: #620 · 관련 ADR: 없음 > · 스냅샷: `poc4_active_source_20260714/` · 원본 archive: `poc4_active_source_20260714/poc4_active_source_20260714.tar.gz` · 목표 구현 파일: `apps/poc4/mcp_discovery_ui.py`, `config/mcp_servers.json`, VPD Backoffice의 `/mcp` endpoint · 테스트: PoC_4 단위 테스트 및 실제 MCP HTTP smoke test ## 1. 목적 (Why) PoC_4의 MCP Discovery UI가 KB VPD MCP를 사용자별 Bearer 토큰으로 안전하게 호출하면서도, 환경별 URL·전송 방식·오류 처리를 하나의 명확한 계약으로 관리한다. 현재 UI의 기본 호출 흐름은 동작한다. 다만 `kb_mcp` 설정에는 `endpoint_url`과 `base_url_env`가 함께 있고, UI는 `endpoint_url`을 우선 사용한다. 따라서 `KB_MCP_BASE_URL`을 바꿔도 실제 호출 대상이 바뀌지 않는다. 또한 `custom_python`은 로컬 8500 MCP용 이름이므로 외부 KB VPD MCP의 통신 계약을 설명하지 못한다. ## 2. 범위 (Scope) - **포함**: - `apps/poc4/mcp_discovery_ui.py`의 KB MCP endpoint, 인증 헤더, JSON-RPC, 오류 처리 정비 - `config/mcp_servers.json` 및 sample의 KB MCP 선언 정비 - KB MCP의 단일 도구 `ords.query.kb_select_ai_vpd` 호출 계약 문서화 - VPD Backoffice `/mcp`과의 HTTP 상태·프로토콜 버전 호환성 점검 및 필요한 최소 보완 - **제외 (out of scope)**: - 기존 `kb_vector_mcp` 및 `custom_python` 8500 RAG MCP의 변경 - VPD 권한 규칙, Select AI SQL 생성 규칙, ORDS 비즈니스 로직 변경 - OAuth Authorization Server, 사용자 로그인/토큰 발급 UI 재설계 - VPD 토큰 원문을 환경변수·레지스트리에 저장하는 방식 ## 3. 인수조건 (Acceptance Criteria) - [ ] KB MCP URL은 `KB_MCP_BASE_URL` 하나에서만 해석되고 `/mcp` path가 안전하게 결합된다. - [ ] `initialize`, `notifications/initialized`, `tools/list`, `tools/call`의 모든 HTTP 요청에 현재 선택된 사용자의 `Authorization: Bearer `만 전송된다. - [ ] Bearer 토큰은 JSON-RPC arguments, SQLite 대화 이력, UI 결과, 애플리케이션 로그에 저장·출력되지 않는다. - [ ] 호출 가능한 도구는 `ords.query.kb_select_ai_vpd` 하나이며, 인자는 `prompt`와 `limit`만 허용된다. - [ ] 단일 도구 구성에서는 LLM router를 호출하지 않고 허용 도구를 직접 선택한다. - [ ] 토큰 없음/위조/만료는 HTTP `401`, 유효 토큰의 권한 부족은 HTTP `403`으로 UI에 구분 표시된다. - [ ] 잘못된 Origin의 브라우저 요청은 MCP 서버에서 거부되며, 허용 Origin 및 Origin 없는 네이티브 MCP client 정책이 문서화된다. - [ ] 기존 `kb_vector_mcp` 및 로컬 8500 MCP 회귀 테스트가 통과한다. ## 4. 컨텍스트 & 제약 - KB VPD MCP endpoint: `https://kb.cloud-handson.com/mcp` - 보호 대상: `ords.query.kb_select_ai_vpd`는 ORDS를 거쳐 VPD 컨텍스트가 적용된 Select AI 조회를 실행한다. - 토큰 주체: VPD 권한은 정적 서비스 계정이 아니라 현재 선택된 `KB_STAKEHOLDERS` 사용자 토큰에 의해 결정된다. - UI의 VPD token preset 파일은 데모 편의 기능일 뿐이다. 운영에서는 OS 소유자 전용 권한(`0600`)으로 관리하고 형상관리·로그·SQLite에서 제외한다. - UI가 현재 사용하는 `2025-11-25` MCP protocol version과 서버의 지원 버전은 handshake에서 협상해야 한다. 지원하지 않는 버전을 무조건 강제하지 않는다. - 현재 서버는 stateless JSON-RPC POST 호출로도 동작한다. 서버가 `Mcp-Session-Id`를 발급하면 client는 이후 요청에만 그 값을 포함한다. ## 5. 아키텍처 개요 I/O는 Discovery UI의 HTTP transport와 VPD Backoffice `/mcp`에 한정한다. URL 결합, 허용 도구 검증, 요청·응답 검증, 안전한 오류 변환은 순수 함수로 분리해 네트워크 없이 테스트한다. ``` VPD 사용자 선택 / 토큰 입력 │ (원문은 요청 메모리에만 존재) ▼ PoC_4 MCP Discovery UI ├─ KB_MCP_BASE_URL + "/mcp" ├─ tool allowlist 검증 └─ Authorization: Bearer │ ▼ HTTPS JSON-RPC / Streamable HTTP VPD Backoffice MCP (/mcp) ├─ Origin·토큰 검증 ├─ tools/list: metadata only └─ tools/call: ords.query.kb_select_ai_vpd │ ▼ ORDS Select AI API → VPD context → Oracle ADB ``` ## 6. 데이터 모델 ### 6.1 KB MCP registry 선언 `config/mcp_servers.json`의 KB 선언은 다음 계약을 따른다. 값은 예시이며, 토큰 값은 넣지 않는다. ```json { "id": "kb_mcp", "enabled": true, "provider": "kb_vpd_streamable_http", "transport": "streamable_http", "base_url_env": "KB_MCP_BASE_URL", "endpoint_path": "/mcp", "auth_delivery": "per_request_vpd_bearer", "timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS", "default_tool": "ords.query.kb_select_ai_vpd", "tool_allowlist": ["ords.query.kb_select_ai_vpd"], "router_mode": "direct", "description": "KB VPD Select AI MCP; the current user's VPD bearer is sent only in the Authorization header." } ``` 환경 변수는 아래 두 값만 필요하다. ```dotenv KB_MCP_BASE_URL=https://kb.cloud-handson.com POC3_MCP_TIMEOUT_SECONDS=90 ``` `endpoint_url`, `token_env`, `POC3_MCP_TOKEN`, `BACKOFFICE_MCP_ACCESS_TOKEN`은 KB MCP 선언에 두지 않는다. URL은 registry에 하드코딩하지 않고, VPD 토큰은 사용자별 요청에서만 받는다. ### 6.2 MCP 요청 모든 요청은 다음 헤더를 사용한다. ```http Accept: application/json, text/event-stream Content-Type: application/json MCP-Protocol-Version: Authorization: Bearer ``` `tools/call` body의 `arguments`는 아래와 같이 제한한다. ```json { "name": "ords.query.kb_select_ai_vpd", "arguments": { "prompt": "담당 고객의 보험료 상세를 보여줘", "limit": 50 } } ``` 경계 검증 규칙: - tool name은 정확히 allowlist 값 하나와 일치해야 한다. - `prompt`는 문자열이며 서버와 동일한 최대 길이를 적용한다. - `limit`은 정수 `1..100`으로 clamp한다. - 토큰은 공백·`Bearer ` prefix를 정규화한 뒤 헤더에만 넣는다. - redirect는 허용하지 않는다. 다른 origin으로 Authorization이 전달되어서는 안 된다. ## 7. 함수 명세 (Function Specs) | 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? | |------|-----------|----------------|------|------|-----------|-------| | `resolve_kb_mcp_endpoint` | base URL과 고정 path를 안전하게 결합 | `(server, environ) -> str` | registry, env | HTTPS MCP URL | 누락/비정상 URL | 단순 | | `validate_kb_mcp_server` | KB 선언의 provider·transport·allowlist를 검증 | `(mapping) -> McpServer` | registry row | typed server | 계약 위반 | 단순 | | `mcp_headers` | 요청별 VPD bearer 헤더 생성 | `(token, version, session_id) -> dict` | 사용자 토큰 | 안전한 headers | 토큰 형식 오류 | 단순 | | `discover_kb_tools` | initialize 및 tools/list 후 단일 도구를 검증 | `(server, token) -> McpDiscoveryResult` | endpoint, token | tool descriptor | auth/protocol 오류 | **복잡** | | `call_kb_select_ai` | 고정 도구에 prompt·limit을 전달 | `(server, token, prompt, limit) -> result` | 사용자 요청 | tool result | auth/timeout/JSON-RPC 오류 | **복잡** | | `to_public_mcp_error` | HTTP/JSON-RPC 오류를 안전한 UI 메시지로 변환 | `(exception) -> PublicMcpError` | 내부 오류 | 사용자 메시지 | 원문 노출 금지 | 단순 | ## 8. 흐름 / 알고리즘 1. UI는 `KB_MCP_BASE_URL`을 읽고 `/mcp`만 결합한다. registry의 임의 `endpoint_url`은 KB 서버에 허용하지 않는다. 2. 사용자가 VPD token preset 또는 일회성 Bearer를 선택한다. 토큰은 현재 실행 변수에만 유지한다. 3. client는 `initialize`를 보내고 서버가 반환한 protocol version과 선택 가능한 session ID를 검증한다. 4. `notifications/initialized`를 보낸 뒤 `tools/list`를 실행한다. 5. 응답 목록이 정확히 허용 도구를 포함하는지, 해당 input schema가 `prompt`, `limit` 계약에 맞는지 확인한다. 6. KB 서버는 단일 도구이므로 router model을 호출하지 않고 `ords.query.kb_select_ai_vpd`를 직접 선택한다. 7. `tools/call`은 prompt·clamp된 limit만 body에 넣고 VPD Bearer는 Authorization에만 넣는다. 8. 결과는 화면용 안전 projection만 SQLite에 저장한다. Authorization 헤더와 원문 token은 저장하지 않으며, 진단이 필요하면 단방향 token fingerprint만 별도 보존할 수 있다. ## 9. 엣지케이스 & 에러 처리 | 상황 | client 처리 | 서버 기대 동작 | |------|-------------|----------------| | 토큰 없음 | 호출 전 안내, 네트워크 요청 없음 | 해당 없음 | | 토큰 위조·만료 | `401` → “토큰이 유효하지 않거나 만료됨” | `WWW-Authenticate` 포함 가능 | | 유효하지만 권한 없음 | `403` → “이 사용자에게 조회 권한 없음” | VPD fail-closed 유지 | | allowlist 밖 도구 | 호출 전 차단 | tools/call에서도 차단 | | 429 | 안전하게 재시도하지 않고 잠시 후 재시도 안내 | rate limit 정책 적용 | | timeout | tools/call 자동 재시도 금지 | request ID 기반 감사 추적 | | session ID 미발급 | stateless POST로 진행 | session을 요구하지 않음 | | session ID 발급 | 이후 요청에 `Mcp-Session-Id` 포함 | 세션 소유·만료 검증 | | redirect | 즉시 실패 | Authorization 전달 금지 | | Origin 불일치 | 브라우저 UI에 일반 오류 표시 | `403`으로 거부 | ## 10. 테스트 계획 - registry 단위 테스트 - `KB_MCP_BASE_URL`만으로 endpoint가 `https://kb.cloud-handson.com/mcp`가 되는지 검증 - KB registry에 `endpoint_url`, `token_env`, `custom_python`이 있으면 fail-closed 되는지 검증 - vector MCP 설정은 기존 형식으로 계속 로드되는지 검증 - HTTP transport 단위 테스트 - initialize/tools/list/tools/call 모두 Authorization header가 있고 JSON body에는 token key가 없는지 검증 - `401`, `403`, `429`, timeout, redirect, malformed JSON-RPC 응답을 안전한 메시지로 변환하는지 검증 - session header 반환/재전송 및 stateless fallback을 검증 - 통합 smoke test - 허용된 VPD 사용자 토큰으로 `tools/list`와 `tools/call` 성공 - 잘못된 토큰은 `401`, 타 사용자 권한은 `403` - 설계사와 지점장 토큰으로 동일 질문을 실행해 VPD 행/컬럼 결과가 서로 다른지 확인 - 비밀정보 점검 - chat SQLite, Streamlit log, 예외 메시지에서 토큰 원문 검색 결과 0건 ## 11. 리스크 & 대안 검토 - **선택**: KB MCP 전용 `kb_vpd_streamable_http` 선언을 도입하고, 로컬 8500용 `custom_python`과 분리한다. 외부 HTTPS/VPD Bearer 계약을 코드와 운영 화면에서 명확히 할 수 있다. - **대안 1 — 기존 `custom_python` 재사용**: 동작은 시킬 수 있으나 provider 이름과 endpoint 제약이 실제 KB 서버와 맞지 않아 로컬 MCP와 외부 VPD MCP가 섞인다. - **대안 2 — 고정 MCP access token 사용**: 구현은 간단하지만 모든 사용자가 동일 VPD 주체가 되어 데이터 권한 분리가 무너진다. 채택하지 않는다. - **대안 3 — 즉시 OAuth 2.1 전환**: 표준 상호운용성에는 유리하지만 현재 데모의 VPD token 발급·검증 체계를 대체하므로 별도 인증 서버 설계가 필요하다. - 롤백: 새 registry 선언을 비활성화하고 기존 KB 선언을 복원한다. DB VPD 정책·ORDS endpoint·토큰 데이터는 변경하지 않는다. ## 12. 미해결 질문 (Open Questions) - VPD Backoffice MCP endpoint가 현재 지원할 MCP protocol version을 어떤 값으로 공식 고정할지 결정이 필요하다. - Streamable HTTP의 GET/SSE 및 `Mcp-Session-Id`를 완전 지원할지, stateless POST profile로 운영할지 결정이 필요하다. - 데모 이후 사용자 VPD bearer를 OAuth 2.1 access token으로 전환할지, 현 토큰을 resource-server token으로 계속 운영할지 결정이 필요하다. - chat 대화 이력의 `basis_json`/`details_json`에 민감 데이터 보존 기간과 삭제 정책을 별도로 정해야 한다. ## 13. 2026-07-14 소스 스냅샷 반입 배포 서버의 PoC4 활성 화면 소스를 현재 저장소에 별도 폴더로 반입했다. - 원격 실제 위치: `/home/opc/poc_4/poc4_active_source_20260714.tar.gz` - 사용자 제시 경로 `/home/opc/poc_4/poc4_active_source_20260714/poc4_active_source_20260714.tar.gz`에는 파일이 없었고, 실제 archive는 `/home/opc/poc_4/` 바로 아래에 있었다. - 저장소 위치: `poc4_active_source_20260714/` - 포함 파일: Streamlit UI, MCP router, OCI GenAI client, 모델 profile, token preset sample, 기동/status script, requirements - 보안 확인: 실제 `.env`, 실제 VPD token 원문, 대화 SQLite DB는 포함하지 않았다. `vpd_token_presets.json`에는 placeholder만 있다. - DB 참조 경로: VPD 개발본 배포 서버에서는 `/home/opc/kbmcp/.env`의 접속 정보를 사용하고, wallet directory는 `/home/opc/wallet/kbaipoc`를 사용한다. 두 경로의 파일 내용은 저장소에 포함하지 않는다. - 런타임 전제: Python 3.11 이상. 현재 개발 서버 기본 `python3`는 3.6.8이므로 이 소스의 문법 검증에는 맞지 않는다. - 검증: 배포 서버 원본 경로에서 PoC4 전용 Python `3.11.15`로 `py_compile` 통과. 기존 Java 백오피스 `mvn -q test` 통과. 주의: 이 반입은 소스 스냅샷 보관이다. 아직 본 설계서의 목표 계약인 `kb_vpd_streamable_http`, `KB_MCP_BASE_URL` 단일 endpoint 해석, 단일 도구 direct routing을 구현 완료했다는 의미는 아니다.