mcp-server
README.md
# MCP Server
범용 **MCP(Model Context Protocol) 서버**입니다. 내부/외부/OpenAPI 기반 API를 MCP Tool로 등록하고, MCP 클라이언트(예: Codex, Claude 등 MCP 지원 클라이언트)에 **Streamable HTTP** 전송으로 노출합니다. 관리자 Portal, RBAC/세션 인증, OpenAPI Import, 외부 **Credential Service** 연동을 통한 Credential 주입, 감사 로그를 제공합니다.
> 문서 기준: 이 README는 저장소의 실제 소스코드(`app/`, `alembic/`, `pyproject.toml`, `Dockerfile`, `docker-compose.yml`, `Jenkinsfile`, `.env.example`/`env.template`)를 직접 분석해 작성되었습니다. 코드로 확인되지 않은 내용은 "확인 불가"로 표기합니다.
---
## 1. 프로젝트 소개
- **목적**: 이질적인 백엔드 API들(수동 등록 / 내부 핸들러 / OpenAPI 문서 Import)을 하나의 MCP 서버로 통합해, MCP 클라이언트가 표준 프로토콜(`tools/list`, `tools/call`)로 도구를 발견·실행할 수 있게 합니다.
- **해결하는 문제**: 도구별로 제각각인 인증/노출/정책을 서버가 중앙에서 관리하고, 위험도·Scope·사용자별 선택·확인(confirmation) 정책과 감사 로그를 일관되게 적용합니다.
- **MCP Server의 역할**: 도구 레지스트리 + 정책 게이트 + 실행 오케스트레이터. 업스트림 API로의 HTTP 실행과 감사 로깅을 담당합니다.
- **Credential Service와의 관계**: 실제 자격증명(Secret)은 이 서버가 보관하지 않습니다. 외부 **Credential Service**(HTTP API)를 호출해 호출자 신원을 확인(`/api/v1/me`)하고, 필요 시 단기 토큰 교환 후 Credential을 reveal 받아 업스트림 요청에 주입합니다. MCP Server는 HashiCorp Vault를 **직접 호출하지 않습니다**(아래 3·6절 참고).
---
## 2. 주요 기능
실제 구현이 확인된 기능만 기재합니다. 다수 실행·인증 기능은 **feature flag로 감싸여 있고 코드 기본값은 보수적으로 off**입니다(11절 표 참고).
- **MCP Tool discovery** — DB에 등록된 도구를 운영자 노출 정책·사용자별 선택·Scope에 따라 `tools/list`로 동적 노출 (`app/mcp/registry.py`, `app/mcp/discoverability.py`)
- **MCP Tool execution** — `tools/call` 시 다단계 정책 게이트 통과 후 업스트림 HTTP 호출 (`app/services/execution/service.py`, `http_dispatcher.py`) *— 기본 비활성(`MCP_TOOL_EXECUTION_ENABLED=false`)*
- **OpenAPI Import** — OpenAPI 문서(URL/파일)를 가져와 도구를 생성/동기화. 신규 도구는 비활성/비노출로 들어와 관리자가 검토 후 활성화 (`app/api/v1/openapi_sources.py`)
- **사용자별 Tool 선택** — 사용자가 자신에게 노출할 도구를 선택 (`user_tool_settings`, `app/services/user_tool_service.py`)
- **관리자 Tool/Provider 관리** — 등록·활성화·MCP 노출·Portal 노출·정책 설정·삭제 (`app/api/v1/tools.py`, `providers.py`)
- **Scope 및 위험도 정책** — 도구별 `required_scopes`, `RiskLevel`, `requires_confirmation` (`app/db/models/policy.py`, `app/domain/scopes.py`)
- **Credential 주입** — REQUEST_HEADER_PASSTHROUGH / Credential Service reveal 주입 (`app/services/execution/`) *— 기본 비활성*
- **감사 로그** — 모든 `tools/list`/`tools/call`을 RECEIVED→완료로 기록, Secret 마스킹 (`app/db/models/audit.py`, `app/services/execution/audit.py`)
- **관리자 Portal / 사용자 Portal** — 서버사이드 렌더링 HTML(Jinja2) + 세션 인증 (`app/admin/`, `app/web/`)
---
## 3. 전체 아키텍처
```mermaid
flowchart LR
Client[MCP Client]
Server[MCP Server<br/>FastAPI + MCP SDK]
DB[(PostgreSQL<br/>schema: mcp)]
Cred[Credential Service<br/>외부 HTTP API]
Upstream[업스트림 Tool API<br/>HTTP]
Vault[HashiCorp Vault]
Client -- "Streamable HTTP /mcp/" --> Server
Server -- "SQLAlchemy async / asyncpg" --> DB
Server -- "/me, token/exchange, reveal" --> Cred
Server -- "도구 실행(HTTP)" --> Upstream
Cred -. "이 저장소 범위 밖(코드로 확인 불가)" .-> Vault
```
- MCP Server는 **Credential Service만** 호출하며, HashiCorp Vault로의 직접 연결선은 없습니다(코드 전반에서 `hvac`/`VAULT_ADDR`/`VAULT_TOKEN`/`hashicorp` 사용 0건).
- Credential Service가 내부적으로 Vault를 쓰는지는 이 저장소 코드로 확인할 수 없어 점선·"확인 불가"로 표기했습니다.
**계층(레이어)**
- `app/api`, `app/web`, `app/admin` — HTTP 진입점(REST v1 / 세션 웹 / 관리자 HTML)
- `app/mcp` — MCP 전송·서버 핸들러·도구 매핑·발견/정렬
- `app/services` — 실행·인증·감사·사용자도구·OpenAPI 등 도메인 서비스
- `app/repositories` — DB 접근 계층
- `app/db`, `alembic` — 모델/스키마/마이그레이션
- `app/domain`, `app/dto`, `app/core` — 열거형·스코프·DTO·설정·로깅·예외
---
## 4. 요청 처리 흐름
실제 코드(`app/mcp/server.py`, `app/services/execution/service.py`) 기준입니다.
### tools/list
```text
MCP 요청 (/mcp/)
→ DB 세션 확인 (없으면 registry_unavailable 오류)
→ (MCP_MCP_CALLER_AUTH_REQUIRED=true일 때) 호출자 인증: API Key 헤더 → Credential Service GET /api/v1/me → status=ACTIVE 확인
→ Principal 생성(McpPrincipal: scopes/roles/email/status)
→ 사용자별 Tool 선택 정책 적용(ADMIN=전체, USER=본인 enable 목록, 매핑 없으면 0개 = fail-closed)
→ 운영자 노출 정책(SQL): provider/tool enabled·미삭제·ACTIVE, tool.mcp_exposed=true, sync_status≠STALE, policy enabled
→ Scope 필터: required_scopes ⊆ caller scopes 인 도구만
→ 입력 스키마 검증 통과분만
→ 페이지네이션(cursor, 상한 MCP_MCP_MAX_DISCOVERABLE_TOOLS) → 목록 반환
```
- 인증 실패 시 DENIED 감사 로그 기록 후 오류 반환.
### tools/call
```text
MCP 요청 (/mcp/)
→ RECEIVED 감사 로그 기록
→ (필요 시) 호출자 인증
→ 전역 실행 플래그 확인(MCP_TOOL_EXECUTION_ENABLED)
→ 도구 조회(노출명) / 삭제·비활성 확인
→ MCP 노출 게이트(enforce_mcp_exposed)
→ 사용자별 선택 게이트(USER는 본인 enable 도구만)
→ Provider/Policy 게이트
→ 실행 타입 게이트(HTTP만 지원)
→ Scope 인가(required_scopes 검사)
→ 인자 검증(OFF/WARN/STRICT)
→ 확인 게이트(requires_confirmation=true & confirm≠true → TOOL_CONFIRMATION_REQUIRED) *Secret 호출 이전*
→ Credential 주입 판단(NONE / REQUEST_HEADER_PASSTHROUGH / CREDENTIAL_SERVICE_CREDENTIAL_INJECTION)
→ 업스트림 HTTP 실행(HttpToolDispatcher)
→ 결과 Secret 마스킹(fail-closed) → 감사 로그 완료 기록 → 결과 반환
```
- 감사 기록은 **fail-open**(감사 실패가 응답을 막지 않음), 그 외 정책은 대부분 **fail-closed**.
- 구분해야 할 개념: **전역 MCP 호출자 인증**(누가 호출했나 — Credential Service `/me`) ≠ **도구별 upstream credential 주입**(어떤 도구에 자격증명을 어떻게 붙이나). `REQUEST_HEADER_PASSTHROUGH`(inbound 헤더 변환)와 `CREDENTIAL_SERVICE_CREDENTIAL_INJECTION`(reveal 값 QUERY 주입)은 별개입니다.
---
## 5. 인증 및 권한 구조
| 방식 | 헤더/전송 | 적용 대상 |
|---|---|---|
| Credential Service API Key (`cs_...`) | `X-Credential-Service-Api-Key` (기본, `MCP_CREDENTIAL_SERVICE_API_KEY_HEADER`) | MCP 요청(`/mcp/`) |
| Bearer(`cs_...` API Key 또는 CS 발급 JWT) | `Authorization: Bearer <token>` | REST 관리 API |
| 세션 쿠키 + CSRF | `SessionMiddleware` 쿠키 + `X-CSRF-Token` | 관리자/사용자 웹 Portal |
- **API Key prefix**: `cs_` (`app/api/rest_auth.py`). `cs_`로 시작하면 API Key 경로로 처리.
- **JWT**: MCP는 JWT를 **발급하지 않고 검증만** 합니다. 알고리즘 **HS256(대칭키)** 전용, 발급자/청중은 `MCP_JWT_ISSUER`/`MCP_JWT_AUDIENCE`(기본 `credential-service`), 시크릿은 `MCP_JWT_SECRET`(32자 이상 필수). REST 경로는 `/me` 재확인으로 JWT `sub`와 일치하는지도 검증.
- **Principal**: Credential Service `/me` 프로필로 생성(`McpPrincipal`/`RestPrincipal`) — scopes/roles/email/status 등 보유.
- **Scope**(정확히 7개, `app/domain/scopes.py`): `resources:read`, `credentials:reveal`, `servers.read`, `ssh.execute`, `logs.read`, `status.read`, `disk.read`. 정확 일치(와일드카드/대소문자 무시 없음).
- **Role**(`UserRole`): `USER`, `ADMIN`. REST 관리 API·관리자 페이지는 ADMIN 필요. 마지막 활성 관리자 강등/비활성 및 자기 자신 강등 방지.
- **상태 검사**: MCP는 `/me` status=ACTIVE, REST/웹은 `McpUser.is_active` 및 `deleted_at IS NULL`.
- **인증 실패 응답**: CS 비활성 → 403, 무효/취소/검증불가(타임아웃·5xx 포함) → 401, JWT 설정 불가 → 503, 미등록/비활성 사용자 → 403, DB 미구성 → 503.
- **Secret 처리 원칙**: 원본 자격증명/Authorization/API Key는 로그·감사에 저장하지 않음. 예시 키는 placeholder(`cs_<...>`)만 사용.
> 기본값 주의: `MCP_MCP_CALLER_AUTH_REQUIRED`, `MCP_WEB_AUTH_ENABLED`, `MCP_JWT_ENABLED`는 모두 기본 **false**입니다. 운영에서는 명시적으로 활성화해야 합니다.
---
## 6. Credential Service 연동
MCP Server와 Credential Service의 책임 경계:
- **MCP Server**: 정책 판단, 도구 실행, 감사. 자격증명 원본을 저장하지 않음.
- **Credential Service(외부)**: 사용자 신원(`/api/v1/me`), API Key→단기 JWT 교환(`/api/v1/auth/token/exchange`), Credential reveal(`/api/v1/my/resources/{resource_id}/credentials/reveal`).
동작:
- **신원 확인**: `GET {base}/api/v1/me` — `Authorization: ApiKey <cs_...>` 또는 `Bearer <JWT>`.
- **토큰 교환**: 요청의 `cs_` API Key → 단기 Exchange JWT(`Authorization: ApiKey <cs_...>`로 요청). TTL 상한 3600초, **재시도 없음**.
- **Credential 주입**: `CREDENTIAL_SERVICE_CREDENTIAL_INJECTION`은 (교환 JWT로) reveal 후 **QUERY 파라미터**로만 주입. `REQUEST_HEADER_PASSTHROUGH`는 허용 목록 헤더를 업스트림 허용 헤더로 전달(CR/LF 인젝션 차단).
- **평문 노출 방지**: reveal 호출은 **단기 Bearer JWT로만**(원본 `cs_` 키는 reveal 엔드포인트로 전송하지 않음). reveal 값은 요청 스코프에서 기억되어 외부 반환 경계에서 마스킹(fail-closed).
- **타임아웃/재시도**: connect 5.0s / read 10.0s / token-exchange 10.0s, 응답 상한 256KiB, 재시도 없음.
- **관련 환경변수**: `MCP_CREDENTIAL_SERVICE_BASE_URL`, `MCP_AUTH_BASE_URL`(미설정 시 base_url fallback), `MCP_CREDENTIAL_SERVICE_API_KEY_HEADER`, `MCP_CREDENTIAL_SERVICE_TOKEN_EXCHANGE_ENABLED`/`_PATH`, `MCP_CREDENTIAL_SERVICE_CREDENTIAL_INJECTION_ENABLED`, `MCP_MCP_CALLER_AUTH_REQUIRED` 등(11절 표 참고).
> **HashiCorp Vault 직접 연결 없음**: 이 저장소 코드는 Vault SDK/주소/토큰을 전혀 사용하지 않습니다. 자격증명 접근은 오직 위 3개 Credential Service HTTP 엔드포인트를 통합니다.
---
## 7. 데이터베이스 구조
- **엔진/ORM**: PostgreSQL + SQLAlchemy 2.0 **비동기**(asyncpg). DB 미구성 시 "bootstrap 모드"로 기동(엔진 없음).
- **전용 스키마**: `mcp`(`MCP_DATABASE_SCHEMA`). 모든 테이블이 스키마 한정, `search_path`도 해당 스키마로 고정(ADR-0004). 스키마명 검증 정규식 `^[a-z][a-z0-9_]{0,62}$`, 예약/공용 스키마 거부.
- **공통 mixin**: UUID PK, 소프트 삭제(`deleted_at`), 타임스탬프. 활성 유니크 제약은 대개 `WHERE deleted_at IS NULL` 부분 유니크.
핵심 엔티티 관계:
```mermaid
erDiagram
mcp_providers ||--o{ mcp_tools : "provider_id (RESTRICT)"
mcp_providers ||--o| mcp_openapi_sources : "provider_id (CASCADE, unique)"
mcp_tools ||--o| mcp_tool_http_configs : "tool_id (CASCADE, 1:1)"
mcp_tools ||--o| mcp_tool_policies : "tool_id (CASCADE, 1:1)"
mcp_users ||--o{ user_tool_settings : "user_id (CASCADE)"
mcp_tools ||--o{ user_tool_settings : "tool_id (CASCADE)"
mcp_users ||--o{ mcp_users : "created_by_user_id (SET NULL)"
mcp_tools ||--o{ mcp_audit_logs : "tool_id (SET NULL)"
mcp_providers ||--o{ mcp_audit_logs : "provider_id (SET NULL)"
```
| 모델 | 테이블 | 설명 |
|---|---|---|
| `Provider` | `mcp_providers` | 도구 제공자(업스트림). 인증 설정 보유 |
| `Tool` | `mcp_tools` | 도구. `mcp_exposed`/`portal_exposed`/인증 override 등 |
| `ToolHttpConfig` | `mcp_tool_http_configs` | 도구 HTTP 실행 설정(1:1) |
| `ToolPolicy` | `mcp_tool_policies` | 정책(category/risk/`required_scopes`/`requires_confirmation`)(1:1) |
| `OpenApiSource` | `mcp_openapi_sources` | Provider의 OpenAPI Import 소스(0..1) |
| `McpUser` | `mcp_users` | 로컬 사용자(USER/ADMIN), Argon2id 해시 |
| `UserToolSetting` | `user_tool_settings` | 사용자별 도구 enable(`is_enabled`) |
| `AuditLog` | `mcp_audit_logs` | 감사 로그(append-only, 스냅샷 보존, 삭제 시 FK는 SET NULL) |
- **감사 로그**: RECEIVED 행 생성 후 완료 시 상태/결과 UPDATE. 원본 자격증명/응답 본문은 저장하지 않고 마스킹된 인자·요약만 저장. **보존/TTL 정책은 코드에 없음(확인 불가)**.
- **정책 저장**: `required_scopes`(JSONB, 기본 `[]`)는 저장·검증하지만 강제는 실행 서비스 게이트에서 수행. `requires_confirmation`은 저장만 하며, 대화형 elicitation 프로토콜이 아니라 실행 게이트(`confirm` 인자)로 적용.
---
## 8. 프로젝트 디렉터리 구조
핵심 경로만 표기합니다.
```text
mcp-server/
├─ app/
│ ├─ main.py # FastAPI 팩토리(create_app), MCP 마운트, 미들웨어/OpenAPI 커스터마이즈
│ ├─ core/ # config(설정), logging(로깅·Secret redact), security, exceptions
│ ├─ api/ # REST 진입점: rest_auth, deps, health, v1/(tools, providers, users, ...)
│ ├─ web/ # 세션 로그인·Portal 라우트·RBAC 가드
│ ├─ admin/ # 관리자 HTML 라우트 + templates/(SSR)
│ ├─ mcp/ # MCP transport(Streamable HTTP)·server 핸들러·registry·tool_mapper·sanitizer
│ ├─ services/ # execution(실행/인증/감사/credential), auth(jwt/principal), user_tool_service, openapi
│ ├─ repositories/ # DB 접근 계층
│ ├─ db/ # base, session, schema, models/(providers/tools/policy/audit/users ...)
│ ├─ domain/ # enums, scopes, provider/credential_binding
│ ├─ dto/ # 요청/응답 DTO
│ └─ scripts/ # seed_initial_users
├─ alembic/ # env.py(스키마 한정 마이그레이션) + versions/(리비전 13개)
├─ tests/ # unit / integration / e2e / fixtures + conftest.py
├─ docs/ # architecture / operations / decisions(ADR) / TODO.md
├─ scripts/ # ci_check.sh, phase 검증 스크립트, update_kustomize_image_tag.sh
├─ Dockerfile # 멀티스테이지(python:3.12-slim, uv)
├─ docker-compose.yml # 로컬 db(postgres:16) + app
├─ Jenkinsfile # CI(빌드/푸시/배포 repo 갱신) — 값은 예시 placeholder
├─ pyproject.toml # 의존성/도구 설정(ruff/mypy/pytest)
└─ uv.lock
```
---
## 9. 요구 환경
- **OS**: Linux / WSL(개발), 컨테이너는 `python:3.12-slim`
- **Python**: `>=3.12` (`pyproject.toml`)
- **패키지 매니저**: `uv`(astral), `uv.lock` 사용
- **PostgreSQL**: 실제 DB 기능 사용 시 필요(compose는 `postgres:16-alpine`)
- **선택적 외부 서비스**: Credential Service(자격증명 연동 사용 시)
- **Docker / Docker Compose**: 로컬 실행·빌드 시(선택)
주요 라이브러리: FastAPI, uvicorn, MCP SDK(`mcp`), SQLAlchemy 2.0(asyncio)+asyncpg, Alembic, pydantic/pydantic-settings, httpx, PyJWT(검증), pwdlib[argon2], itsdangerous, jsonschema, Jinja2.
---
## 10. 로컬 실행
명령은 프로젝트 루트에서 실행합니다. 표기 `[WSL zsh]`는 로컬 셸입니다.
1. 저장소 Clone
```bash
# [WSL zsh]
git clone <YOUR_REPO_URL> mcp-server && cd mcp-server
```
2. 의존성 설치
```bash
# [WSL zsh]
uv sync --dev
```
3. 환경변수 준비 (템플릿 복사 후 값 채우기; 실제 Secret은 커밋 금지)
```bash
# [WSL zsh]
cp .env.example .env
```
4. DB 준비 (로컬 Postgres 컨테이너)
```bash
# [Docker]
docker compose up -d db
```
5. 마이그레이션 (DB 접속 환경변수 설정 후)
```bash
# [WSL zsh]
uv run alembic upgrade head
```
6. (선택) 초기 사용자 seed — `MCP_SEED_*` 환경변수 설정 후
```bash
# [WSL zsh]
uv run python -m app.scripts.seed_initial_users
```
7. 서버 실행
```bash
# [WSL zsh]
uv run uvicorn app.main:app --host 0.0.0.0 --port 8080
```
8. health 확인
```bash
# [WSL zsh]
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/ready
```
9. OpenAPI / MCP 확인
```bash
# [WSL zsh]
curl http://127.0.0.1:8080/openapi.json # Swagger UI: http://127.0.0.1:8080/docs
# MCP 엔드포인트(정규 URL): http://127.0.0.1:8080/mcp/ (Streamable HTTP)
```
> ⚠ 현재 소스 상태에서는 아래 18절의 알려진 이슈(Enum 길이)로 인해 **애플리케이션 import가 실패**합니다. 위 실행 명령들은 코드 정의상 유효하지만, 실제 기동을 위해서는 해당 이슈 해결이 선행되어야 합니다.
---
## 11. 환경변수
접두사 `MCP_` + 대문자 필드명. 값은 `app/core/config.py`의 `Settings` 기준(실제 코드 우선). 실제 Secret 값은 표기하지 않습니다. "필수"는 미설정 시 해당 기능이 동작하지 않는 값을 의미합니다.
### 애플리케이션
| 변수 | 필수 | 기본값 | 설명 | 민감 |
|---|---|---|---|---|
| MCP_APP_NAME | 아니오 | mcp-server | 서비스명 | 아니오 |
| MCP_VERSION | 아니오 | 0.1.0 | 버전 | 아니오 |
| MCP_ENV | 아니오 | local | local/dev/prod | 아니오 |
| MCP_LOG_LEVEL | 아니오 | INFO | 로그 레벨 | 아니오 |
| MCP_HOST | 아니오 | 0.0.0.0 | bind host | 아니오 |
| MCP_PORT | 아니오 | 8080 | bind port | 아니오 |
| MCP_DATABASE_SCHEMA | 아니오 | mcp | 전용 PG 스키마(검증됨) | 아니오 |
### 데이터베이스
| 변수 | 필수 | 기본값 | 설명 | 민감 |
|---|---|---|---|---|
| MCP_DB_HOST | DB 사용 시 | (없음) | PG 호스트 | 아니오 |
| MCP_DB_PORT | 아니오 | 5432 | PG 포트 | 아니오 |
| MCP_DB_USER | DB 사용 시 | (없음) | PG 사용자 | 아니오 |
| MCP_DB_PASSWORD | DB 사용 시 | (없음) | PG 비밀번호 | **예** |
| MCP_DB_NAME | DB 사용 시 | (없음) | PG DB명 | 아니오 |
### 인증 / JWT / 세션
| 변수 | 필수 | 기본값 | 설명 | 민감 |
|---|---|---|---|---|
| MCP_WEB_AUTH_ENABLED | 아니오 | false | 웹/관리자 인증 활성 | 아니오 |
| MCP_SESSION_SECRET | web_auth 시 필수 | (없음) | 세션 서명 시크릿 | **예** |
| MCP_SESSION_COOKIE_NAME | 아니오 | mcp_session | 세션 쿠키명 | 아니오 |
| MCP_SESSION_TTL_SECONDS | 아니오 | 3600 | 세션 TTL | 아니오 |
| MCP_COOKIE_SECURE | 아니오 | false | Secure 쿠키 | 아니오 |
| MCP_COOKIE_SAMESITE | 아니오 | lax | SameSite | 아니오 |
| MCP_PASSWORD_MIN_LENGTH | 아니오 | 8 | 비밀번호 최소 길이 | 아니오 |
| MCP_PASSWORD_MAX_LENGTH | 아니오 | 128 | 비밀번호 최대 길이 | 아니오 |
| MCP_JWT_ENABLED | 아니오 | false | JWT 검증 활성 | 아니오 |
| MCP_JWT_ALGORITHM | 아니오 | HS256 | HS256만 허용 | 아니오 |
| MCP_JWT_SECRET | jwt 시 필수 | (없음) | JWT 검증 시크릿(≥32자) | **예** |
| MCP_JWT_ISSUER | 아니오 | credential-service | 발급자 | 아니오 |
| MCP_JWT_AUDIENCE | 아니오 | credential-service | 청중 | 아니오 |
### MCP
| 변수 | 필수 | 기본값 | 설명 |
|---|---|---|---|
| MCP_MCP_ENABLED | 아니오 | true | MCP 전송 마운트 |
| MCP_MCP_PATH | 아니오 | /mcp | 마운트 경로(정규 URL `/mcp/`) |
| MCP_MCP_MAX_DISCOVERABLE_TOOLS | 아니오 | 500 | 노출 도구 상한 |
| MCP_MCP_PAGE_SIZE | 아니오 | 100 | 페이지 크기 |
| MCP_MCP_JSON_RESPONSE | 아니오 | false | 단일 JSON vs SSE |
| MCP_MCP_DNS_REBINDING_PROTECTION | 아니오 | false | Host 헤더 보호 |
| MCP_MCP_ALLOWED_HOSTS | 아니오 | [] | 허용 Host(콤마 구분) |
| MCP_MCP_CALLER_AUTH_REQUIRED | 아니오 | false | MCP 호출자 인증 요구 |
### Credential Service
| 변수 | 필수 | 기본값 | 설명 | 민감 |
|---|---|---|---|---|
| MCP_CREDENTIAL_SERVICE_BASE_URL | 연동 시 | (없음) | Credential Service base URL | 아니오 |
| MCP_AUTH_BASE_URL | 아니오 | (없음, base_url fallback) | 인증(/me·exchange) base URL | 아니오 |
| MCP_CREDENTIAL_SERVICE_API_KEY_HEADER | 아니오 | X-Credential-Service-Api-Key | 요청 API Key 헤더명 | 아니오 |
| MCP_CREDENTIAL_SERVICE_CREDENTIAL_INJECTION_ENABLED | 아니오 | false | reveal 주입 활성 | 아니오 |
| MCP_CREDENTIAL_SERVICE_TOKEN_EXCHANGE_ENABLED | 아니오 | false | 토큰 교환 활성 | 아니오 |
| MCP_CREDENTIAL_SERVICE_TOKEN_EXCHANGE_PATH | 아니오 | /api/v1/auth/token/exchange | 교환 경로 | 아니오 |
| MCP_CREDENTIAL_SERVICE_PROFILE_PATH | 아니오 | /api/v1/me | 프로필 경로 | 아니오 |
| MCP_CREDENTIAL_SERVICE_CONNECT_TIMEOUT_SECONDS | 아니오 | 5.0 | connect timeout | 아니오 |
| MCP_CREDENTIAL_SERVICE_READ_TIMEOUT_SECONDS | 아니오 | 10.0 | read timeout | 아니오 |
| MCP_CREDENTIAL_SERVICE_TOKEN_EXCHANGE_TIMEOUT_SECONDS | 아니오 | 10.0 | 교환 timeout | 아니오 |
| MCP_CREDENTIAL_SERVICE_MAX_RESPONSE_BYTES | 아니오 | 262144 | 응답 상한 | 아니오 |
| MCP_ALLOWED_CREDENTIAL_SERVICE_CREDENTIAL_FIELDS | 아니오 | api_key,token,refresh_token,client_secret,webhook_secret | reveal 허용 필드 | 아니오 |
### Tool 정책 / Credential passthrough
| 변수 | 기본값 | 설명 |
|---|---|---|
| MCP_TOOL_EXECUTION_ENABLED | false | 도구 실행 전역 활성 |
| MCP_REQUEST_CREDENTIAL_PASSTHROUGH_ENABLED | false | 헤더 passthrough 활성 |
| MCP_ALLOWED_CREDENTIAL_SOURCE_HEADERS | [X-Credential-Service-Api-Key] | 소스 헤더 허용목록 |
| MCP_ALLOWED_PASSTHROUGH_TARGET_HEADERS | [Authorization] | 대상 헤더 허용목록 |
| MCP_HTTP_EXEC_CONNECT_TIMEOUT_SECONDS | 5.0 | 실행 connect timeout |
| MCP_HTTP_EXEC_MAX_RESPONSE_BYTES | 2097152 | 실행 응답 상한 |
### 감사 로그
| 변수 | 기본값 | 설명 |
|---|---|---|
| MCP_AUDIT_LOG_ENABLED | true | 감사 로그 활성 |
| MCP_AUDIT_MAX_TEXT_LENGTH | 2000 | 텍스트 최대 길이 |
### OpenAPI Import
`MCP_OPENAPI_FETCH_CONNECT_TIMEOUT_SECONDS`(5.0), `MCP_OPENAPI_FETCH_READ_TIMEOUT_SECONDS`(15.0), `MCP_OPENAPI_MAX_DOCUMENT_BYTES`(5242880), `MCP_OPENAPI_MAX_UPLOAD_BYTES`(5242880), `MCP_OPENAPI_MAX_REDIRECTS`(3), `MCP_OPENAPI_ALLOW_PRIVATE_NETWORKS`(false), `MCP_OPENAPI_MAX_OPERATIONS`(500), `MCP_OPENAPI_MAX_REF_DEPTH`(20), `MCP_OPENAPI_MAX_SCHEMA_DEPTH`(30).
### 개발/테스트 (Settings 클래스 밖, seed 스크립트가 직접 사용)
`MCP_SEED_ADMIN_USERNAME`, `MCP_SEED_ADMIN_PASSWORD`(**민감**), `MCP_SEED_USER_USERNAME`, `MCP_SEED_USER_PASSWORD`(**민감**), `MCP_SEED_UPDATE_EXISTING`(기본 false).
> 코드·템플릿 불일치: `.env.example`/`env.template`는 위 변수 중 일부(예: 웹 세션 그룹, 일부 Credential Service 세부 변수, `MCP_AUTH_BASE_URL`, `MCP_VERSION`, `MCP_SEED_*`)를 생략합니다. 생략된 변수는 코드 기본값을 따릅니다.
---
## 12. 데이터베이스 Migration
- **도구**: Alembic(비동기 엔진). `alembic.ini`의 `sqlalchemy.url`은 비워두고 `alembic/env.py`가 앱 설정에서 주입.
- **스키마 한정**: `version_table_schema=mcp`, autogenerate가 `mcp` 스키마만 대상으로 함(다른 서비스 객체 보호). 온라인 실행 시 마이그레이션 전에 스키마를 별도 트랜잭션으로 생성.
- **리비전 수**: `alembic/versions/` 13개. 각 리비전에 `upgrade()`/`downgrade()` 구현되어 있음(스텁 아님).
```bash
# [WSL zsh] (DB 접속 환경변수 설정 후)
uv run alembic upgrade head # 최신 스키마 적용
uv run alembic revision --autogenerate -m "메시지" # 리비전 생성(개발)
```
- **downgrade**: 리비전에 구현되어 있으나 프로젝트 문서/CI에서 운영 downgrade 절차는 규정하지 않음. 운영 적용 시 스키마 격리·별도 트랜잭션 생성 특성을 확인하세요.
- 컨테이너/CI는 마이그레이션을 자동 실행하지 않습니다(Dockerfile·Jenkins에 alembic 실행 없음).
---
## 13. 테스트 및 품질 검사
```bash
# [WSL zsh]
uv run pytest # 전체 테스트 (testpaths=tests, asyncio_mode=auto)
uv run pytest -m integration # integration 마커만 (PostgreSQL 필요)
uv run pytest tests/unit/test_config.py # 특정 파일
uv run ruff check . # lint
uv run ruff format --check . # 포맷 검사
uv run ruff format . # 포맷 적용
uv run mypy app # 타입 체크(strict)
bash scripts/ci_check.sh # 전체 CI 게이트(포맷→lint→mypy→pytest→phase 검증)
```
- 커버리지 설정(`[tool.coverage]`)은 없음. `[project.scripts]` 콘솔 진입점 없음.
- 현재 소스 상태의 실제 실행 결과는 18절 참조(일부 FAIL/BLOCKED).
---
## 14. Docker 실행
```bash
# [Docker]
docker compose up -d db # 로컬 Postgres만
docker compose up # db + app 함께
docker build -t mcp-server:dev . # 이미지 빌드(멀티스테이지)
```
- **Dockerfile**: 멀티스테이지(`python:3.12-slim`), 비루트 `app` 사용자, `uv sync --frozen --no-dev`, `EXPOSE 8080`, `HEALTHCHECK`가 `/health` 확인, `CMD`는 `uvicorn app.main:app --host 0.0.0.0 --port 8080`. 컨테이너는 마이그레이션을 실행하지 않음.
- **docker-compose.yml**: `db`(`postgres:16-alpine`, 5432, healthcheck `pg_isready`, 볼륨 `mcp_pgdata`), `app`(Dockerfile 빌드, `db` healthy 후 기동, `MCP_DB_HOST=db`, 8080 노출). DB 비밀번호 기본값은 로컬 개발용입니다.
---
## 15. Kubernetes 및 배포
- **이 저장소에는 범용 Kubernetes/Kustomize/Argo CD manifest가 포함되어 있지 않습니다.** 실제 배포 manifest는 외부 배포 저장소에 존재하는 것으로 참조만 됩니다.
- 포함된 것: `Jenkinsfile`(빌드→레지스트리 푸시→배포 repo의 이미지 태그 갱신), `scripts/update_kustomize_image_tag.sh`(kustomization의 `newTag` 한 줄만 안전 치환).
- **주의**: `Jenkinsfile`의 레지스트리/배포 repo/자격증명 ID 등은 실제 값이 아닌 **예시 placeholder**(`registry.example.com:8082`, `github.com/example-org/...`, `jenkins@example.com` 등)입니다. 실제 운영 값으로 교체가 필요합니다.
- GitHub Actions(`.github/`) 없음. Redis/메시지 브로커 없음. HashiCorp Vault 직접 연동 없음.
---
## 16. API 및 MCP 사용 예시
placeholder만 사용합니다(유효한 실제 키/Secret 아님).
health:
```bash
# [WSL zsh]
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/ready
```
시스템 정보(무인증):
```bash
# [WSL zsh]
curl http://127.0.0.1:8080/api/v1/system/info
curl http://127.0.0.1:8080/api/v1/system/mcp
```
관리 REST API(ADMIN, Bearer):
```bash
# [WSL zsh]
curl -H "Authorization: Bearer cs_<YOUR_API_KEY>" http://127.0.0.1:8080/api/v1/tools
```
MCP 클라이언트 연결(개념 — 실제 클라이언트 설정 형식에 맞게):
```text
transport : Streamable HTTP
url : http://127.0.0.1:8080/mcp/
header : X-Credential-Service-Api-Key: cs_<YOUR_API_KEY> # MCP_MCP_CALLER_AUTH_REQUIRED=true 인 경우
```
- `tools/list` / `tools/call`은 MCP 클라이언트가 위 엔드포인트로 JSON-RPC를 전송하면 서버가 4절 흐름대로 처리합니다.
- 관리자 UI: `http://127.0.0.1:8080/admin` · 사용자 Portal: `http://127.0.0.1:8080/portal/tools` (웹 인증 활성 시 로그인 필요).
---
## 17. 보안 주의사항
- **API Key / JWT Secret**: `.env`에만 두고 저장소에 커밋하지 마세요. `MCP_JWT_SECRET`은 32자 이상 필수, placeholder 값은 기동 시 거부됩니다.
- **로그/감사 내 Secret**: 로거는 필드명 기반으로 Secret을 마스킹(`***REDACTED***`)하고, httpx/httpcore 로그를 WARNING으로 낮춰 쿼리스트링 유출을 억제합니다. 감사 로그는 원본 자격증명/응답 본문을 저장하지 않습니다.
- **Credential injection / reveal**: reveal은 단기 Bearer JWT로만 호출하고 원본 API Key를 reveal 엔드포인트로 보내지 않습니다. 반환 경계에서 Secret을 마스킹(fail-closed)합니다.
- **운영 HTTPS / 쿠키**: 운영에서는 `MCP_COOKIE_SECURE=true` 및 TLS 종단(리버스 프록시)을 사용하세요.
- **MCP 전송 보안**: MCP 마운트는 네트워크 계층에서 인증되지 않습니다. 네트워크 ACL/프록시로 보호하고, 외부 노출 시 `MCP_MCP_DNS_REBINDING_PROTECTION`+`MCP_MCP_ALLOWED_HOSTS` 설정을 검토하세요.
- **CORS**: 저장소 코드에서 별도 CORS 미들웨어 설정은 확인되지 않았습니다(확인 불가). 필요 시 리버스 프록시/게이트웨이에서 통제하세요.
- **DB 자격증명**: `MCP_DB_PASSWORD`는 민감정보입니다. compose 기본 비밀번호는 로컬 전용입니다.
- **HashiCorp Vault 토큰**: 이 서버는 Vault 토큰을 사용하지 않습니다(직접 연동 없음).
- **관리자 기능**: 마지막 활성 관리자 보호·자기 강등 방지 등 안전장치가 있으나, `MCP_WEB_AUTH_ENABLED=false`(기본)에서는 웹 인증이 우회되므로 운영에서 반드시 활성화하세요.
- **공개 저장소 Secret 관리**: 실제 IP/도메인/자격증명을 README·코드·커밋에 포함하지 마세요.
---
## 18. 현재 제한 사항
코드 분석과 경량 검증(의존성 설치 후 실제 실행)에서 확인된 사실입니다.
- **[치명적] 애플리케이션 import 실패** — `AuthType`의 값 `CREDENTIAL_SERVICE_CREDENTIAL_INJECTION`(39자)이 DB 컬럼 정의 `SAEnum(..., length=32)`보다 길어 SQLAlchemy가 `ValueError: length must be larger or equal than the length of the longest enum value. 32 < 39`를 발생시킵니다. `app/db/models/provider.py`·`policy.py`·`tool.py`의 `length=32` 컬럼들이 영향받으며, 이로 인해 앱 import·OpenAPI 생성·pytest 수집이 모두 실패합니다.
- **[타입/정의 충돌]** `app/services/execution/credential_service.py`에서 `CredentialResolver` 이름이 중복 정의(Protocol vs 구현 클래스)되어 `mypy`가 `no-redef`/`Cannot instantiate protocol`을 보고합니다.
- **[품질 검사 드리프트]** `ruff check`(E501 라인 길이 등)·`ruff format --check`(다수 파일 포맷 차이)·`mypy`가 현재 통과하지 않습니다.
- **문서·코드 주석 불일치**: `app/mcp/`의 일부 주석은 "tools/call 미구현(Phase 5)"이라고 되어 있으나, 실제 코드는 플래그 활성 시 업스트림 HTTP 실행·Credential 주입을 수행합니다.
- **로깅 표기**: 로깅 모듈 docstring은 "구조적(structured)"이라 하지만 실제는 stdout 평문 포맷입니다.
- **감사 로그 보존 정책 없음**(append-only, TTL/파기 미구현).
- **외부 의존 검증 불가**: 실제 DB/Credential Service 연결이 필요한 동작은 이 문서 작성 범위에서 검증하지 않았습니다.
> 위 이슈들은 README 작성 범위(문서만 수정)에서 코드를 변경하지 않았습니다. 별도 수정이 필요합니다.
---
## 19. 라이선스
- 저장소에 별도의 `LICENSE` 파일은 없습니다.
- `pyproject.toml`에는 `license = { text = "Proprietary" }`로 선언되어 있습니다.
라이선스 정책이 확정되지 않았다면 공개 전에 명시적인 `LICENSE` 파일 추가를 권장합니다(임의로 라이선스를 지정하지 않았습니다).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues