diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c812b6f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +.git +.venv +.pytest_cache +.ruff_cache +__pycache__ +*.py[cod] +*.egg-info +.env +.env.* +!.env.example +.hermes +.worktrees +*.sqlite3 +*.sqlite +*.db +*.db-* +*.pdf +*.xls +*.xlsx +*.csv +tmp_* +docs +scripts +tests diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6137897 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.12-slim + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + JARCA_LEDGER_DB_PATH=/data/ledger.sqlite3 \ + JARCA_MCP_TRANSPORT=streamable-http \ + JARCA_MCP_HOST=0.0.0.0 \ + JARCA_MCP_PORT=8000 \ + JARCA_MCP_STREAMABLE_HTTP_PATH=/mcp + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY jarca_ledger_mcp ./jarca_ledger_mcp +COPY dashboard ./dashboard + +RUN python -m pip install --upgrade pip \ + && python -m pip install '.[dashboard]' + +VOLUME ["/data"] +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD python -c "import os, socket; s=socket.create_connection(('127.0.0.1', int(os.getenv('JARCA_MCP_PORT', '8000'))), timeout=3); s.close()" + +CMD ["jarca-ledger-mcp"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..23a045a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + jarca-ledger: + build: + context: . + dockerfile: Dockerfile + image: jarca-ledger:latest + container_name: jarca-ledger + restart: unless-stopped + ports: + - "8000:8000" + environment: + JARCA_LEDGER_DB_PATH: /data/ledger.sqlite3 + JARCA_MCP_TRANSPORT: streamable-http + JARCA_MCP_HOST: 0.0.0.0 + JARCA_MCP_PORT: "8000" + JARCA_MCP_STREAMABLE_HTTP_PATH: /mcp + PYTHONUNBUFFERED: "1" + volumes: + - /volume1/docker/jarca-ledger/data:/data diff --git a/docs/batches/bat-015_20260707_docker-network-mcp-deployment/design.md b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/design.md new file mode 100644 index 0000000..4ec6fe1 --- /dev/null +++ b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/design.md @@ -0,0 +1,44 @@ +# BAT-015 Design + +## Architecture Decision + +`jarca-ledger` is deployed as an independent Docker service that directly serves FastMCP `streamable-http` transport on port 8000. `genichin-agent` is no longer the process launcher for Jarca Ledger MCP. It may remain a client, but the Jarca Ledger service lifecycle belongs to Docker. + +## Runtime topology + +```text +finance-agent / Hermes client + │ + │ Streamable HTTP MCP + ▼ +http://192.168.55.253:8000/mcp + │ + ▼ +jarca-ledger container + ├─ jarca-ledger-mcp FastMCP server + ├─ /data/ledger.sqlite3 + └─ Docker healthcheck on 127.0.0.1:8000 + │ + ▼ +Synology host /volume1/docker/jarca-ledger/data +``` + +## Transport contract + +- Default remains `stdio` when no env is set. +- `JARCA_MCP_TRANSPORT=streamable-http` runs Uvicorn/FastMCP network transport. +- `JARCA_MCP_HOST=0.0.0.0`, `JARCA_MCP_PORT=8000`, `JARCA_MCP_STREAMABLE_HTTP_PATH=/mcp` define the Docker service endpoint. +- Tool names and tool payload contracts remain unchanged. + +## Data migration design + +- Source: `jarca:/root/.local/share/jarca/ledger.sqlite3` +- Target: `jarca-ledger:/data/ledger.sqlite3` +- Snapshot method: SQLite `Connection.backup()` to avoid copying a live DB mid-write. +- Pre-replacement safety: back up the new empty `/data/ledger.sqlite3` under `/data/backups/pre_migration_empty_.sqlite3`. +- Verification: sha256, `PRAGMA integrity_check`, table counts, MCP `get_ledger_db_status`. + +## Security and exposure boundary + +- This batch exposes MCP only on the Synology host/internal network port mapping `8000:8000`. +- Internet exposure, reverse proxy, TLS, and auth policy are not included and must be handled as a separate approved change before external access. diff --git a/docs/batches/bat-015_20260707_docker-network-mcp-deployment/implementation.md b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/implementation.md new file mode 100644 index 0000000..4b1ee43 --- /dev/null +++ b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/implementation.md @@ -0,0 +1,41 @@ +# BAT-015 Implementation + +## Code changes + +- `jarca_ledger_mcp/adapters/mcp/server.py` + - Added `McpRuntimeConfig` dataclass. + - Added env parsing for `JARCA_MCP_TRANSPORT`, `JARCA_MCP_HOST`, `JARCA_MCP_PORT`, `JARCA_MCP_STREAMABLE_HTTP_PATH`, `JARCA_MCP_SSE_PATH`, `JARCA_MCP_MESSAGE_PATH`, `JARCA_MCP_STATELESS_HTTP`. + - Instantiates FastMCP with configured host/port/path values. + - Calls `mcp.run(transport=_runtime_config.transport)`. +- `tests/test_mcp_http_runtime.py` + - Added config tests for default stdio, streamable-http env, and invalid transport rejection. + +## Deployment files + +- `Dockerfile` + - Builds `jarca-ledger:latest` from `python:3.12-slim`. + - Installs `.[dashboard]` dependencies. + - Exposes port 8000 and runs `jarca-ledger-mcp`. + - Adds TCP healthcheck against `127.0.0.1:${JARCA_MCP_PORT}`. +- `.dockerignore` + - Excludes `.env`, DBs, financial source files, tests/docs/scripts not needed in image, and local artifacts. +- `docker-compose.yml` + - Defines `container_name: jarca-ledger`. + - Maps `8000:8000`. + - Mounts `/volume1/docker/jarca-ledger/data:/data`. + - Sets streamable-http MCP env. + - Uses `restart: unless-stopped`. + +## Data migration implementation + +1. Created a consistent source snapshot inside the old `jarca` container using SQLite backup API. +2. Copied snapshot to the host temp path. +3. Backed up the initial empty `jarca-ledger` DB under `/data/backups/pre_migration_empty_20260705152129.sqlite3`. +4. Stopped `jarca-ledger`, removed WAL/SHM sidecars, copied the snapshot to `/data/ledger.sqlite3`, normalized permissions, and restarted. +5. Waited for Docker health to become `healthy`. + +## Documentation changes + +- Added `docs/runbooks/docker-network-mcp-deploy.md`. +- Added this StagePilot post-implementation trace: dcy-013, REQ-019, BAT-015. +- Updated `docs/project-structure.md` and `docs/runtime-flows.md` to include Docker/network MCP runtime. diff --git a/docs/batches/bat-015_20260707_docker-network-mcp-deployment/index.md b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/index.md new file mode 100644 index 0000000..7e62872 --- /dev/null +++ b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/index.md @@ -0,0 +1,20 @@ +# BAT-015 Docker network MCP deployment + +- Profile: standard +- Status: release-candidate +- Included REQ: REQ-019 +- Discovery: dcy-013 +- Owner: 렛저장인 +- Date (KST): 2026-07-07 + +## 목표 + +REQ-019에 따라 `jarca-ledger`를 `genichin-agent` 생명주기에서 분리된 독립 Docker service로 배포하고, streamable-http MCP endpoint와 Synology volume 기반 운영 DB를 검증한다. + +## 산출물 + +- FastMCP runtime transport 설정화 +- Dockerfile / `.dockerignore` / `docker-compose.yml` +- Docker network MCP 배포 runbook +- 기존 `jarca` 운영 DB 이전 +- pytest, Docker health, MCP smoke, DB integrity/table-count verification diff --git a/docs/batches/bat-015_20260707_docker-network-mcp-deployment/planning.md b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/planning.md new file mode 100644 index 0000000..7edea82 --- /dev/null +++ b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/planning.md @@ -0,0 +1,24 @@ +# BAT-015 Planning + +## Scope + +- Implement and verify REQ-019 Docker network MCP deployment. +- Preserve existing stdio MCP compatibility while enabling `streamable-http` runtime mode. +- Deploy `jarca-ledger` as an independent Docker service with Synology bind volume. +- Migrate the existing operating DB from `jarca` to `jarca-ledger` with backup/snapshot evidence. +- Refresh deployment runbook and baseline docs. + +## Design Gate + +- Required: yes +- Rationale: this batch changes runtime topology, deployment ownership, exposed MCP transport, Docker health behavior, and operating DB location. A design/evidence trail is required even though the code diff is small. + +## Work plan + +1. Add MCP runtime config helpers for stdio/sse/streamable-http transport selection. +2. Add tests for default stdio, streamable-http env config, and invalid transport rejection. +3. Add Dockerfile, `.dockerignore`, compose service, healthcheck, and runbook. +4. Build and run `jarca-ledger` with `/volume1/docker/jarca-ledger/data:/data`. +5. Verify network MCP using a streamable-http client against `http://192.168.55.253:8000/mcp`. +6. Snapshot existing `jarca` DB with SQLite backup API, back up the new empty DB, replace `/data/ledger.sqlite3`, restart, and verify table counts/integrity. +7. Update StagePilot Discovery/SRS/BAT/baseline docs. diff --git a/docs/batches/bat-015_20260707_docker-network-mcp-deployment/verification.md b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/verification.md new file mode 100644 index 0000000..c6d6feb --- /dev/null +++ b/docs/batches/bat-015_20260707_docker-network-mcp-deployment/verification.md @@ -0,0 +1,141 @@ +# BAT-015 Verification + +## Verification summary + +Status: PASS + +The Docker network MCP deployment is implemented and running. `jarca-ledger` is independent of `genichin-agent`, exposes streamable-http MCP on port 8000, uses `/volume1/docker/jarca-ledger/data` as the operating volume, and serves the migrated operating ledger DB. + +## Acceptance criteria evidence + +### AC-1. Runtime config tests pass + +- Command: `python -m pytest tests/test_mcp_http_runtime.py -q` +- Result: PASS +- Output: `... [100%]` + +### AC-2. Full regression suite passes + +- Command: `python -m pytest -q` +- Result: PASS +- Output: `........................ss..................ss... [100%]` + +### AC-3. Docker build and independent service start succeed + +- Command: `docker compose build` +- Result: PASS, image `jarca-ledger:latest` built. +- Command: `docker compose up -d --no-build` +- Result: container `jarca-ledger` created/started. + +Docker state evidence: + +```text +state=running health=healthy restart=unless-stopped +``` + +Mount evidence: + +```text +/volume1/docker/jarca-ledger/data -> /data (bind) +``` + +Log evidence: + +```text +StreamableHTTP session manager started +Application startup complete. +Uvicorn running on http://0.0.0.0:8000 +``` + +### AC-4. Network MCP smoke passes + +Endpoint used from `genichin-agent` container: + +```text +http://192.168.55.253:8000/mcp +``` + +Smoke evidence: + +```text +tool_count 56 +ledger_health_check success true +db_path /data/ledger.sqlite3 +schema_version 10 +active_transactions 6225 +``` + +### AC-5. Operating DB migration preserved data + +Source DB before migration: + +```text +jarca:/root/.local/share/jarca/ledger.sqlite3 +size 8343552 +sha256 f43532a41db3fd64edfe26d3dcc6f54ab8cf34bf79018fed9e53324996e4413b +accounts 8 +transactions 6312 +import_batches 13 +category_rules 46 +ledger_categories 62 +``` + +Migration snapshot evidence: + +```text +snapshot_integrity ok +snapshot_size 8343552 +snapshot_sha256 e940a237e25e71561fe1af332f29cb11e92f02612b4ff89a7ab562585f82f278 +``` + +Post-migration target evidence: + +```text +path /data/ledger.sqlite3 +size 8343552 +sha256 e940a237e25e71561fe1af332f29cb11e92f02612b4ff89a7ab562585f82f278 +integrity ok +accounts 8 +transactions 6312 +budgets 0 +import_batches 13 +category_rules 46 +ledger_categories 62 +manual_assets 0 +asset_snapshots 0 +``` + +MCP `get_ledger_db_status` evidence: + +```text +integrity_check ok +table_counts.accounts 8 +table_counts.transactions 6312 +table_counts.import_batches 13 +table_counts.category_rules 46 +table_counts.ledger_categories 62 +``` + +### AC-6. Documentation and baselines updated + +- `docs/runbooks/docker-network-mcp-deploy.md` added. +- `docs/project-structure.md` updated with Dockerfile/compose/network MCP service and deployment boundaries. +- `docs/runtime-flows.md` updated with Docker network MCP and DB migration flows. +- StagePilot trace added: `dcy-013`, `REQ-019`, `BAT-015`. + +## Safety boundary + +- No raw financial statement source files were copied into the image. +- `.dockerignore` excludes DB/source financial files from image build context. +- Existing empty target DB was backed up before replacement. +- Existing source DB was not modified; migration used a snapshot copy. + +## Blocking Issues + +- None for internal Synology network operation. + +## Follow-up candidates + +- Add remote MCP endpoint registration to the finance-agent profile. +- Consider a separate auth/TLS/reverse-proxy Discovery before any external network exposure. +- Consider an automated backup scheduler as a separate operations requirement. diff --git a/docs/batches/index.md b/docs/batches/index.md index 8b25dfa..e4ad8f7 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -22,3 +22,4 @@ | BAT-012 | standard | release-candidate | REQ-016 | dcy-011 | [bat-012_20260703_import-pipeline-split](bat-012_20260703_import-pipeline-split/) | | BAT-013 | standard | release-candidate | REQ-017 | dcy-011 | [bat-013_20260703_advanced-workflow-service-split](bat-013_20260703_advanced-workflow-service-split/) | | BAT-014 | standard | release-candidate | REQ-018 | dcy-011 | [bat-014_20260703_adapter-surface-narrowing-baseline-refresh](bat-014_20260703_adapter-surface-narrowing-baseline-refresh/) | +| BAT-015 | standard | release-candidate | REQ-019 | dcy-013 | [bat-015_20260707_docker-network-mcp-deployment](bat-015_20260707_docker-network-mcp-deployment/) | diff --git a/docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md b/docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md index 0d0a5bd..d151400 100644 --- a/docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md +++ b/docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md @@ -5,9 +5,9 @@ - 문서 ID: dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing - 이슈명: Jarca Ledger 백엔드 리팩터링 Phase 4+ 후속 주기 (write service 분리 → import pipeline 분리 → advanced finance workflow 분리 → MCP/Streamlit adapter 표면 축소) - 작성 시각(KST): 2026-07-03 07:10 -- 마지막 갱신 시각(KST): 2026-07-03 08:23 +- 마지막 갱신 시각(KST): 2026-07-07 22:23 - 대체됨: 없음 -- 후속 Discovery 참조: 없음 +- 후속 Discovery 참조: `docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md` - 생성된 REQ 참조: `REQ-015`, `REQ-016`, `REQ-017`, `REQ-018` # 1. 계획 상태 요약 diff --git a/docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md b/docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md new file mode 100644 index 0000000..0dc1d78 --- /dev/null +++ b/docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md @@ -0,0 +1,178 @@ +# 0. 문서 상태 +이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다. + +- 상태: confirmed +- 문서 ID: dcy-013_20260707_docker-network-mcp-deployment +- 이슈명: Jarca Ledger 독립 Docker 배포 및 network MCP endpoint 제공 +- 작성 시각(KST): 2026-07-07 22:23 KST +- 마지막 갱신 시각(KST): 2026-07-07 22:23 KST +- 대체됨: 없음 +- 후속 Discovery 참조: 없음 +- 생성된 REQ 참조: `REQ-019` + +# 1. 계획 상태 요약 + +- 연계 문서 현황: + - 선행 Discovery: `docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md` — Phase 8 optional HTTP adapter를 Deferred 후보로 남겼다. + - 관련 REQ/BAT: `REQ-018`/`BAT-014`는 MCP/Streamlit adapter surface narrowing과 baseline refresh를 완료했으나, network MCP 배포 자체는 Out of Scope였다. + - 관련 baseline 문서: `docs/project-structure.md`, `docs/runtime-flows.md` + - 관련 runbook: `docs/runbooks/docker-network-mcp-deploy.md` +- 해석: + - 재무관리 에이전트가 `genichin-agent`의 stdio `docker exec` 실행에 의존하지 않고 Jarca Ledger를 사용하려면, Jarca Ledger 자체가 Docker daemon 아래 독립 서비스로 실행되고 network MCP endpoint를 직접 제공해야 한다. + - 이번 문서는 이미 수행된 긴급 배포 작업을 StagePilot 추적성 안으로 편입해, Dockerfile/compose/transport 설정/DB 이전/검증 evidence를 REQ/BAT 기준으로 남기는 post-implementation SDLC 보정 문서다. + +# 2. 요구사항 판정 결과 + +- 판정: 신규 후속 주기 +- 근거 + - dcy-011은 Phase 8 optional HTTP adapter를 구현하지 않고 Deferred로 남겼으므로, network MCP endpoint와 Docker service 배포는 dcy-011의 하위 구현이 아니라 별도 deployment/interface 반복이다. + - 기존 stdio MCP는 클라이언트가 프로세스를 실행하는 모델이어서 `genichin-agent` 실행 환경에 의존한다. 이번 요청은 `jarca-ledger` 컨테이너가 자체 생명주기와 endpoint를 갖도록 하는 운영 구조 변경이다. + +# 3. 문제점의 요약 + +- 현재 상태 + - 기존 `jarca` 컨테이너에는 `/root/.local/share/jarca/ledger.sqlite3` 운영 DB가 존재했고, `accounts=8`, `transactions=6312`, `import_batches=13`, `category_rules=46`, `ledger_categories=62`가 들어 있었다. + - 새 `jarca-ledger` 컨테이너 초기 배포 직후 `/data/ledger.sqlite3`는 빈 schema 상태였고, 운영 DB 이전이 누락되어 있었다. + - 기존 MCP entrypoint는 FastMCP 기본 stdio 실행만 사용했으며, network MCP transport 설정과 Docker 배포 정의가 StagePilot 문서에 반영되어 있지 않았다. + - `docs/project-structure.md`와 `docs/runtime-flows.md`는 MCP stdio/local dashboard 중심 baseline을 유지했고, network MCP/Docker service flow를 아직 반영하지 않았다. +- 기대 상태 + - `jarca-ledger` 컨테이너는 `genichin-agent`와 독립적으로 `restart: unless-stopped` 정책 아래 실행된다. + - 컨테이너는 `streamable-http` MCP endpoint `http://:8000/mcp`를 직접 제공한다. + - 운영 DB는 `/volume1/docker/jarca-ledger/data/ledger.sqlite3`에 영속화되며, 기존 `jarca` 운영 DB 데이터가 일관성 snapshot 방식으로 이전된다. + - 재무관리 에이전트는 Docker exec/attach가 아니라 network MCP endpoint를 소비한다. +- 이해관계자 + - 가계 재무관리 에이전트 — Jarca Ledger MCP tools를 안정적으로 호출하는 소비자 + - 가웨인/dev-butler — 배포 구조와 기술 기준을 관리하는 개발 책임자 + - 주인님/운영자 — Synology Docker volume과 운영 DB의 실제 소유자 +- 주요 사용자 시나리오 + - 재무관리 에이전트가 `http://192.168.55.253:8000/mcp`에 접속해 `ledger_health_check`, `get_ledger_db_status`, 거래/계좌/리포트 도구를 호출한다. + - `genichin-agent` 컨테이너가 재시작되거나 중단되어도 `jarca-ledger` 컨테이너와 DB는 계속 유지된다. + - 운영자가 Docker 상태, healthcheck, bind mount, DB integrity/hash/table counts를 evidence로 확인한다. + +# 4. 이번에 정의할 변경 + +- FastMCP runtime transport 설정화: `JARCA_MCP_TRANSPORT`, `JARCA_MCP_HOST`, `JARCA_MCP_PORT`, `JARCA_MCP_STREAMABLE_HTTP_PATH`로 stdio와 streamable-http 실행을 선택할 수 있게 한다. +- 독립 Docker service 배포: `Dockerfile`, `.dockerignore`, `docker-compose.yml`을 추가하고 `container_name=jarca-ledger`, volume `/volume1/docker/jarca-ledger/data:/data`, port `8000:8000`, restart `unless-stopped`를 고정한다. +- 운영 DB 이전 및 검증: 기존 `jarca` 컨테이너 DB를 SQLite backup API로 snapshot한 뒤 새 volume으로 이전하고, MCP smoke와 DB status로 동일 데이터를 검증한다. +- baseline 문서 갱신: `docs/project-structure.md`, `docs/runtime-flows.md`, deployment runbook이 network MCP 배포 구조를 반영해야 한다. + +## 영향 범위 + +- 코드 영향: + - `jarca_ledger_mcp/adapters/mcp/server.py` + - `tests/test_mcp_http_runtime.py` +- 배포 영향: + - `Dockerfile`, `.dockerignore`, `docker-compose.yml` + - Synology host path `/volume1/docker/jarca-ledger/data` +- 문서 영향: + - `docs/runbooks/docker-network-mcp-deploy.md` + - `docs/project-structure.md`, `docs/runtime-flows.md` + - StagePilot Discovery/SRS/Batch register와 verification evidence + +# 5. 요구사항 목록 + +## 기능 요구사항 +- FR-1: MCP server는 기존 stdio 기본값을 유지하면서 환경변수로 `streamable-http` transport를 활성화할 수 있어야 한다. +- FR-2: Docker 배포는 `jarca-ledger` 컨테이너가 독립 실행되고 `/volume1/docker/jarca-ledger/data`를 `/data`로 bind mount해야 한다. +- FR-3: network MCP endpoint는 `http://:8000/mcp`에서 tool discovery와 `ledger_health_check`를 성공시켜야 한다. +- FR-4: 기존 `jarca` 운영 DB는 일관성 snapshot으로 이전되어 주요 table counts와 integrity가 보존되어야 한다. +- FR-5: baseline docs와 runbook은 Docker network MCP 운영 흐름과 `genichin-agent` 비의존 원칙을 반영해야 한다. + +## 비기능 요구사항 +- NFR-1 (operational independence): `jarca-ledger` 컨테이너 생명주기는 `genichin-agent`가 아니라 Docker daemon/restart policy에 의해 관리되어야 한다. +- NFR-2 (financial data safety): 운영 DB 이전 전 새 빈 DB 백업을 남기고, 기존 DB는 SQLite backup API로 snapshot해 복사해야 한다. +- NFR-3 (contract preservation): 기존 MCP tool 이름과 주요 응답 shape는 network transport 도입 후에도 유지되어야 한다. + +## 범위 경계 + +- In Scope + - FastMCP streamable-http transport 설정화 + - Docker image/compose 배포 + - Synology volume 기반 운영 DB 이전 + - MCP network smoke 및 DB integrity verification + - StagePilot/baseline/runbook 문서 반영 +- Out of Scope + - 별도 FastAPI/REST business API 구현 + - 외부 공개용 reverse proxy/TLS/auth 설계 + - 자동 백업 스케줄러 + - dcy-012 review-decision/workbench 기능 구현 + +# 6. 리스크/가정 목록 + +## 리스크 +- R-1 (High): 운영 DB 이전 누락 또는 잘못된 DB 교체 시 재무관리 에이전트가 빈 ledger를 source of truth로 오인할 수 있다. + - 완화 방안: 이전 전후 table counts, sha256, integrity_check, MCP `get_ledger_db_status`를 verification evidence로 남긴다. +- R-2 (Medium): `127.0.0.1:8000`은 `genichin-agent` 내부에서는 agent 컨테이너 자신을 의미하므로 smoke endpoint를 잘못 잡을 수 있다. + - 완화 방안: agent 컨테이너에서 검증할 때는 Synology host IP `192.168.55.253:8000` 또는 공유 Docker network DNS를 사용한다고 runbook에 명시한다. +- R-3 (Medium): network MCP endpoint가 내부망에 열리므로 접근제어/TLS가 없는 상태에서 외부 노출되면 금융 tool surface가 위험해질 수 있다. + - 완화 방안: 현재 범위는 내부망 배포로 제한하고, 외부 공개/reverse proxy/auth는 별도 후속 Discovery로 분리한다. + +## 가정 +- A-1: Synology Docker daemon은 `/volume1/docker/jarca-ledger/data` bind mount를 생성/사용할 수 있다. +- A-2: 재무관리 에이전트는 streamable-http MCP client 또는 Hermes remote MCP 설정을 통해 endpoint를 소비할 수 있다. +- A-3: 기존 `jarca` 컨테이너의 `/root/.local/share/jarca/ledger.sqlite3`가 현 시점 운영 ledger source였다. + +## 오픈 질문 +- 없음 — 이번 배포 범위와 endpoint/volume/컨테이너 이름은 사용자 지시와 실제 배포 evidence로 확정되었다. + +# 7. 초기 성공 기준 + +- S-1: `docker inspect jarca-ledger`에서 `state=running`, `health=healthy`, `restart=unless-stopped`, mount `/volume1/docker/jarca-ledger/data -> /data`가 확인된다. +- S-2: MCP streamable-http client가 `http://192.168.55.253:8000/mcp`에서 56개 tool을 발견하고 `ledger_health_check`에 성공한다. +- S-3: `get_ledger_db_status`가 `/data/ledger.sqlite3`, `integrity_check=ok`, `accounts=8`, `transactions=6312`, `import_batches=13`, `category_rules=46`, `ledger_categories=62`를 보고한다. +- S-4: `python -m pytest -q`와 신규 runtime config 테스트가 통과한다. +- S-5: baseline docs와 runbook이 network MCP 배포와 운영 DB 이전 흐름을 설명한다. + +## 측정 방식 및 데이터 출처 + +- S-1: `docker inspect`, `docker logs`, Docker healthcheck +- S-2: Python `mcp.client.streamable_http.streamablehttp_client` smoke +- S-3: MCP `get_ledger_db_status` 및 SQLite integrity/table count probe +- S-4: pytest output +- S-5: `docs/project-structure.md`, `docs/runtime-flows.md`, `docs/runbooks/docker-network-mcp-deploy.md`, BAT-015 verification + +# 8. REQ로 넘기기 전 확인 체크 + +- [x] 컨테이너명/volume/endpoint 확정 +- [x] 기존 DB source와 이전 대상 확인 +- [x] network MCP transport 지원 가능성 확인 +- [x] 배포 및 smoke evidence 확보 +- [x] baseline 문서 갱신 요구 명시 + +# 9. 파일 처리 결과 + +- 처리 결과: 생성 +- 생성 경로: `docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md` +- 참조 문서: + - `docs/discovery/dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md` + - `docs/srs/Interface/req-018_adapter-surface-narrowing-baseline-refresh.md` + - `docs/project-structure.md` + - `docs/runtime-flows.md` + - `docs/runbooks/docker-network-mcp-deploy.md` + - `stage-pilot/templates/project-structure.md` + - `stage-pilot/templates/runtime-flows.md` + +# 10. 사용자 결정 필요 항목 요약 + +## DECIDE +- 없음 + +## CONFIRM +- 없음 + +## DATA +- 없음 + +# 11. Discovery Freeze + +## 섹션 참조 +- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) +- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) +- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) + +## Freeze 플래그 +- Handoff Decision: confirmed +- Handoff Rationale: 사용자 지시에 따라 Docker network MCP 배포와 운영 DB 이전이 이미 완료되었고, 검증 evidence가 확보되어 REQ/BAT post-implementation 문서화로 인계한다. +- Ready for REQ Drafting: true +- Confirmed By: 진환 고 +- Confirmed At (KST): 2026-07-07 22:23 KST diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 2db80a6..a58ab4a 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -18,3 +18,4 @@ | dcy-010_20260702_backend-core-layering-mcp-adapter-realignment | 20260702 | Jarca Ledger 백엔드 코어 분리 및 layered 아키텍처 재편, MCP adapter 재정렬 | [Discovery](./dcy-010_20260702_backend-core-layering-mcp-adapter-realignment.md) | confirmed | | dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing | 20260703 | Jarca Ledger 백엔드 리팩터링 Phase 4+ 후속 주기 (write service 분리 → import pipeline 분리 → advanced finance workflow 분리 → MCP/Streamlit adapter 표면 축소) | [Discovery](./dcy-011_20260703_backend-refactor-phase4-write-import-workflow-adapter-narrowing.md) | confirmed | | dcy-012_20260703_review-queue-reconciliation-workbench-persistence | 20260703 | 확인필요 큐 / 잔액대조 워크벤치 영속화 및 운영 모델 정립 | [Discovery](./dcy-012_20260703_review-queue-reconciliation-workbench-persistence.md) | draft | +| dcy-013_20260707_docker-network-mcp-deployment | 20260707 | Jarca Ledger 독립 Docker 배포 및 network MCP endpoint 제공 | [Discovery](./dcy-013_20260707_docker-network-mcp-deployment.md) | confirmed | diff --git a/docs/project-structure.md b/docs/project-structure.md index 6b37215..c878ef6 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -4,8 +4,8 @@ - Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml` - Status: draft - Owner: Jarca / project maintainer -- Last Updated (KST): 2026-07-03 -- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-010, BAT-012, BAT-013, BAT-014 +- Last Updated (KST): 2026-07-07 +- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-010, BAT-012, BAT-013, BAT-014, dcy-013/BAT-015 ## Project Summary - Jarca 가계부 전용 MCP 서버와 Streamlit 대시보드로 계좌/카드 거래, 예산, 분류 규칙, import, 자산/스냅샷, 현금흐름 분석을 SQLite에 저장하고 운영한다. @@ -15,7 +15,7 @@ ## Tech Stack - Python >=3.11 -- FastMCP / MCP server +- FastMCP / MCP server (stdio and streamable-http) - SQLite - Streamlit - pandas / plotly @@ -26,7 +26,8 @@ ## Primary Runtime - Type: mixed - Planned Entry Points: - - `jarca-ledger-mcp` :: MCP stdio server for Hermes Agent tools. + - `jarca-ledger-mcp` :: MCP stdio server for local Hermes Agent tools and streamable-http server when configured by env. + - `jarca-ledger` Docker container :: independent Streamable HTTP MCP service on `:8000/mcp`. - `dashboard/app.py` :: Streamlit dashboard for local ledger operations. - `pytest` :: Regression test entry point. @@ -43,6 +44,9 @@ ```text jarca-ledger-mcp/ ├── README.md +├── Dockerfile +├── docker-compose.yml +├── .dockerignore ├── pyproject.toml ├── uv.lock ├── .env.example @@ -104,6 +108,7 @@ jarca-ledger-mcp/ │ ├── contract_snapshots/ │ │ └── response_contract_v1_0.json │ ├── test_contract_regression.py +│ ├── test_mcp_http_runtime.py │ ├── test_dashboard_app.py │ ├── test_service.py │ ├── test_asset_snapshots.py @@ -125,6 +130,7 @@ jarca-ledger-mcp/ │ ├── srs/ │ ├── batches/ │ ├── runbooks/ +│ │ └── docker-network-mcp-deploy.md │ ├── releases/ │ ├── project-structure.md │ ├── runtime-flows.md @@ -174,7 +180,7 @@ jarca-ledger-mcp/ - `jarca_ledger_mcp/adapters/mcp/tool_contracts.py` - Narrow MCP adapter contract surface that re-exports only the public tool callables consumed by the FastMCP server. - `jarca_ledger_mcp/adapters/mcp/server.py` - - FastMCP tool registration layer that depends on `tool_contracts.py` instead of broad direct service imports. + - FastMCP tool registration layer that depends on `tool_contracts.py` instead of broad direct service imports. Runtime transport is configurable: stdio by default, or streamable-http through `JARCA_MCP_TRANSPORT=streamable-http` with host/port/path env settings. - `jarca_ledger_mcp/adapters/streamlit/service_gateway.py` - Compatibility aggregator over page/domain gateway modules. - `jarca_ledger_mcp/adapters/streamlit/*_gateway.py` @@ -197,6 +203,10 @@ jarca-ledger-mcp/ - Local browser evidence helper using synthetic/validation DBs only. - `scripts/ledger_restore_smoke.py` - Script-only restore smoke helper; restore remains outside the MCP tool surface. +- `Dockerfile` / `docker-compose.yml` + - Build and run the independent `jarca-ledger` Docker service. Compose fixes `container_name=jarca-ledger`, `restart=unless-stopped`, port `8000:8000`, and bind mount `/volume1/docker/jarca-ledger/data:/data`. +- `docs/runbooks/docker-network-mcp-deploy.md` + - Operator runbook for building, starting, and verifying the Docker Streamable HTTP MCP deployment and DB path. ## Dependency Rules - `adapters/mcp/server.py` should remain a thin tool adapter and delegate business logic to `service.py` facade/application modules. @@ -216,7 +226,7 @@ jarca-ledger-mcp/ ## Current Gaps / Planned Changes - Application modules are now separated by phase-0~3 domain seams, but deeper repository/use-case split remains future Phase 4+ work. - Streamlit now uses page/domain gateways; deeper presenter/view-model extraction remains future work. -- HTTP adapter and remote ops re-architecture remain out of scope. +- Network MCP over FastMCP streamable-http is now available for internal Synology operation; broader HTTP/REST business API, external exposure, reverse proxy/TLS/auth, and remote ops re-architecture remain future work. ## Update Triggers - Update this document when package boundaries change. @@ -231,3 +241,4 @@ jarca-ledger-mcp/ - 2026-06-30 KST: REQ-007/REQ-008/BAT-007 기준으로 MCP response contract snapshot/test 위치와 required-check job 계약 추가. - 2026-07-03 KST: REQ-013/REQ-014/BAT-010 기준으로 application / infrastructure / adapter layering, compatibility facade/shim, Streamlit gateway 구조를 반영했다. - 2026-07-03 KST: REQ-016/REQ-017/REQ-018 및 BAT-012/BAT-013/BAT-014 기준으로 import/advanced owning services, parser seam, MCP contract helper, page/domain Streamlit gateways를 반영했다. +- 2026-07-07 KST: REQ-019/BAT-015 기준으로 Dockerfile/compose, `jarca-ledger` independent container, streamable-http MCP endpoint, Synology volume `/volume1/docker/jarca-ledger/data`, and Docker deployment runbook을 반영했다. diff --git a/docs/runbooks/docker-network-mcp-deploy.md b/docs/runbooks/docker-network-mcp-deploy.md new file mode 100644 index 0000000..7d9e676 --- /dev/null +++ b/docs/runbooks/docker-network-mcp-deploy.md @@ -0,0 +1,62 @@ +# Jarca Ledger Docker Network MCP 배포 Runbook + +## 목적 + +`jarca-ledger`를 `genichin-agent` 생명주기와 분리된 독립 Docker 서비스로 배포하고, stdio `docker exec`가 아니라 HTTP 기반 Streamable MCP endpoint를 직접 제공한다. + +## 운영 고정값 + +| 항목 | 값 | +|---|---| +| 컨테이너 이름 | `jarca-ledger` | +| 이미지 | `jarca-ledger:latest` | +| Host data path | `/volume1/docker/jarca-ledger/data` | +| Container data path | `/data` | +| SQLite DB | `/data/ledger.sqlite3` | +| MCP transport | `streamable-http` | +| MCP listen | `0.0.0.0:8000` | +| MCP path | `/mcp` | + +## 배포 + +```bash +cd /home/ubuntu/workspaces/jarca-ledger-mcp +docker compose up -d --build +``` + +## 검증 + +```bash +docker ps --filter name=jarca-ledger +docker inspect jarca-ledger --format='MOUNTS: see HostConfig.Binds and Mounts in docker inspect output' +docker inspect jarca-ledger | python3 -c "import json,sys; data=json.load(sys.stdin)[0]; [print(f'{m.get(\"Source\")} -> {m.get(\"Destination\")} ({m.get(\"Type\")})') for m in data.get('Mounts', [])]" +docker logs --tail 80 jarca-ledger +``` + +Python MCP client smoke: + +```bash +python - <<'PY' +import anyio +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async def main(): + async with streamablehttp_client('http://127.0.0.1:8000/mcp') as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + tools = await session.list_tools() + print('tool_count', len(tools.tools)) + result = await session.call_tool('ledger_health_check', {}) + print(result.content[0].text) + +anyio.run(main) +PY +``` + +## 운영 원칙 + +- `jarca-ledger` 컨테이너는 Docker daemon의 `restart: unless-stopped` 정책으로 독립 실행된다. +- `genichin-agent`는 이 서비스를 배포하거나 실행시키는 필수 구성요소가 아니다. +- 재무관리 에이전트는 `http://:8000/mcp` 또는 같은 Docker network에서 `http://jarca-ledger:8000/mcp`로 접속한다. +- 운영 DB와 import 원본/민감자료는 이미지에 포함하지 않고 `/volume1/docker/jarca-ledger/data`에만 둔다. diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 59dcd03..452aea7 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -4,8 +4,8 @@ - Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml` - Status: draft - Owner: Jarca / project maintainer -- Last Updated (KST): 2026-07-03 -- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-008, BAT-010, BAT-012, BAT-013, BAT-014 +- Last Updated (KST): 2026-07-07 +- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-008, BAT-010, BAT-012, BAT-013, BAT-014, dcy-013/BAT-015 - Primary Runtime: mixed ## Project Summary @@ -20,19 +20,36 @@ - Does not duplicate full acceptance criteria or low-level implementation details. ## Covered Entry Points -- `jarca-ledger-mcp` :: MCP stdio server for Hermes tool calls. +- `jarca-ledger-mcp` :: MCP stdio server for local Hermes tool calls, or streamable-http server when configured by env. +- `jarca-ledger` Docker service :: Streamable HTTP MCP endpoint at `http://:8000/mcp`. - `dashboard/app.py` :: Streamlit local dashboard. - `pytest` :: regression test runner. - `.github/workflows/ci.yml` :: GitHub Actions CI gate runner for PR/push/manual checks. ## Shared Components -- SQLite database at `~/.local/share/jarca/ledger.sqlite3` by default. +- SQLite database at `~/.local/share/jarca/ledger.sqlite3` by default for local runs; Docker service uses `JARCA_LEDGER_DB_PATH=/data/ledger.sqlite3` backed by `/volume1/docker/jarca-ledger/data`. - `jarca_ledger_mcp/service.py` compatibility facade over application modules. - `jarca_ledger_mcp/infrastructure/sqlite/*` for DB path resolution, schema, migration, and connection lifecycle. - `jarca_ledger_mcp/adapters/mcp/tool_contracts.py` + `adapters/mcp/server.py` for MCP contract and registration seams. - `jarca_ledger_mcp/adapters/streamlit/*_gateway.py` + `service_gateway.py` compatibility aggregator for dashboard-facing service calls. - `jarca_ledger_mcp/infrastructure/parsers/*` plus legacy parser modules for KB/Toss/SeoulPay sources. + +## Flow: `Docker network MCP service` +1. Docker daemon starts the `jarca-ledger` container from `jarca-ledger:latest` with `restart: unless-stopped`. +2. Compose mounts `/volume1/docker/jarca-ledger/data` to `/data` and sets `JARCA_LEDGER_DB_PATH=/data/ledger.sqlite3`. +3. `jarca-ledger-mcp` starts FastMCP with `JARCA_MCP_TRANSPORT=streamable-http`, `JARCA_MCP_HOST=0.0.0.0`, `JARCA_MCP_PORT=8000`, and path `/mcp`. +4. Finance agents connect to `http://:8000/mcp` or a shared Docker network DNS endpoint. +5. The MCP adapter routes tool calls through `adapters/mcp/tool_contracts.py` and existing application/persistence services. +6. Docker healthcheck verifies TCP readiness on `127.0.0.1:8000`; functional readiness is verified by MCP tool discovery and `ledger_health_check`. + +## Flow: `Operating DB migration to jarca-ledger container` +1. Identify source DB in the legacy `jarca` container: `/root/.local/share/jarca/ledger.sqlite3`. +2. Create a consistent source snapshot using SQLite `Connection.backup()` rather than copying a live DB file directly. +3. Back up the current target `/data/ledger.sqlite3` under `/data/backups/` before replacement. +4. Stop `jarca-ledger`, remove target WAL/SHM sidecar files, copy the snapshot to `/data/ledger.sqlite3`, and restart. +5. Verify Docker health, SQLite `PRAGMA integrity_check`, table counts, sha256, and MCP `get_ledger_db_status`. + ## Flow: `MCP server tool call` 1. Hermes MCP client invokes a tool exposed by `jarca_ledger_mcp.server` / `jarca_ledger_mcp.adapters.mcp.server`. 2. MCP adapter forwards arguments to the matching callable exposed by `adapters/mcp/tool_contracts.py`. @@ -121,7 +138,7 @@ ## Current Gaps / Planned Changes - Browser-level evidence exists locally and remains outside required CI gates by design. - Post-Phase-7 adapter narrowing is complete for the current compatibility architecture; deeper repository/use-case extraction and richer presenter/view-model layers remain future work. -- HTTP adapter and remote ops re-architecture remain deferred. +- Streamable HTTP MCP is now deployed for internal Synology operation. External exposure, reverse proxy/TLS/auth, and broader HTTP/REST adapter work remain deferred. ## Update Triggers - Update this document when a command entry point changes. @@ -138,3 +155,4 @@ - 2026-06-30 KST: REQ-007/REQ-008/BAT-007 기준으로 contract-regression/lint CI required-check flow 추가. - 2026-07-03 KST: REQ-013/REQ-014/BAT-010 기준으로 service facade → application, db shim → infrastructure/sqlite, MCP/Streamlit adapter seam flow를 반영했다. - 2026-07-03 KST: REQ-016/REQ-017/REQ-018 및 BAT-012/BAT-013/BAT-014 기준으로 imports/advanced owning services, parser seam, MCP tool_contracts, Streamlit page/domain gateway flow를 반영했다. +- 2026-07-07 KST: REQ-019/BAT-015 기준으로 Docker network MCP service flow와 legacy `jarca` DB → `jarca-ledger` volume migration flow를 추가했다. diff --git a/docs/srs/Deployment/req-019_docker-network-mcp-deployment.md b/docs/srs/Deployment/req-019_docker-network-mcp-deployment.md new file mode 100644 index 0000000..3c5101b --- /dev/null +++ b/docs/srs/Deployment/req-019_docker-network-mcp-deployment.md @@ -0,0 +1,88 @@ +# REQ-019 Docker network MCP deployment and operating DB migration + +- Status: Implemented +- Type: Deployment +- Priority: High +- Owner: 렛저장인 +- Source Discovery: `docs/discovery/dcy-013_20260707_docker-network-mcp-deployment.md` +- Batch: BAT-015 +- Created At (KST): 2026-07-07 22:23 KST +- Approved At (KST): 2026-07-07 22:23 KST +- Implemented At (KST): 2026-07-07 22:23 KST + +## 1. 배경 + +재무관리 에이전트가 Jarca Ledger를 사용하려면 `genichin-agent`가 stdio 프로세스를 실행해 주는 구조가 아니라, `jarca-ledger` 자체가 독립 Docker 서비스로 실행되고 network MCP endpoint를 직접 제공해야 한다. 또한 기존 `jarca` 컨테이너의 운영 DB가 새 volume으로 이전되어야 빈 ledger를 운영 source로 오인하지 않는다. + +## 2. Intent + +`jarca-ledger` 컨테이너를 Synology Docker daemon 아래 독립 실행 서비스로 배포하고, `streamable-http` MCP endpoint와 `/volume1/docker/jarca-ledger/data` 영속 volume을 운영 기준으로 고정한다. 기존 `jarca` 운영 DB를 안전하게 이전하고, MCP smoke와 DB status evidence로 재무관리 에이전트 투입 가능 상태를 검증한다. + +## 3. 범위 + +### In Scope +- FastMCP runtime transport 설정화: stdio 기본값 유지 + `streamable-http` 선택 가능 +- Dockerfile, `.dockerignore`, `docker-compose.yml` 추가 +- `container_name=jarca-ledger`, `restart=unless-stopped`, port `8000:8000`, volume `/volume1/docker/jarca-ledger/data:/data` +- 기존 `jarca` DB snapshot 및 새 volume 교체 +- network MCP tool discovery / health / DB status smoke +- Docker deployment runbook 및 baseline docs 갱신 + +### Out of Scope +- public internet exposure, TLS, reverse proxy, auth policy +- 별도 REST business API +- dcy-012 review-decision persistence 기능 +- 자동 backup scheduler + +## 4. Requirement + +### FR-1. MCP runtime transport configuration +`jarca-ledger-mcp`는 기본 stdio 실행을 보존하면서 환경변수 `JARCA_MCP_TRANSPORT=streamable-http`, `JARCA_MCP_HOST`, `JARCA_MCP_PORT`, `JARCA_MCP_STREAMABLE_HTTP_PATH`로 network MCP endpoint를 제공할 수 있어야 한다. + +### FR-2. Independent Docker deployment +Repository는 `jarca-ledger:latest` image를 빌드하고 `jarca-ledger` 컨테이너를 `restart: unless-stopped`로 실행하는 Docker compose 정의를 제공해야 한다. + +### FR-3. Synology volume and DB path +컨테이너는 host `/volume1/docker/jarca-ledger/data`를 `/data`로 mount하고 `JARCA_LEDGER_DB_PATH=/data/ledger.sqlite3`를 사용해야 한다. + +### FR-4. Operating DB migration +기존 `jarca` 컨테이너의 `/root/.local/share/jarca/ledger.sqlite3` 운영 DB를 SQLite backup snapshot으로 복사해 새 `/data/ledger.sqlite3`로 이전해야 하며, 이전 전 빈 DB 백업을 남겨야 한다. + +### FR-5. Deployment documentation and baselines +`docs/runbooks/docker-network-mcp-deploy.md`, `docs/project-structure.md`, `docs/runtime-flows.md`는 network MCP endpoint, Docker service, volume, 운영 검증 절차를 설명해야 한다. + +## 5. Acceptance Criteria + +- AC-1: `tests/test_mcp_http_runtime.py`가 stdio default, streamable-http env, invalid transport rejection을 검증하고 통과한다. +- AC-2: `docker compose build`와 `docker compose up -d --no-build` 후 `jarca-ledger`가 `running`, `healthy`, `restart=unless-stopped` 상태다. +- AC-3: Docker inspect에서 `/volume1/docker/jarca-ledger/data -> /data` bind mount가 확인된다. +- AC-4: MCP streamable-http smoke가 `http://192.168.55.253:8000/mcp`에서 56개 tool discovery와 `ledger_health_check` 성공을 확인한다. +- AC-5: `get_ledger_db_status`가 `/data/ledger.sqlite3`, `integrity_check=ok`, `accounts=8`, `transactions=6312`, `import_batches=13`, `category_rules=46`, `ledger_categories=62`를 보고한다. +- AC-6: 전체 `python -m pytest -q`가 통과한다. +- AC-7: 배포 runbook과 baseline docs가 network MCP 운영 구조를 반영한다. + +## 6. Impacted Area + +- `jarca_ledger_mcp/adapters/mcp/server.py` +- `tests/test_mcp_http_runtime.py` +- `Dockerfile` +- `.dockerignore` +- `docker-compose.yml` +- `docs/runbooks/docker-network-mcp-deploy.md` +- `docs/project-structure.md` +- `docs/runtime-flows.md` +- Synology host path `/volume1/docker/jarca-ledger/data` + +## 7. Verification Evidence Summary + +- Full pytest: `python -m pytest -q` → PASS (`........................ss..................ss... [100%]`) +- Runtime config tests: `python -m pytest tests/test_mcp_http_runtime.py -q` → PASS (`... [100%]`) +- Docker image build: `docker compose build` → PASS, image `jarca-ledger:latest` built +- Container start: `docker compose up -d --no-build` → container `jarca-ledger` started +- Health: `state=running health=healthy restart=unless-stopped` +- MCP smoke: `tool_count 56`, `ledger_health_check success true`, `db_path /data/ledger.sqlite3` +- DB status after migration: `integrity_check ok`, `accounts=8`, `transactions=6312`, `import_batches=13`, `category_rules=46`, `ledger_categories=62` + +## 8. Change Log + +- 2026-07-07 22:23 KST: dcy-013 범위를 바탕으로 REQ-019를 생성하고, 이미 수행된 Docker network MCP 구현/배포/DB 이전 evidence를 근거로 Implemented 상태로 기록했다. diff --git a/docs/srs/index.md b/docs/srs/index.md index 7f65b23..0e3b2eb 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -33,7 +33,7 @@ ## Next Requirement ID -- Current: `REQ-019` +- Current: `REQ-020` - Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다. ## Requirement Register @@ -58,6 +58,7 @@ | REQ-016 | Import pipeline application/infrastructure split and evidence-preserving parser relocation | Non-Functional | Implemented | High | 렛저장인 | [req-016_import-pipeline-application-infrastructure-split.md](Non-Functional/req-016_import-pipeline-application-infrastructure-split.md) | | REQ-017 | Advanced finance workflow service split and compatibility-preserving domain isolation | Non-Functional | Implemented | High | 렛저장인 | [req-017_advanced-finance-workflow-service-split.md](Non-Functional/req-017_advanced-finance-workflow-service-split.md) | | REQ-018 | MCP/Streamlit adapter surface narrowing and baseline refresh for dcy-011 completion | Interface | Implemented | High | 렛저장인 | [req-018_adapter-surface-narrowing-baseline-refresh.md](Interface/req-018_adapter-surface-narrowing-baseline-refresh.md) | +| REQ-019 | Docker network MCP deployment and operating DB migration | Deployment | Implemented | High | 렛저장인 | [req-019_docker-network-mcp-deployment.md](Deployment/req-019_docker-network-mcp-deployment.md) | ## Recent Change Log Summary @@ -92,3 +93,4 @@ - 2026-07-03 08:01 KST: BAT-013 advanced workflow split 구현과 full pytest/MCP discovery evidence를 근거로 `REQ-017`을 Implemented로 전환했다. - 2026-07-03 08:03 KST: dcy-011 Phase 7 adapter narrowing + baseline refresh 범위를 `REQ-018`로 생성하고 BAT-014 입력으로 Approved 상태를 확정했다. - 2026-07-03 08:23 KST: BAT-014 adapter narrowing + baseline refresh 구현과 AppTest/full pytest/MCP discovery evidence를 근거로 `REQ-018`을 Implemented로 전환했다. +- 2026-07-07 22:23 KST: dcy-013 Docker network MCP 배포 범위로 REQ-019를 생성하고, BAT-015 구현·검증 evidence 및 운영 DB 이전 완료를 근거로 Implemented 상태로 등록했다. diff --git a/jarca_ledger_mcp/adapters/mcp/server.py b/jarca_ledger_mcp/adapters/mcp/server.py index bd6b9b0..a82d06b 100644 --- a/jarca_ledger_mcp/adapters/mcp/server.py +++ b/jarca_ledger_mcp/adapters/mcp/server.py @@ -1,13 +1,80 @@ from __future__ import annotations +import os +from dataclasses import dataclass from pathlib import Path +from typing import Literal + from dotenv import load_dotenv from mcp.server.fastmcp import FastMCP from . import tool_contracts as contracts load_dotenv(Path(__file__).resolve().parents[3] / ".env") -mcp = FastMCP("jarca-ledger-mcp") + +McpTransport = Literal["stdio", "sse", "streamable-http"] +_ALLOWED_TRANSPORTS: set[str] = {"stdio", "sse", "streamable-http"} + + +@dataclass(frozen=True) +class McpRuntimeConfig: + transport: McpTransport = "stdio" + host: str = "127.0.0.1" + port: int = 8000 + streamable_http_path: str = "/mcp" + sse_path: str = "/sse" + message_path: str = "/messages/" + stateless_http: bool = False + + +def _env_bool(name: str, default: bool = False) -> bool: + raw = os.getenv(name) + if raw is None or raw == "": + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def _env_int(name: str, default: int) -> int: + raw = os.getenv(name) + if raw is None or raw == "": + return default + try: + return int(raw) + except ValueError as exc: + raise ValueError(f"{name} must be an integer, got {raw!r}") from exc + + +def _env_path(name: str, default: str) -> str: + raw = os.getenv(name, default).strip() or default + return raw if raw.startswith("/") else f"/{raw}" + + +def get_mcp_runtime_config() -> McpRuntimeConfig: + transport = os.getenv("JARCA_MCP_TRANSPORT", "stdio").strip() or "stdio" + if transport not in _ALLOWED_TRANSPORTS: + allowed = ", ".join(sorted(_ALLOWED_TRANSPORTS)) + raise ValueError(f"JARCA_MCP_TRANSPORT must be one of: {allowed}; got {transport!r}") + return McpRuntimeConfig( + transport=transport, # type: ignore[arg-type] + host=os.getenv("JARCA_MCP_HOST", "127.0.0.1").strip() or "127.0.0.1", + port=_env_int("JARCA_MCP_PORT", 8000), + streamable_http_path=_env_path("JARCA_MCP_STREAMABLE_HTTP_PATH", "/mcp"), + sse_path=_env_path("JARCA_MCP_SSE_PATH", "/sse"), + message_path=_env_path("JARCA_MCP_MESSAGE_PATH", "/messages/"), + stateless_http=_env_bool("JARCA_MCP_STATELESS_HTTP", False), + ) + + +_runtime_config = get_mcp_runtime_config() +mcp = FastMCP( + "jarca-ledger-mcp", + host=_runtime_config.host, + port=_runtime_config.port, + streamable_http_path=_runtime_config.streamable_http_path, + sse_path=_runtime_config.sse_path, + message_path=_runtime_config.message_path, + stateless_http=_runtime_config.stateless_http, +) @mcp.tool() def ledger_health_check() -> dict: @@ -290,7 +357,7 @@ def delete_ledger_category(category: str, subcategory: str = "", hard_delete: bo return contracts.delete_ledger_category(category, subcategory, hard_delete, force) def main() -> None: - mcp.run() + mcp.run(transport=_runtime_config.transport) if __name__ == "__main__": main() diff --git a/tests/test_mcp_http_runtime.py b/tests/test_mcp_http_runtime.py new file mode 100644 index 0000000..2d0fb56 --- /dev/null +++ b/tests/test_mcp_http_runtime.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import importlib + + +def test_mcp_runtime_config_defaults_to_stdio(monkeypatch): + monkeypatch.delenv("JARCA_MCP_TRANSPORT", raising=False) + monkeypatch.delenv("JARCA_MCP_HOST", raising=False) + monkeypatch.delenv("JARCA_MCP_PORT", raising=False) + monkeypatch.delenv("JARCA_MCP_STREAMABLE_HTTP_PATH", raising=False) + + server = importlib.import_module("jarca_ledger_mcp.adapters.mcp.server") + config = server.get_mcp_runtime_config() + + assert config.transport == "stdio" + assert config.host == "127.0.0.1" + assert config.port == 8000 + assert config.streamable_http_path == "/mcp" + + +def test_mcp_runtime_config_accepts_streamable_http_env(monkeypatch): + monkeypatch.setenv("JARCA_MCP_TRANSPORT", "streamable-http") + monkeypatch.setenv("JARCA_MCP_HOST", "0.0.0.0") + monkeypatch.setenv("JARCA_MCP_PORT", "8765") + monkeypatch.setenv("JARCA_MCP_STREAMABLE_HTTP_PATH", "/ledger-mcp") + + server = importlib.import_module("jarca_ledger_mcp.adapters.mcp.server") + config = server.get_mcp_runtime_config() + + assert config.transport == "streamable-http" + assert config.host == "0.0.0.0" + assert config.port == 8765 + assert config.streamable_http_path == "/ledger-mcp" + + +def test_mcp_runtime_config_rejects_invalid_transport(monkeypatch): + monkeypatch.setenv("JARCA_MCP_TRANSPORT", "websocket") + + server = importlib.import_module("jarca_ledger_mcp.adapters.mcp.server") + + try: + server.get_mcp_runtime_config() + except ValueError as exc: + assert "JARCA_MCP_TRANSPORT" in str(exc) + else: + raise AssertionError("invalid MCP transport should be rejected")