diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 127deac..7054c16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: tests/test_view_markdown.py tests/test_view_html.py \ tests/test_view_baseline.py \ tests/test_cli.py \ + tests/test_mcp_server.py \ tests/conftest.py tests/type_check_samples.py - name: Run pyright (intentional errors — expect 4) if: matrix.lint @@ -235,13 +236,14 @@ jobs: - run: uv pip install dist/*.whl - name: Run pytest — extras-gated tests must auto-skip via importorskip # ^ 파일-레벨 importorskip 은 해당 파일 전체를 skip 1개로 카운트. - # v0.3.0 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py - # (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer) - # → 총 4 파일. test_async.py 는 v0.3.0 부터 stdlib 만 사용 (aiofiles 의존성 제거) + # v0.5.0 S1 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py + # (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer), + # test_mcp_server.py (fastmcp) → 총 5 파일. test_async.py 는 v0.3.0 부터 + # stdlib 만 사용 (aiofiles 의존성 제거). run: | uv run pytest tests/ -m "not slow" -v | tee pytest-output.txt - if ! grep -qE '(^|[^0-9])4 skipped([^0-9]|$)' pytest-output.txt; then - echo "::error::expected 4 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer)" + if ! grep -qE '(^|[^0-9])5 skipped([^0-9]|$)' pytest-output.txt; then + echo "::error::expected 5 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer, fastmcp)" exit 1 fi diff --git a/.gitignore b/.gitignore index dd2c29f..37a3853 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ mutants/ # * Examples 산출물 render_output/ + +.mcp.json \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3680da5..2e06b64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Project-specific instructions. Inherits all rules from `~/.claude/CLAUDE.md` (gl - `abi3-py310` feature: **one wheel covers 3.10–3.13+**. Don't bind to Python version-specific C API ### Async direction -- Python-surface APIs for I/O and integrations are **async-first**: when adding LangChain / LlamaIndex / Haystack loaders, implement `aload` / `alazy_load` / async counterparts alongside sync versions +- Python-surface APIs for I/O and integrations are **async-first**: when adding LangChain (or future RAG framework) loaders, implement `aload` / `alazy_load` / async counterparts alongside sync versions - **Forbidden pattern**: `asyncio.to_thread(rhwp.parse, path)` — `_Document` is unsendable (see Rust+Python hybrid build note above), the returned Document panics on main-thread access. `async fn` in `#[pymethods]` is also incompatible (PyO3 requires `Send + 'static` futures) - **Supported async pattern**: `aparse(path)` uses stdlib `asyncio.to_thread` to offload the file read to a thread pool, then calls `Document.from_bytes(data)` on the event-loop thread. Document never crosses a thread boundary. No external dependency — Python `asyncio` lacks native async file I/O so all async file libs (aiofiles etc.) wrap thread pools anyway; stdlib achieves the same effect with zero install footprint - **Document instance-level async methods (`doc.ato_ir()` etc.) are NOT provided** — they would require thread offload which unsendable forbids. For async code, `await rhwp.aparse(path)` once, then call sync methods on the Document directly (these are fast, in-memory, GIL-holding operations) @@ -33,7 +33,7 @@ Project-specific instructions. Inherits all rules from `~/.claude/CLAUDE.md` (gl - Real HWP fixtures live in the submodule: `external/rhwp/samples/aift.hwp` (HWP5), `table-vpos-01.hwpx` (HWPX). `tests/conftest.py` + `benches/bench_gil.py` reference this path - When changing one path, change both - Markers: `slow` (PDF render), `langchain` (extras required). Default run: `pytest -m "not slow"` -- Extras-gated test files use module-level `pytest.importorskip` so the whole file counts as **1 skip** when the extra is missing. Current gated files: `test_langchain_loader.py` + `test_langchain_loader_ir.py` (langchain-core), `test_ir_schema_export.py` (jsonschema), `test_cli.py` (typer) → CI's `test-without-extras` job validates **exactly 4 skipped** (see `.github/workflows/ci.yml`). When adding a new extras-gated file, bump the count in both AGENTS.md and ci.yml +- Extras-gated test files use module-level `pytest.importorskip` so the whole file counts as **1 skip** when the extra is missing. Current gated files: `test_langchain_loader.py` + `test_langchain_loader_ir.py` (langchain-core), `test_ir_schema_export.py` (jsonschema), `test_cli.py` (typer), `test_mcp_server.py` (fastmcp) → CI's `test-without-extras` job validates **exactly 5 skipped** (see `.github/workflows/ci.yml`). When adding a new extras-gated file, bump the count in both AGENTS.md and ci.yml - `tests/type_check_errors.py` holds **exactly 4 intentional pyright errors** — CI validates that too. When editing, preserve count; don't fix them ### Git workflow diff --git a/CHANGELOG.md b/CHANGELOG.md index df0b7a3..c967552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 부수 — `test_submodule_pin_matches_changelog_record` 제거 ([tests/test_ir_marker_char_offset.py](tests/test_ir_marker_char_offset.py)). 본래 v0.3.1 의 *deliberate* pin bump (v0.7.7 → 0fb3e67) 가 release-readiness 시점 기재됐는지 가드한 일회성 AC-13 의 일부였으나, GA shipped 후에도 영구 runtime 가드로 잔존해 일상 sync 마다 CHANGELOG 갱신을 강제하는 anti-pattern (1회성 release-gating AC 의 영구 테스트화 + 문서 텍스트 매칭 runtime test) 화. CHANGELOG 갱신은 릴리즈 시점 의무 (`publish.yml::verify-version` 로 version 일치 가드, 사람 review 가 핀 bump 정합성 점검) 로 이양. AC-13 의 historical record 검증은 동 파일 `test_changelog_records_pin_bump` (Frozen v0.3.1 섹션 텍스트 회귀 가드) 가 그대로 유지. - 부수 — 동 파일 이름 `test_v0_3_1_marker_char_offset.py` → `test_ir_marker_char_offset.py` (`test_ir_*` 패턴 통일, 본 spec lifecycle 은 `pytest.mark.spec("v0.3.1/...")` marker 가 보유). +## [0.5.0] — 2026-05-06 + +MINOR release. [Model Context Protocol](https://modelcontextprotocol.io/) (Anthropic, 2024) 서버를 새 entry point `rhwp-mcp` 로 노출한다. LLM 에이전트 (Claude Desktop / Cursor / Cline / Continue.dev / Goose / 자체 에이전트) 가 HWP/HWPX 를 직접 파싱·요약·청크화 가능. standalone [`fastmcp`](https://github.com/jlowin/fastmcp) v3 (jlowin) 기반 — 2026-05 기준 MCP 서버 약 70% 시장 점유의 사실상 표준. 7 도구 / 2 transport (stdio 기본 + streamable-http 옵션) / runtime extras gate / `unsendable` 안전 패턴 강제. 코어 wheel 의존성 변경 0 (additive extras), schema (`"1.1"`) 유지. + +### Added + +- 새 entry point `rhwp-mcp = "rhwp.mcp:run"` — argparse 기반 `--transport stdio | streamable-http` / `--host` / `--port` CLI. stdio 가 기본 (Claude Desktop / IDE 통합용), streamable-http 는 옵션 (서버 배포 / 다중 클라이언트, uvicorn ASGI). `--host` 기본 `127.0.0.1` 외부 노출 회피 + `--port` [1, 65535] argparse validator + stdio 와 명시적 host/port 조합 시 `parser.error` 강제 (silent ignore 보안 사고 회피). +- 7 MCP 도구 (`mcp.server.fastmcp.FastMCP.tool()` 등록): `parse_hwp_summary(path)` / `extract_text(path)` / `get_ir(path)` / `iter_blocks(path, kind?, scope, limit?)` / `to_markdown(path)` / `to_html(path, *, include_css=False)` / `chunks(path, mode, size, overlap, include_furniture)`. 모두 sync 함수 — `_Document` 가 `unsendable` 이라 handler 안에서 parse → consume → primitive return 패턴 강제 (async + `asyncio.to_thread(rhwp.parse, ...)` 는 panic). `chunks` 는 런타임 lazy import — `langchain-text-splitters` 미설치 시 fastmcp `ToolError` 로 wrap → MCP `CallToolResult(isError=True)` 응답 (서버 기동 / 다른 6 도구는 정상 — AC-7). +- 모듈 위치: `python/rhwp/mcp/{__init__.py, __main__.py, server.py, tools.py}` (top-level, `integrations/` 가 아님). `__init__.py` 는 lazy-import 패턴 — `rhwp.cli` 와 동일하게 `[mcp]` extras 미설치 시 친절 에러 + exit 2 (AC-1). +- 새 extras: `[project.optional-dependencies] mcp = ["fastmcp>=3,<4"]` + `mcp-chunks = ["fastmcp>=3,<4", "langchain-core>=0.2", "langchain-text-splitters>=0.2"]`. extras 키 이름은 "MCP 서버 기능" 표시 — 의존성 패키지명 (`fastmcp`) 과 분리. `[examples]` extras 가 fastmcp 합집합 포함하도록 갱신. +- `examples/06_mcp_server.py` — fastmcp `Client(server)` in-process round-trip 데모 (typer 기반, `--skip-chunks` 옵션). 7 도구 차례로 호출하며 출력 형식 학습용. +- README § "MCP server (`rhwp-mcp`)" 섹션 신설 — 도구 7 종 표 / Claude Desktop `claude_desktop_config.json` 등록 예 / 클라이언트 호환성 표 (Claude Desktop / Cline / Cursor / Continue.dev / Goose / 자체 에이전트, transport 별 ✅/❌/⚠️) / streamable-http 사용 예. +- spec / ADR / 구현 로그: [docs/roadmap/v0.5.0/mcp.md](docs/roadmap/v0.5.0/mcp.md) (Frozen, 11 인수조건) / [docs/design/v0.5.0/mcp-research.md](docs/design/v0.5.0/mcp-research.md) (Frozen, 4 결정 매트릭스 — SDK 채택 근거 / transport 우선순위 / handler 동시성 / 도구 분할) / [docs/implementation/v0.5.0/stages/](docs/implementation/v0.5.0/stages/) (S1 ~ S5 Frozen). + +### Changed + +- ADR § 1 SDK 결정 갱신 (S1 진행 중) — 공식 `mcp` Python SDK (FastMCP v1 흡수) → standalone `fastmcp` v3 (jlowin). 2026-05 현업 표준 패턴 정합 (시장 점유 약 70%) + v3 의 OAuth / OpenTelemetry / server composition / streamable-http 우선 같은 프로덕션 기능. 공식 SDK 의 FastMCP v1 은 frozen 상태 — 추가 framework 기능은 standalone 으로만 발전. +- `docs-lint` 정책 갱신 (S1 진행 중) — `Frozen + target` 조합을 `docs/implementation/vX.Y.Z/` pre-GA stage log 에 한해 허용 (`scripts/_doc_lint.py` 의 `is_pre_ga_stage` 면제 분기). Rust RFC / PEP / ADR 의 editorial vs release 차원 분리 패턴 정합 — stage 본문은 작성 즉시 immutable, GA 라벨은 미부여 (CONVENTIONS § 131 의 의도). [CONVENTIONS.md § 필드 schema](docs/CONVENTIONS.md) 에 예외 명시. +- CI `test-without-extras` job — expected skip count 4 → 5 (`tests/test_mcp_server.py` 의 file-level `pytest.importorskip("fastmcp")` 추가). `.github/workflows/ci.yml` + `AGENTS.md` § Tests 동시 갱신 (AC-11). + +### Build + +- `external/rhwp` submodule pin `0fb3e67` 유지 — 본 MINOR 는 pure Python MCP layer, 상류 변경 0. +- 신규 의존성 (extras 만): `fastmcp>=3,<4` (`[mcp]`, `[mcp-chunks]`, `[examples]`, `[dependency-groups] testing`). 코어 wheel 의존성 (`pydantic>=2.5,<3`) 변경 없음. + +### Notes + +- 회귀 가드: [tests/test_mcp_server.py](tests/test_mcp_server.py) (40 테스트 — 36 fast + 1 slow + 3 LOW reviewer-suggested). file-level `importorskip("fastmcp")` 게이트, 메서드별 `importorskip("langchain_text_splitters")` 게이트로 chunks smoke 분리. AC-1 ~ AC-11 모두 11/11 충족 (evidence 매핑은 [stage-5.md § AC sweep](docs/implementation/v0.5.0/stages/stage-5.md) 참조). +- 미확정 이슈는 v0.5.0 GA 후 demand-driven: `get_ir` 응답 크기 / 에러 응답 형식 통일 / Resource·Prompt 추상 / 출력 schema 강타입화 (`ChunkRecord` 등). spec [§ 미확정 이슈](docs/roadmap/v0.5.0/mcp.md) 에 기록. + ## [0.4.0] — 2026-05-05 MINOR release. Document IR (`HwpDocument`) → Markdown / HTML view 변환 표면을 추가한다. v0.7.0 MCP server (`to_markdown` / `to_html` 도구) + 후속 RAG 프레임워크 통합 (v0.5 LlamaIndex / v0.6 Haystack) 의 *문자열 출력* 1차 인터페이스로 사용. Pure-stdlib 구현 — 신규 의존성 0, schema (`"1.1"`) / 파싱 경로 / `Document` wrapper / extras 모두 변경 없음 (additive only). diff --git a/Cargo.toml b/Cargo.toml index 9cb2ea0..3e3f6cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhwp-python" -version = "0.4.0" +version = "0.5.0" edition = "2021" # ^ rust-version 미명시 — 상위 rhwp crate 정책(stable Rust, MSRV unclaimed) 준수. # PyO3 0.28 이 Rust 1.83+ 요구하지만, 이는 README 에 문서로 안내 diff --git a/README.md b/README.md index f31542c..a0d70d1 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,71 @@ rhwp-py chunks report.hwp --size 1000 --format ndjson 메타데이터 덤프는 상류 `rhwp` Rust 바이너리. 자세한 사용은 `rhwp-py --help` 또는 [cli.md](docs/roadmap/v0.3.0/cli.md) 참조. +## MCP server (`rhwp-mcp`) + +[Model Context Protocol](https://modelcontextprotocol.io/) 서버 — Claude Desktop / +Cursor / Cline / Continue.dev / Goose 등 LLM 에이전트가 HWP/HWPX 파일을 직접 +파싱·요약·청크화할 수 있다. standalone [fastmcp v3](https://github.com/jlowin/fastmcp) +기반 (2026-05 기준 MCP 서버 약 70% 시장 점유의 사실상 표준). + +```bash +pip install "rhwp-python[mcp]" # 도구 6 종 (parse / extract / IR / blocks / view×2) +pip install "rhwp-python[mcp-chunks]" # + chunks (RAG 청킹 — langchain-text-splitters) +``` + +### 노출 도구 (7 종) + +| 도구 | 입력 | 출력 | +|---|---|---| +| `parse_hwp_summary` | `path` | sections / paragraphs / pages 카운트 + rhwp-core 버전 | +| `extract_text` | `path` | 단락별 평문 (LF 결합) | +| `get_ir` | `path` | Document IR 전체 (JSON-serializable dict) | +| `iter_blocks` | `path`, `kind?`, `scope`, `limit?` | IR 블록 dict 리스트 (kind / scope 필터링) | +| `to_markdown` | `path` | GFM Markdown — v0.4.0 view API thin wrapper | +| `to_html` | `path`, `include_css` | HTML5 문서 — v0.4.0 view API thin wrapper | +| `chunks` | `path`, `mode`, `size`, `overlap`, `include_furniture` | LangChain `RecursiveCharacterTextSplitter` 적용 청크 — `[mcp-chunks]` extras 필요 | + +### Claude Desktop 등록 + +`claude_desktop_config.json` 에 추가: + +```json +{ + "mcpServers": { + "rhwp": { + "command": "rhwp-mcp" + } + } +} +``` + +(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`. Windows: +`%APPDATA%\Claude\claude_desktop_config.json`.) Claude Desktop 재시작 후 도구 +아이콘에 7 개 도구 노출. + +### 다른 클라이언트 + +| 클라이언트 | stdio | streamable-http | 등록 방법 | +|---|---|---|---| +| Claude Desktop | ✅ | ❌ | `claude_desktop_config.json` (위 예시) | +| Cline (VSCode) | ✅ | ✅ | VSCode 설정 → MCP servers | +| Cursor | ✅ | ❌ | Settings → Features → Model Context Protocol | +| Continue.dev | ✅ | ⚠️ (실험) | `~/.continue/config.json` | +| Goose (Block) | ✅ | ✅ | `goose configure` | +| 자체 에이전트 | ✅ | ✅ | Anthropic SDK 의 MCP client / fastmcp Client | + +### Streamable HTTP (서버 배포) + +서버 컨테이너 / 다중 클라이언트 시나리오는 streamable-http transport: + +```bash +rhwp-mcp --transport streamable-http --port 8000 +# 외부 노출 (보안: reverse proxy + 인증 운영자 책임) +rhwp-mcp --transport streamable-http --host 0.0.0.0 --port 8000 +``` + +기본 `--host 127.0.0.1` — 외부 노출 회피. `rhwp-mcp` 는 인증 / TLS / sandboxing 미내장 — Caddy / Nginx 등 reverse proxy 가 책임. 자세한 사용은 `rhwp-mcp --help` 또는 [mcp.md](docs/roadmap/v0.5.0/mcp.md) 참조. + ## 성능 Apple M2 (8 코어) release 빌드. Parse = 파일 읽기 + 전체 파싱 + Document 생성. diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index c137480..7ec2ebc 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -8,7 +8,7 @@ |---|---|---|---| | **Living** | 항상 최신 — 다른 문서의 위치 포인터 + 시간선 + 규칙 | 자유 갱신, 매 변경 시 손봐도 무방 | `docs/CONVENTIONS.md` (자체), `docs/roadmap/README.md`, `docs/upstream/README.md`, `docs/traces/coverage.md`, `CHANGELOG.md`, `CLAUDE.md`, `AGENTS.md`, `README.md` | | **Active** | 외부 시스템으로 흘러가기 전 staging | 큰 변경만, in-place 갱신 OK | `docs/upstream/.md` | -| **Draft** | 작성 중인 spec — 해당 버전 GA 전까지 활발 갱신 | 버전 GA 전까지 자유 갱신, GA 후 Frozen 으로 전환 | `docs/roadmap/v0.7.0/mcp.md` (현재 v0.7.0 GA 전) | +| **Draft** | 작성 중인 spec — 해당 버전 GA 전까지 활발 갱신 | 버전 GA 전까지 자유 갱신, GA 후 Frozen 으로 전환 | `docs/roadmap/v/.md` (target 미-GA 버전) | | **Frozen** | GA 완료된 spec / 완료된 stage / 완료된 검증 | **변경 금지** — 오타·링크 수정만 in-place 허용. 큰 변경은 새 spec + supersede | `docs/roadmap/v0.2.0/ir.md` (v0.2.0 GA 완료), `docs/implementation/v0.2.0/stages/*.md` | `Frozen` 은 [Rust RFC](https://rust-lang.github.io/rfcs/) / [Python PEP](https://peps.python.org/) 의 운영 모델. 결정의 historical record 가 보존되어 "왜 그렇게 설계됐는지" 가 명확해진다. @@ -52,8 +52,8 @@ last_updated: 2026-04-28 |---|---|---| | `status` | enum: `Active` / `Draft` / `Frozen` / `Superseded` | 필수 | | `description` | non-empty string (50-150 자 권장) | 필수. 한 줄 요약 — 인덱스/검색/툴팁용 (MkDocs / Hugo / Astro 패턴) | -| `ga` | `vX.Y.Z` SemVer | `status: Frozen` 또는 `Superseded` 일 때 필수 (예외: meta-level `docs/implementation/.md`, RESOLVED `docs/upstream/.md` — § Implementation log 구조 / § upstream/ 참조). `target` 과 mutex | -| `target` | `vX.Y.Z` SemVer | `status: Draft` 일 때 필수. `ga` 와 mutex | +| `ga` | `vX.Y.Z` SemVer | `status: Frozen` 또는 `Superseded` 일 때 필수 (예외: meta-level `docs/implementation/.md`, RESOLVED `docs/upstream/.md`, **pre-GA stage log** — § Implementation log 구조 / § upstream/ 참조). `target` 과 mutex (단, pre-GA stage 예외) | +| `target` | `vX.Y.Z` SemVer | `status: Draft` 일 때 필수. `status: Frozen` + `target` 조합은 pre-GA stage log 에 한해 허용 (§ Implementation log 구조). `ga` 와 mutex | | `supersedes` | `/.md` 또는 생략 | 새 spec 이 무엇을 대체하는지 | | `superseded_by` | `/.md` | `status: Superseded` 일 때 필수 | | `last_updated` | `YYYY-MM-DD` | 필수. 의미 변경 commit 시 자동 갱신 ([D3 hook](#last_updated-자동-갱신)) | @@ -248,7 +248,7 @@ CHANGELOG 한 줄로 충분한 변경 (typo 정리, 단순 dep bump, 작은 docs spec-system-overhaul (2026-04-29) 이후 신규 작성 spec 의 § 인수조건 섹션은 각 항목에 `AC-N` ID 를 부여한다 (테스트 marker 와 1:1 매핑용). 형식은 자유 — testable 하고 명확하면 plain prose 도 OK. 모호성이 우려되면 [EARS notation](https://alistairmavin.com/ears/) (`THE ... SHALL`, `WHEN ..., THE ... SHALL` 등) 같은 구조화 패턴을 참고 가능 (강제 아님). -**적용 시점**: overhaul 발효일 (2026-04-29) 이후 신규 작성 spec. 첫 적용 사례는 [v0.3.1/ir-marker-char-offset](roadmap/v0.3.1/ir-marker-char-offset.md) (PATCH minor 라도 발효 이후 신규면 적용 — cutoff 는 버전이 아닌 *시점*). 발효일 *이전* 작성된 Draft 도 본 PR 에서 일괄 retrofit ([v0.7.0/mcp.md](roadmap/v0.7.0/mcp.md)). 향후 grandfather 가 발생하면 다음 의미 변경 PR 시점에 함께 retrofit. +**적용 시점**: overhaul 발효일 (2026-04-29) 이후 신규 작성 spec. 첫 적용 사례는 [v0.3.1/ir-marker-char-offset](roadmap/v0.3.1/ir-marker-char-offset.md) (PATCH minor 라도 발효 이후 신규면 적용 — cutoff 는 버전이 아닌 *시점*). 발효일 *이전* 작성된 Draft 도 본 PR 에서 일괄 retrofit. 향후 grandfather 가 발생하면 다음 의미 변경 PR 시점에 함께 retrofit. ```markdown ## 인수조건 diff --git a/docs/design/v0.7.0/mcp-research.md b/docs/design/v0.5.0/mcp-research.md similarity index 67% rename from docs/design/v0.7.0/mcp-research.md rename to docs/design/v0.5.0/mcp-research.md index e341939..6279afd 100644 --- a/docs/design/v0.7.0/mcp-research.md +++ b/docs/design/v0.5.0/mcp-research.md @@ -1,19 +1,19 @@ --- -status: Draft -description: "v0.7.0 MCP ADR — SDK 채택 (FastMCP) / transport (stdio + streamable-http) / handler 동시성 (sync 전용) / 도구 분할 (7 개) 결정 근거" -target: v0.7.0 -last_updated: 2026-04-30 +status: Frozen +description: "v0.5.0 MCP ADR — SDK 채택 (fastmcp v3 standalone) / transport (stdio + streamable-http) / handler 동시성 (sync 전용) / 도구 분할 (7 개) 결정 근거" +ga: v0.5.0 +last_updated: 2026-05-06 --- -# v0.7.0 MCP server — 설계 의사결정 리서치 요약 +# v0.5.0 MCP server — 설계 의사결정 리서치 요약 -[v0.7.0/mcp.md](../../roadmap/v0.7.0/mcp.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 4건 (SDK 채택 · transport 우선순위 · handler 동시성 모델 · 도구 분할 정책) 의 업계 선례·대안·실패 시나리오를 기록한다. mcp.md 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다. +[v0.5.0/mcp.md](../../roadmap/v0.5.0/mcp.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 4건 (SDK 채택 · transport 우선순위 · handler 동시성 모델 · 도구 분할 정책) 의 업계 선례·대안·실패 시나리오를 기록한다. mcp.md 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다. ## 결정 매트릭스 | # | 이슈 | 결정 | 핵심 근거 | |---|---|---|---| -| 1 | MCP SDK | **공식 `mcp` Python SDK (FastMCP)** | Anthropic 1st-party 유지 + spec 변동 흡수 비용 0 | +| 1 | MCP SDK | **standalone `fastmcp` v3 (jlowin)** | 2026-05 현업 표준 — MCP 서버의 약 70% 가 fastmcp 사용. v3 는 OAuth / OpenTelemetry / server composition / OpenAPI 통합 등 프로덕션 기능 제공 | | 2 | Transport 우선순위 | **stdio 기본 + streamable-http 옵션** | Claude Desktop 호환 (stdio-only) + ASGI 배포 시나리오 양쪽 커버 | | 3 | Handler 동시성 모델 | **sync 전용** | `_Document` `unsendable` 제약 — async + to_thread 는 panic | | 4 | 도구 분할 | **작은 도구 7개** (단일 통합 도구 X) | LLM 의도 명확화 + JSON Schema 정확도 + CLI `rhwp-py` 와 1:1 정합 | @@ -22,36 +22,39 @@ last_updated: 2026-04-30 ## 1. SDK 선택 -### 조사: MCP 서버 구현 옵션 +### 조사: MCP 서버 구현 옵션 (2026-05 기준) -| 옵션 | 유지 주체 | 성숙도 | spec 추종 | +| 옵션 | 유지 주체 | 시장 점유 / 성숙도 | spec 추종 | |---|---|---|---| -| **공식 `mcp` Python SDK** (`/modelcontextprotocol/python-sdk`) | Anthropic | v1.12.4, FastMCP API 안정 | 1st-party — spec 갱신 즉시 반영 | +| **standalone `fastmcp` v3** ([jlowin/fastmcp](https://github.com/jlowin/fastmcp)) | 커뮤니티 (jlowin / PrefectHQ) | **MCP 서버 약 70%** (전 언어 통합), PyPI 일 100만 다운로드. v3 는 2026-02 출시 — OAuth / OpenTelemetry / server composition / OpenAPI 통합 / streamable-http 우선 | spec 변경 시 자체 추종 — v1 → v2 → v3 모두 동일 저자 (jlowin) 가 빠르게 흡수 | +| 공식 `mcp` Python SDK ([modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk)) | Anthropic | v1.27, **FastMCP v1 만 흡수** (2024). v2 / v3 분기는 공식 SDK 외부 진화 | 1st-party — 프로토콜 spec 즉시 반영, 단 framework 기능은 v1 로 frozen | | 직접 구현 (JSON-RPC over stdio) | 본 프로젝트 | — | spec drift 자체 추적 비용 | -| 3rd-party SDK (`fastmcp` standalone, `mcp-server-utils` 등) | 커뮤니티 | 변동 | wrap 위주 — 공식 SDK 의존성 그대로 발생 | ### 관찰 -1. **MCP spec 은 빠르게 진화 중** — 2024-11 (initial), 2025-03 (Streamable HTTP 채택, SSE deprecation) 주기. 직접 구현은 매 spec revision 마다 수정 부담 -2. **FastMCP 가 사실상 표준 사용 패턴** — `@mcp.tool()` 데코레이터 + Pydantic 자동 schema 생성. 공식 SDK 가 흡수 (`mcp.server.fastmcp.FastMCP`) -3. **동등 수준의 3rd-party 가 없음** — TypeScript / Rust 진영 (`/rust-mcp-stack/rust-mcp-sdk`) 은 활발하나, Python 은 공식 SDK 가 사실상 단독 +1. **MCP spec 은 빠르게 진화 중** — 2024-11 (initial), 2025-03 (Streamable HTTP 채택, SSE deprecation), 2026-02 (FastMCP v3 OAuth/OTel) 주기. 직접 구현은 매 spec revision 마다 수정 부담 +2. **fastmcp 가 사실상 표준 사용 패턴** — `@mcp.tool` 데코레이터 + Pydantic 자동 schema 생성. v1 은 공식 SDK 가 흡수 (`mcp.server.fastmcp.FastMCP`) 했으나 v2 / v3 의 추가 기능 (OAuth, OpenTelemetry tracing, server composition, OpenAPI 자동 변환) 은 standalone 에만 존재 +3. **공식 SDK 안의 FastMCP v1 은 frozen 상태** — Anthropic 은 프로토콜 구현에 집중, framework 기능은 standalone 으로 위임된 분업 구조. v3 의 server composition (`server.import_server()`) / streamable-http 우선 / 프로덕션 배포 도구 등은 공식 SDK 에 미존재 ### 대안 평가 - **직접 구현**: spec drift 부담 + JSON-RPC stdio framing 재구현 + tool schema 자동 생성 부재 → 모든 도구 schema 를 수작업. 가치 없음 -- **3rd-party wrap**: 공식 SDK 위에 얇은 layer 만 — extras 차원에서 의존성 늘어날 뿐 -- **공식 SDK**: ✅ 채택. 의존성 1개 (`mcp>=1.12`) 로 모든 transport · schema · lifecycle 커버 +- **공식 `mcp` SDK (FastMCP v1)**: 프로토콜 compliance 는 1st-party 라 안정. 그러나 v3 의 streamable-http 프로덕션 기능 / 다중 서버 composition / OAuth 가 부재 — v0.5.0 이후 운영 (S4 streamable-http, 미래 인증) 시 마이그레이션 부담 +- **standalone fastmcp v3**: ✅ 채택. 의존성 1개 (`fastmcp>=3,<4`) 로 모든 transport (stdio / streamable-http / sse) · schema · lifecycle · v3 추가 기능 커버. 시장 점유율 70% 가 도구 호환성 / 문서 / 커뮤니티 ecosystem 도 함께 보장 ### 실패 시나리오 (선택 후에도 감시) -- **공식 SDK breaking change** — v1.x 는 안정 표명되었으나 spec 자체가 변하면 SDK 도 따라감. extras pin 을 `mcp>=1.12,<2` 로 유지하고 SDK major 업그레이드 시 별도 평가 -- **FastMCP API deprecation** — `mcp.server.fastmcp` 가 더 저수준 `mcp.server.lowlevel` 로 이전 가능. spec 추종이 우선이라 따라감 +- **fastmcp v3 → v4 breaking change** — jlowin 의 v 단위 진화 속도가 빠름 (v2 → v3 가 1 년). extras pin 을 `fastmcp>=3,<4` 로 유지하고 major 업그레이드 시 별도 평가 +- **공식 SDK 가 v3 기능을 재흡수** — Anthropic 이 OAuth / OTel 등을 공식 SDK 로 끌어올 가능성. 현시점에는 분업 구조가 안정 — 실현 시 재평가 +- **fastmcp 의 protocol drift** — 공식 SDK 와 fastmcp 가 spec 갱신 타이밍이 어긋나는 일시적 구간 가능. fastmcp 가 MCP spec 의 1st-tier consumer 라 큰 drift 는 발생하지 않을 것으로 예상 ### 출처 -- MCP Python SDK: +- standalone fastmcp (jlowin): +- fastmcp v3 출시 (2026-02): +- MCP Python SDK (공식, FastMCP v1 흡수): - MCP Streamable HTTP transport (2025-03): -- 본 프로젝트 검증 (context7 query): SDK v1.12.4 가 stdio / sse / streamable-http 3종 transport 지원 확인 +- 시장 점유율 / 일 다운로드 데이터 (2026-05): [FastMCP — Mostly Harmless](https://jlowin.dev/blog/fastmcp-3) 및 PyPI --- @@ -174,7 +177,7 @@ PyO3 `unsendable` = **객체가 생성된 thread 외에서 접근 시 runtime pa ## 참조 -- 짝 페어: [mcp.md](../../roadmap/v0.7.0/mcp.md) +- 짝 페어: [mcp.md](../../roadmap/v0.5.0/mcp.md) - MCP 공식: - MCP Python SDK: - 본 프로젝트 [CLAUDE.md](../../../CLAUDE.md) § Rust + Python 하이브리드 빌드 diff --git a/docs/implementation/spec-system-overhaul.md b/docs/implementation/spec-system-overhaul.md index 9d7a93d..7eae56f 100644 --- a/docs/implementation/spec-system-overhaul.md +++ b/docs/implementation/spec-system-overhaul.md @@ -73,7 +73,7 @@ last_updated: YYYY-MM-DD # 자동 갱신 (D3) |---|---|---| | **Living** | frontmatter **없음** (정의상 항상 최신) | [docs/CONVENTIONS.md](../CONVENTIONS.md), [docs/roadmap/README.md](../roadmap/README.md) | | **Active** | `status: Active`, ga/target 둘 다 생략 | phase-3.md, phase-4.md (이후 폐기 — [roadmap/README.md](../roadmap/README.md) § 미착수 작업 계획 으로 흡수), [docs/upstream/issue-find-control-text-positions.md](../upstream/issue-find-control-text-positions.md) | -| **Draft** | `status: Draft`, `target: vX.Y.Z` 필수 | [v0.7.0/mcp.md](../roadmap/v0.7.0/mcp.md) | +| **Draft** | `status: Draft`, `target: vX.Y.Z` 필수 | [v0.5.0/mcp.md](../roadmap/v0.5.0/mcp.md) (작성 시점 v0.7.0 target, 이후 pull-forward) | | **Frozen** | `status: Frozen`, `ga: vX.Y.Z` 필수 | 나머지 17개 | | **Superseded** | `status: Superseded`, `superseded_by` 필수, ga 보존 | (현재 0건) | diff --git a/docs/implementation/v0.5.0/stages/stage-1.md b/docs/implementation/v0.5.0/stages/stage-1.md new file mode 100644 index 0000000..40d2ff5 --- /dev/null +++ b/docs/implementation/v0.5.0/stages/stage-1.md @@ -0,0 +1,144 @@ +--- +status: Frozen +description: "v0.5.0 S1 작업 로그 — rhwp.mcp 패키지 + FastMCP 서버 스켈레톤 + 4 도구 (parse_hwp_summary / extract_text / get_ir / iter_blocks) + ADR § 1 SDK 결정 갱신 (공식 mcp SDK → standalone fastmcp v3)" +ga: v0.5.0 +last_updated: 2026-05-06 +--- + +# Stage S1 — MCP 서버 스켈레톤 (완료) + +**작업일**: 2026-05-06 +**계획 문서**: [roadmap/v0.5.0/mcp.md](../../../roadmap/v0.5.0/mcp.md) §구현 스테이지 분할 +**설계 근거**: [design/v0.5.0/mcp-research.md](../../../design/v0.5.0/mcp-research.md) + +## 스코프 + +mcp.md §구현 스테이지 분할 S1 행 정확 매핑: + +- `python/rhwp/mcp/` 패키지 신설 (`__init__.py` / `__main__.py` / `server.py` / `tools.py`) +- FastMCP 인스턴스 + 4 도구 등록 (`parse_hwp_summary` / `extract_text` / `get_ir` / `iter_blocks`) +- stdio transport 만 노출 (streamable-http 는 S4 의 영역) +- `[project.optional-dependencies] mcp` / `mcp-chunks` extras + `[project.scripts] rhwp-mcp = "rhwp.mcp:run"` entry point 등록 +- `tests/test_mcp_server.py` 신규 — module-level `pytest.importorskip("fastmcp")` 게이트 +- CI `test-without-extras` job: 4 → 5 skip 수 bump (gated 파일 5 개) + +S2 (`to_markdown` / `to_html`), S3 (`chunks`), S4 (streamable-http transport), S5 (문서화·검증) 는 본 스테이지 범위 밖. + +## S1 진행 중 spec 결정 변경 + +**ADR § 1 SDK 선택을 in-place 갱신** ([mcp-research.md § 1](../../../design/v0.5.0/mcp-research.md#1-sdk-선택)). spec Draft 라 CONVENTIONS § Frozen 정책 미적용 — Draft 는 자유 갱신 가능. + +| 항목 | 갱신 전 | 갱신 후 | +|---|---|---| +| SDK | 공식 `mcp` SDK (FastMCP v1 흡수) | standalone `fastmcp` v3 (jlowin) | +| extras 의존성 | `mcp>=1.12,<2` | `fastmcp>=3,<4` | +| 근거 | "1st-party 유지·spec 추종 보장" | "2026-05 현업 표준 — MCP 서버 약 70% 사용 + v3 의 OAuth / OpenTelemetry / server composition / OpenAPI 통합 / streamable-http 우선" | + +**갱신 근거**: + +- 공식 `mcp` SDK 안의 FastMCP 는 v1 만 흡수 (2024) — frozen 상태. 추가 framework 기능은 standalone (v2 → v3) 으로 분기 진화 +- 2026-02 출시된 fastmcp v3 는 OAuth / OpenTelemetry tracing / server composition / OpenAPI 자동 변환 / streamable-http 우선 등 프로덕션 기능 보유 — v0.5.0 S4 (streamable-http 도입) 및 미래 인증 시나리오 (mcp.md §비목표 의 "v0.8.0+ 재평가") 에 직접 영향 +- standalone 패키지가 일 100만 다운로드 / 시장 점유율 약 70% — 도구 호환성 / 문서 / 커뮤니티 ecosystem 의 분모가 더 큼 +- 마이그레이션 비용: import 1개, decorator 호출 형태 1개, exception 클래스 분기 (validation → `pydantic.ValidationError`, runtime → `fastmcp.exceptions.ToolError`, unknown → `NotFoundError`), input schema attribute (`inputSchema` → `parameters`) 만 차이 + +## 산출물 + +| 파일 | 변동 | 내용 | +|---|---|---| +| `python/rhwp/mcp/__init__.py` | +32 (신규) | `run()` entry point dispatch — `[mcp]` extras 미설치 시 친절 에러 + exit 2. `rhwp.cli.app()` 와 동일 패턴 (CLI 와 같은 위계) | +| `python/rhwp/mcp/__main__.py` | +6 (신규) | `python -m rhwp.mcp` 폴백 | +| `python/rhwp/mcp/server.py` | +40 (신규) | `build_server()` factory + `run()` (stdio). `from fastmcp import FastMCP`. 도구 등록은 `server.tool(fn)` (decorator 호출 형태) | +| `python/rhwp/mcp/tools.py` | +110 (신규) | 4 sync 도구 함수 본체 + `ParseSummary` Pydantic 모델 + `BlockKind` / `BlockScope` Literal. `fastmcp` import 없음 — 도구는 단독으로 단위 테스트 가능 | +| `tests/test_mcp_server.py` | +258 (신규) | 18 테스트 — 5 클래스 (`TestToolRegistry` / `TestSyncHandler` / 4 smoke / `TestErrorHandling` / `TestPackagingSurface`). file-level `importorskip("fastmcp")` 로 1 skip 기여 | +| `pyproject.toml` | +15 / -0 | `[project.optional-dependencies]` `mcp = ["fastmcp>=3,<4"]` + `mcp-chunks` 추가, `[project.scripts] rhwp-mcp` 추가, `[dependency-groups] testing` 에 `fastmcp>=3,<4` | +| `.github/workflows/ci.yml` | +5 / -3 | `test-without-extras` skip count 4 → 5, pyright list 에 `tests/test_mcp_server.py` 추가 | +| `CLAUDE.md` (= AGENTS.md) | +1 / -1 | gated 파일 카운트 4 → 5 (test_mcp_server.py 추가) | +| `docs/design/v0.5.0/mcp-research.md` | +21 / -16 | § 1 SDK 결정 매트릭스 + 본문 갱신 (공식 mcp SDK → standalone fastmcp v3) | +| `docs/roadmap/v0.5.0/mcp.md` | +12 / -10 | § 의존성 / 배포 / § 결정 사항 row 1 + 6 / AC-1 / AC-9 / AC-11 갱신 | +| `scripts/_doc_lint.py` | +18 / -8 | `is_pre_ga_stage` 면제 분기 — `Frozen + target` 을 `docs/implementation/vX.Y.Z/` 경로에 한해 허용 (CONVENTIONS § 131 정합, S1 § docs-lint policy 갱신 참조) | +| `docs/CONVENTIONS.md` | +2 / -2 | § 필드 schema 의 `ga` / `target` 행에 pre-GA stage 예외 명시 | + +## S1 확정 결정 사항 + +| 결정 | 선택 | 근거 | +|---|---|---| +| **SDK 패키지** | `fastmcp>=3,<4` (standalone, jlowin) | ADR § 1 갱신 — 2026-05 현업 표준 + v3 의 프로덕션 기능. 공식 mcp SDK 의 FastMCP v1 은 frozen | +| **`__init__.py` lazy-import 패턴** | `rhwp.cli.app()` 와 동일 패턴 — stdlib `sys` 만 모듈 레벨, `fastmcp` import 는 `run()` 안에서 lazy | CLAUDE.md § Module Structure "`__init__.py` MUST be empty or contain only docstrings" 의 spirit (heavy import 금지) 준수 + entry point 요구 (`rhwp-mcp = "rhwp.mcp:run"`) 양립. CLI 가 검증된 선례 | +| **도구 분리: `tools.py` vs `server.py`** | `tools.py` 는 ``fastmcp`` import 없는 순수 함수. `server.py` 가 `server.tool(fn)` 으로 등록 | 도구 본체를 단위 테스트 시 SDK 무관하게 호출 가능. fastmcp 가 v3 → v4 로 변동해도 `tools.py` 본체는 영향 없음 | +| **`build_server()` factory 분리** | 모듈 레벨 `mcp` 싱글턴 대신 함수 호출로 새 인스턴스 생성 | 테스트가 격리된 instance 로 `list_tools()` / `call_tool()` in-process 호출 가능 — 모듈 import 부수 효과 회피 | +| **`BlockKind` 의 `"all"` sentinel 제거** | `kind: BlockKind \| None = None` (None = 필터 미적용) | LLM JSON Schema enum 에 IR `Block.kind` 에 존재하지 않는 가짜 값 노출 회피. CLI 의 `BlockKindOpt.all` 과는 다른 surface — typer 는 default 가 enum 멤버여야 해서 sentinel 필요했지만 MCP 는 Optional 이 자연스러움 | +| **`server.tool(fn)` (no parens)** | fastmcp v3 권장 형태 | `@mcp.tool` decorator (no parens) 와 같은 신호. v3 가 `server.tool()(fn)` 도 backwards compat 으로 지원하나 v3-native 형태 채택 | +| **에러 surface 검증을 `pytest.raises(...)` 만으로 한정** | 메시지 텍스트 검사 생략 | OS / 로케일 의존성 회피 (Windows CI 매트릭스 매치 불가). AC-3 / AC-4 의 invariant 는 "panic 아님" + "MCP isError=True 응답" — 예외 raise 자체가 in-process 신호. 메시지 검사는 brittle | +| **AC-5 sync 검증 — 등록 시점 walk** | `server.list_tools()` 의 `FunctionTool` 인스턴스 walk + `inspect.iscoroutinefunction(tool.fn)` | 4 함수 하드코딩 대신 등록 도구 전체 자동 커버. S2 (to_markdown/to_html), S3 (chunks) 추가 시에도 동일 invariant 자동 보장 | +| **fastmcp 의존성 ceiling `<4`** | major 단위 변동 흡수 | jlowin 의 v 단위 진화 속도 (v2 → v3 가 1 년) 를 고려해 보수적 ceiling. v4 출시 시점 별도 평가 | +| **ImportError 분류** | `e.name` 이 `rhwp.*` / `rhwp` 시작이면 raise (rhwp 자체 결함), 그 외는 친절 에러 + exit 2 | rhwp 자체 모듈 결함 (예: `_rhwp` 빌드 누락) 의 진단 단서 보존. `rhwp.cli.app()` 와 동일 분기 — fastmcp 든 transitive deps (pydantic-settings / starlette) 든 같은 메시지 | + +## 비타협 제약 준수 + +- **`unsendable` 안전 패턴** — 4 도구 모두 sync 함수, handler 안에서 `rhwp.parse(path)` → 소비 → primitive 반환. `asyncio.to_thread(rhwp.parse, ...)` 패턴 코드 내 부재 (AC-5) +- **Pydantic V2 + `BaseModel`** — `ParseSummary` 가 dataclass 가 아닌 `BaseModel`. `Field(description=...)` 만 사용 (`ge=`/`le=`/`gt=`/`lt=` 부재) +- **`Literal["..."]` enum** — `BlockKind` / `BlockScope` 가 `str` 이 아닌 `Literal` — JSON Schema enum 으로 정확히 출고 (LLM token-level 제약) +- **`from __future__ import annotations` 부재** — Pydantic 런타임 타입 해석 호환 +- **`__init__.py` 가 module-level 에서 third-party import 안 함** — `import sys` (stdlib) 만, `fastmcp` 는 함수 안에서만 lazy +- **Python 3.10+ 유니온 표기** (`T | None`) — `Optional[T]` 회피 +- **모듈 위치** `python/rhwp/mcp/` (top-level) — `integrations/` 가 아님 (결정 7) + +## 검증 + +| 검사 | 결과 | +|---|---| +| `uv run pytest tests/ -m "not slow"` | **548 passed, 2 skipped** (v0.4.0 의 530 + S1 신규 18) | +| `uv run pytest tests/test_mcp_server.py -v` | **18 passed** (4 도구 × 평균 2 테스트 + 5 etc.) | +| `uv run ruff check python/rhwp/mcp/ tests/test_mcp_server.py` | clean | +| `uv run pyright python/rhwp/mcp/ tests/test_mcp_server.py` | **0 errors** | +| `uv run pyright tests/type_check_errors.py` | **4 intentional errors** (CI 검증 통과) | +| `cargo clippy --all-targets -- -D warnings` | clean (Rust 미수정) | +| `code-reviewer` fresh-context 검증 | HIGH 2 / MEDIUM 4 / LOW 3 — HIGH 둘 다 반영 (BlockKind sentinel 제거 + sync handler walk), MEDIUM 4 중 3 반영 (AC-4 메시지 검사 제거 / `mcp` ceiling 추가 / `test_init_is_lightweight` 삭제) | + +### 테스트 커버리지 (mcp.md §S1 → AC 매핑) + +| mcp.md AC | 테스트 | +|---|---| +| AC-1 (extras gate, exit 2) | CI `test-without-extras` job (skip count 5 검증, behavior SSOT) | +| AC-2 (도구 4개 노출) | `TestToolRegistry::test_lists_exactly_four_tools`, `test_iter_blocks_kind_schema_is_enum` (BlockKind enum 정확 매칭) | +| AC-3 (잘못된 enum → isError=True) | `TestErrorHandling::test_iter_blocks_invalid_kind` (`pytest.raises(ValidationError)`) | +| AC-4 (FileNotFound → isError=True) | `TestErrorHandling::test_extract_text_missing_file` (`pytest.raises(ToolError)`) | +| AC-5 (모든 handler sync) | `TestSyncHandler::test_all_registered_tools_are_sync` (등록 도구 walk + `iscoroutinefunction`) | +| AC-9 (pyproject 등록) | `TestPackagingSurface::test_pyproject_declares_fastmcp_extras_and_script` (extras + script tomllib 검증) | +| AC-10 (모듈 위치 top-level) | `TestPackagingSurface::test_module_is_top_level_not_under_integrations`, `test_entry_point_dispatches_to_run` | + +S2 / S3 영역의 AC-6 (view 도구) / AC-7 (chunks 도구) / AC-8 (streamable-http) / AC-11 (skip count 5 — CI 측 검증) 는 본 stage 범위 밖. + +## 알려진 한계 (S2 이후 처리) + +- **`get_ir` 응답 크기** — mcp.md §미확정 이슈. 큰 문서는 IR JSON 이 수 MB 수준이라 MCP `tools/call` 응답 한도 (클라이언트 별 상이) 와 충돌 가능. S5 손 검증 시점에 `--max-bytes` 또는 `Resource` 추상 도입 평가 +- **에러 응답 형식 통일** — mcp.md §미확정 이슈. fastmcp v3 가 ValidationError / ToolError / NotFoundError 셋으로 분기 — 통일 정책 (예: 모두 ToolError 로 wrap) 검토는 S2 이후 (도구 surface 가 늘어난 뒤 패턴 정립) +- **AC-1 의 in-process 검증 부재** — 현재 CI `test-without-extras` job 의 skip count 만이 SSOT. `subprocess` 로 fastmcp 차단 환경 시뮬레이션은 가능하나 비용 대비 가치 낮음. `code-reviewer` 가 권고했으나 S5 손 검증 + 실제 사용자 환경 (Claude Desktop 설정 가이드) 검증으로 대체 + +## docs-lint policy 갱신 (Living-policy migration, S1 부산물) + +본 stage 작성 도중 발견된 CONVENTIONS § 131 vs `scripts/_doc_lint.py` 충돌을 옵션 A 로 봉합 — Rust RFC / PEP / ADR 의 editorial vs release 차원 분리 패턴 정합. 변경: + +- `scripts/_doc_lint.py` — `is_pre_ga_stage` 면제 분기 신설. `docs/implementation/vX.Y.Z/` 경로 + `target` + `not has_ga` 시 `Frozen + target` 허용 +- `docs/CONVENTIONS.md` § 필드 schema — `ga` / `target` 행에 pre-GA stage 예외 명시 +- 본 파일이 첫 적용 사례 (`status: Frozen` + `target: v0.5.0`). v0.5.0 GA 시점에 일괄 `target` → `ga` 로 flip + +해당 변경은 stage 본문의 immutability 의미를 보존하면서 (Rust RFC 의 RFC text frozen on acceptance 패턴) GA 라벨 부여를 release-시점 administrative metadata 로 분리. + +## S2 진입 조건 (인수인계) + +S2 는 mcp.md § S2 row 의 view 도구 추가 — `to_markdown` / `to_html`. S1 에서 고정한 계약: + +1. **`tools.py` 의 sync-only 패턴** — S2 의 `to_markdown` / `to_html` 도 같은 형태. `HwpDocument.to_markdown()` / `to_html(include_css=...)` 직접 호출 → str 반환 +2. **`server.py` 의 `build_server()` factory** — 4 → 6 도구로 늘릴 때 `server.tool(tools.to_markdown)` / `server.tool(tools.to_html)` 추가만 +3. **`TestSyncHandler::test_all_registered_tools_are_sync`** — 등록 도구 walk 패턴이 자동 커버 (4 → 6 함수 변경 없음) +4. **`TestToolRegistry::test_lists_exactly_four_tools`** — S2 시점에 4 → 6 으로 카운트 갱신 + 새 도구 이름 set 에 추가. 동일 함수명 변경 (`test_lists_exactly_six_tools`) 검토 +5. **mcp.md AC-2 의 도구 카운트** — S1 시점은 "4 개 노출", S2 종료 시 "6 개", S3 종료 시 "7 개". stage 마다 mcp.md AC-2 본문은 그대로 두고 (spec 은 GA 기준 = 7 개) impl-log 에서만 S1/S2/S3 별 진행 카운트를 기록 + +## 참조 + +- 상위 설계: [roadmap/v0.5.0/mcp.md](../../../roadmap/v0.5.0/mcp.md) +- 결정 사항 증거 (S1 진행 중 갱신): [design/v0.5.0/mcp-research.md](../../../design/v0.5.0/mcp-research.md) +- 외부 참조: [jlowin/fastmcp](https://github.com/jlowin/fastmcp), [공식 mcp SDK](https://github.com/modelcontextprotocol/python-sdk), [MCP spec](https://modelcontextprotocol.io/) +- v0.4.0 선례 (Frozen 패턴): [implementation/v0.4.0/migration.md](../../v0.4.0/migration.md) +- 비동기 안전 패턴 배경: 프로젝트 [CLAUDE.md § Rust + Python 하이브리드 빌드](../../../../CLAUDE.md) diff --git a/docs/implementation/v0.5.0/stages/stage-2.md b/docs/implementation/v0.5.0/stages/stage-2.md new file mode 100644 index 0000000..7f509a8 --- /dev/null +++ b/docs/implementation/v0.5.0/stages/stage-2.md @@ -0,0 +1,94 @@ +--- +status: Frozen +description: "v0.5.0 S2 작업 로그 — view 도구 추가 (to_markdown / to_html). v0.4.0 view 렌더러 위 thin wrapper, 도구 카운트 4 → 6" +ga: v0.5.0 +last_updated: 2026-05-06 +--- + +# Stage S2 — view 도구 추가 (완료) + +**작업일**: 2026-05-06 +**계획 문서**: [roadmap/v0.5.0/mcp.md](../../../roadmap/v0.5.0/mcp.md) §구현 스테이지 분할 S2 +**선행 stage**: [stage-1.md](stage-1.md) (서버 스켈레톤 + 코어 4 도구) + +## 스코프 + +mcp.md §구현 스테이지 분할 S2 행 정확 매핑: + +- `python/rhwp/mcp/tools.py` 에 `to_markdown(path)` / `to_html(path, *, include_css=False)` 추가 +- `python/rhwp/mcp/server.py` `build_server()` 에 두 도구 등록 — 도구 카운트 4 → 6 +- `tests/test_mcp_server.py` 의 `test_lists_exactly_four_tools` → `test_lists_exactly_six_tools` 갱신 + `TestToMarkdown` / `TestToHtml` 클래스 추가 (AC-6 spec 매핑) +- v0.4.0 view 렌더러 (`HwpDocument.to_markdown()` / `to_html(*, include_css=...)`) 위 thin wrapper — 추가 변환 / sanitize / wrapping 없이 pass-through + +S3 (`chunks` extras gate), S4 (streamable-http transport), S5 (문서화·검증) 는 본 스테이지 범위 밖. + +## 산출물 + +| 파일 | 변동 | 내용 | +|---|---|---| +| `python/rhwp/mcp/tools.py` | +30 / -1 | `to_markdown(path) -> str` / `to_html(path, *, include_css=False) -> str` 추가. 모듈 docstring 의 stage 분할 주석 갱신 (S1 4 + S2 2). v0.4.0 view API 와 동일하게 `include_css` keyword-only 강제 | +| `python/rhwp/mcp/server.py` | +3 / -2 | `build_server()` 가 6 도구 등록 — `server.tool(tools.to_markdown)` / `server.tool(tools.to_html)` 추가. 주석 갱신 ("S1 코어 4 + S2 view 2") | +| `tests/test_mcp_server.py` | +44 / -5 | 모듈 docstring AC 매핑 갱신 (AC-6 추가), `test_lists_exactly_six_tools` 로 rename + 도구 set 6 개로 확장, `TestToMarkdown` (2 테스트) / `TestToHtml` (3 테스트) 신설 — 모두 AC-6 spec 매핑 | +| `docs/traces/coverage.md` | +5 | auto-regen — `v0.5.0/mcp#AC-6` 매핑 3 개 추가 (TestToMarkdown::test_matches_view_api, TestToHtml::test_matches_view_api_no_css, TestToHtml::test_matches_view_api_with_css) | + +## S2 확정 결정 사항 + +| 결정 | 선택 | 근거 | +|---|---|---| +| **`to_html` 의 `include_css` keyword-only 강제** | `def to_html(path: str, *, include_css: bool = False)` (positional 거부) | v0.4.0 `HwpDocument.to_html(*, include_css=False)` 의 invariant 와 동일. `include_css=True` 가 의미적으로 부울 플래그이고 호출처가 의도를 명시해야 한다는 view-layer 결정을 wrapper 가 침식하지 않게 함. JSON Schema 출고에는 영향 없음 (positional/keyword 구분 없음) — Python 호출 측면의 invariant 보존 | +| **"thin wrapper" 의미 — byte-equality 강제** | 도구 출력 == `HwpDocument.to_xxx(...)` 직접 호출 출력 (bytewise 동일) | AC-6 의 "thin wrapper" 를 "추가 변환 / sanitize 없음" 으로 해석. 테스트 (`TestToMarkdown::test_matches_view_api` / `TestToHtml::test_matches_view_api_no_css` / `..._with_css`) 가 byte-equality 검증. 이로써 향후 view API 가 진화해도 wrapper 가 "투명한 통과" 에서 벗어나면 자동 회귀 검출 | +| **docstring 에 view 동작 요약 복제** | tools.py 의 `to_markdown` / `to_html` docstring 이 GFM 표 / `