Skip to content

v0.5.1: MCP tool 출력 강타입화 (PATCH)#17

Merged
DanMeon merged 6 commits into
mainfrom
refactor/mcp-typed-output
May 7, 2026
Merged

v0.5.1: MCP tool 출력 강타입화 (PATCH)#17
DanMeon merged 6 commits into
mainfrom
refactor/mcp-typed-output

Conversation

@DanMeon
Copy link
Copy Markdown
Owner

@DanMeon DanMeon commented May 7, 2026

Summary

v0.5.0 GA 한 rhwp-mcp 의 7 도구 중 약타입 (dict[str, Any] / list[dict[str, Any]]) 으로 반환하던 3 도구 (get_ir / iter_blocks / chunks) 의 출력을 Pydantic V2 모델 (HwpDocument / list[Block] / list[ChunkRecord]) 로 강타입화. fastmcp v3 의 자동 outputSchema 가 약타입 (additionalProperties: true 만) → 강타입 (필드별 type / Discriminator + Tag 11 변형 / required 명시) 으로 강화 — LLM 응답 정확도 향상. wire format byte-equal — 외부 클라이언트 (Claude Desktop / Cline 등) 영향 0.

변경 요약

영역 변경
python/rhwp/mcp/tools.py 3 함수 시그니처 강화 + ChunkRecord 신규 BaseModel
python/rhwp/ir/nodes.py UnknownBlock.kind JSON Schema not.enum constraint — fastmcp strict oneOf 호환. 런타임 동작 0 변경, schema export 만 strict 화
python/rhwp/ir/schema/hwp_ir_v1.json packaged JSON Schema 자동 재생성
tests/test_mcp_server.py 5 신규 테스트 클래스 (AC-1~AC-7) — typed signatures / outputSchema 강화 / wire format byte-equal / typed result.data / Pydantic round-trip
Cargo.toml + CHANGELOG.md 0.5.0 → 0.5.1 release marker + [0.5.1] 섹션
spec / ADR / migration 모두 Frozen + GA 인덱스 갱신
README.md MCP 도구 표 출력 컬럼 + v0.5.1 마이그 노트

작업 중 표면화된 fastmcp v3 한계 (spec 본문 정확화)

  1. result.structured_content wrap 분기 — BaseModel = inline (no wrap) / list[T] = {"result": [...]} envelope. spec § wire format byte-equal 의 wrap 분기 표 추가.
  2. callable Discriminator + Tag 유니온의 client-side deserialization 한계 — fastmcp Client 의 자동 reconstruct 가 oneOf 11 변형을 dynamic 모델로 변환 못 해 list element dict 폴백. server side 의 typed 출력은 AC-2 가 cover, wire format byte-equal 은 AC-5 가 cover. v0.5.0 dict access 패턴 그대로 backwards-compat.
  3. strict oneOf validation 충돌 — ParagraphBlock + UnknownBlock 양쪽 valid 인스턴스에서 RuntimeError: Invalid structured content. 결정 8 (UnknownBlock.kind not.enum) 추가로 해결.

Why

v0.5.0 S3 의 code-reviewer 가 LOW-2 로 권고: "iter_blocks / get_ir / chunks 가 같은 패턴 (list[dict[str, Any]]) 이라 일괄 후속 polish 처리". v0.5.0 GA (2026-05-06) 직후 후속 polish 시점이 v0.5.1 PATCH. outputSchema 본문이 48 bytes (additionalProperties: true only) → 약 28-32 KB (Discriminator + Tag 11 변형, $defs 약 20 종) 로 확장 — LLM 의 응답 해석 / 후속 도구 호출 정확도에 측정 가능한 개선.

Related Issues

없음 — code-reviewer LOW-2 후속 polish.


검증

  • uv run pytest tests/ -m "not slow"585 passed, 2 skipped (pre-existing aift fixture), 6 deselected
  • uv run pyright python/rhwp/ tests/test_mcp_server.py — 0 errors (변경 파일) + 4 intentional tests/type_check_errors.py 보존
  • uv run ruff check (변경 파일) — clean
  • uv run python scripts/lint_docs.py — exit 0
  • uv run python scripts/generate_spec_trace.py --check — up to date (16 v0.5.1 mappings)
  • code-reviewer fresh-context (sub-agent) — PASS, HIGH 0 / MEDIUM 4 / LOW 2. MEDIUM-1 (측정값 stale) + MEDIUM-3 (lib name in new comments) 처리 완료. MEDIUM-2 (Cargo / CHANGELOG) 는 본 PR 의 release marker commit 으로 처리. MEDIUM-4 / LOW-1 / LOW-2 는 pre-existing 또는 spec design intent.

Spec / ADR / Migration

Commit history

d3a39f5 docs: v0.5.1 GA — Frozen 전환 + roadmap 인덱스 갱신
0272e0b chore: v0.5.1 release marker
d9f61a1 docs(v0.5.1): mcp-typed-output spec / ADR / migration
747d6b2 test(mcp): v0.5.1 typed-output AC-1~AC-7 회귀 가드
28774a2 refactor(mcp): get_ir / iter_blocks / chunks 출력 강타입화
03a399d fix(ir): UnknownBlock.kind not.enum for fastmcp strict oneOf 호환

머지 후 GitHub Release v0.5.1 published 로 publish.yml 트리거 (Trusted Publisher OIDC → PyPI 자동 배포).

DanMeon and others added 6 commits May 7, 2026 13:47
callable Discriminator + Tag 유니온의 자동 schema 가 oneOf 11 변형으로
펼쳐지는데, UnknownBlock 의 `kind: str` + `extra: allow` 가 known kind
인스턴스 (예: 빈 ParagraphBlock) 와도 매칭 → jsonschema strict oneOf
exactly-one 위반 → fastmcp Client 가 RuntimeError. 결과적으로 v0.5.1
typed output 의 wire format 호환이 client side 에서 깨짐.

Field(json_schema_extra=callable) 로 schema 에 not.enum: sorted(_KNOWN_KINDS)
삽입. callable 분리는 _KNOWN_KINDS 가 모듈 정의 순서상 뒤에 위치하는
NameError 회피. 런타임 동작 0 변경 — model_validate / round-trip /
forward-compat 라우팅 모두 보존, schema export 만 strict 화.

packaged hwp_ir_v1.json 도 자동 재생성.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dict[str, Any] / list[dict[str, Any]] 반환을 Pydantic V2 모델로 전환:

- get_ir(path) -> HwpDocument
- iter_blocks(path, ...) -> list[Block]
- chunks(path, ...) -> list[ChunkRecord]

ChunkRecord 신규 BaseModel — page_content: str + metadata: dict[str, Any].
metadata 는 mode × block kind 동적 키 집합 (HwpLoader SSOT) 이라 자유 dict
유지 — mode 별 분기 모델 (3-11 배 schema 비대) 거부.

호출 시그니처 / 입력 schema / wire format (result.structured_content) 모두
v0.5.0 과 byte-equal — fastmcp 자동 직렬화에 위임 (model_dump(mode="json")
수동 호출 제거). outputSchema 만 약타입 → 강타입으로 강화 (LLM 응답 정확도).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 신규 테스트 클래스 + 기존 dict access 검증 typed model 전환:

- TestTypedSignatures (AC-1, AC-2, AC-3, AC-7) — 정적 반환 어노테이션 +
  ChunkRecord.metadata 자유 dict
- TestTypedOutputSchema (AC-4) — fastmcp 자동 outputSchema 가 HwpDocument
  defs / Block oneOf 11 변형 / ChunkRecord page_content+metadata 노출
- TestBackwardsCompat (AC-5) — fastmcp Client in-process 의 byte-equal
  회귀 가드. wrap 분기 정확화: HwpDocument = inline / list[T] = {"result":...}
- TestTypedClientData (AC-6) — result.data 의 typed-or-dict 분기. fastmcp v3
  의 oneOf deserialization 한계로 iter_blocks list element 는 dict 폴백,
  v0.5.0 access 패턴 그대로 동작
- TestTypedModelRoundTrip — Pydantic dump → load → equality

기존 TestGetIr / TestIterBlocks / TestChunks 의 dict 인덱싱 검증을
typed attribute access 로 교체.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- roadmap/v0.5.1/mcp-typed-output.md (Draft) — 결정 8 항목 + AC-1~AC-10.
  결정 8 (UnknownBlock.kind not.enum), § wire format byte-equal 의 wrap
  분기 표 (BaseModel = inline / list[T] = {"result": ...}), AC-5/AC-6 의
  fastmcp v3 한계 반영
- design/v0.5.1/mcp-typed-output-research.md (Draft) — 결정 매트릭스 row 5
  + § 5 (UnknownBlock not.enum 옵션 비교 + 검증자 반박 + 1차 소스). § 2
  검증자 반박에 fastmcp client deserialization 한계 추가
- implementation/v0.5.1/migration.md (Frozen) — 본 PATCH 의 산출물 / 결정
  매핑 / 호환성 / 검증 / AC↔테스트 매핑 / fastmcp v3 한계 / GA 절차 인계
- README.md MCP 도구 표 출력 컬럼 갱신 + v0.5.1 마이그 노트
- roadmap/README.md v0.5.1 row Draft 인덱스 등록
- traces/coverage.md 16 v0.5.1 AC mappings 자동 갱신

Cargo.toml bump / CHANGELOG [0.5.1] / spec Draft→Frozen flip / git tag 는
별도 GA 절차 step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cargo.toml version bump 0.5.0 → 0.5.1 (pyproject.toml 의 dynamic = ["version"]
가 본 SSOT 를 읽음 — publish.yml::verify-version 이 git tag 와 일치 가드).

CHANGELOG.md [0.5.1] — 2026-05-07 섹션 신설:
- Added: ChunkRecord BaseModel (page_content + metadata: dict[str, Any])
- Changed: get_ir / iter_blocks / chunks 출력 강타입화, README 도구 표 + 마이그
- Fixed: UnknownBlock.kind 의 JSON Schema not.enum constraint (fastmcp strict
  oneOf 호환)
- Build: submodule pin 62a458a (v0.7.10) 유지, extras / 의존성 변경 없음
- Notes: spec / ADR / migration 위치, fastmcp v3 한계 2 가지 언급

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontmatter flip:
- roadmap/v0.5.1/mcp-typed-output.md: status: Draft → Frozen, target → ga
- design/v0.5.1/mcp-typed-output-research.md: 동일
- implementation/v0.5.1/migration.md: target → ga (status 는 이미 Frozen — pre-GA stage 면제 그대로 적용)

roadmap/README.md:
- 현재 상태 v0.5.1 row: Draft → Frozen GA (2026-05-07)
- 활성 spec 인덱스 row: Draft → Frozen
- v0.6.0+ narrative + quoted note 의 v0.5.1 표기: Draft → Frozen
- 구현/검증 로그 표에 v0.5.1 → implementation/v0.5.1/migration.md row 추가

Living-policy schema migration — CONVENTIONS § Frozen 면제 조항 정합 (GA
시점 frontmatter flip 은 허용된 in-place 갱신).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DanMeon DanMeon merged commit 0fd49fc into main May 7, 2026
17 checks passed
@DanMeon DanMeon deleted the refactor/mcp-typed-output branch May 7, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant