Skip to content

Commit 1048ce6

Browse files
authored
Merge pull request #21 from DanMeon/chore/publish-rollup
v0.6.1: v0.6.0 publish 누락 회복 + 후속 polish 통합
2 parents 8b45fde + 4083a27 commit 1048ce6

9 files changed

Lines changed: 262 additions & 54 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ jobs:
154154
strategy:
155155
fail-fast: false
156156
matrix:
157-
os: [macos-latest, windows-latest]
157+
# ^ macos-latest 는 상류 edwardkim/rhwp#823 (headless macOS 에서 PNG 렌더
158+
# hang — CoreText downloadable lookup IPC 영구 대기) 해결 시 복귀.
159+
# 현재 GHA macos runner 에서 30분+ hang 으로 wheel 검증이 불가.
160+
os: [windows-latest]
158161
defaults:
159162
run:
160163
shell: bash

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.1] — 2026-05-18
11+
12+
PATCH release. v0.6.0 (Frozen, 2026-05-10) 의 GitHub Release / PyPI publish 가 누락된 상태에서 발견된 release 인프라 정합화 + 후속 polish 를 한 묶음 PATCH 로 발행한다. 사용자 영향: PyPI 첫 게시 패키지가 `v0.5.1` 다음 `v0.6.1` 로 점프 — v0.6.0 의 모든 표면 (페이지 PNG 렌더링 + 문서 시스템 개편) 은 변경 없이 그대로 포함하며 `[0.6.0]` 섹션은 historical record 로 보존. 외부 공개 API / IR schema (`"1.1"`) 변경 0.
13+
14+
### Added
15+
16+
- `examples/07_render_png.py` 신규 — v0.6.0 PNG 표면의 typer 진입점 예제 (단일 페이지 / `--all` 일괄 / `--scale` / `--max-pixels` / `--output-dir` / `--prefix`). Pillow 가 있으면 디코드 dimension 까지 검증, 없으면 PNG magic + 길이만 출력 (graceful degrade). README §"페이지 PNG 렌더링 (VLM 입력)" 의 typer 진입점 시연.
17+
- `examples/README.md` §7 항목 + "일곱 스크립트" 안내. `[examples]` extras 에 Pillow 추가 (07 디코드 검증용).
18+
- `benches/bench_gil.py``png_task` 추가 — `parse + render_png(page=0)` 의 ThreadPoolExecutor worker 1/2/4/8 별 wall-clock 비교 (기존 `parse + render_pdf` 패턴 동형). `--json` 플래그 출고 옵션 — drift 추적 / ADR 첨부 재활용. v0.6.0 spec row-6 의 "≥50 ms 임계 충족" rationale 을 closed-loop 으로 실측 검증.
19+
20+
### Changed
21+
22+
- `rhwp.parse` / `rhwp.aparse` / `rhwp.arender_png` / `Document.__init__``path: str``path: str | os.PathLike[str]` 수용. 내부에서 `str(path)` 정규화 후 Rust `_Document(&str)` 위임. 사용자가 `pathlib.Path` 인스턴스를 그대로 넘길 수 있다 — IDE 자동완성 정합. v0.5.x 의 `str` 호출 시그니처는 그대로 유지 (additive widening, breaking 아님).
23+
24+
### Build
25+
26+
- `external/rhwp` submodule pin `62a458a` (v0.7.10) → `1899ef9b` (v0.7.12). v0.6.0 Build 섹션 disclosure 의 v0.7.10 record 와 wheel binary 의 불일치를 해소. 본 binding 관점 변경 0 — 공개 API / IR schema (`"1.1"`) / wheel 의존성 모두 동일, `cargo check` clean + `maturin develop --release` clean + IR baseline byte-equal (`tests/test_view_baseline.py` 2/2 pass) + 회귀 가드 592 통과 직접 검증. 상류 v0.7.11 + v0.7.12 GA 흡수 — Renderer 시각 회귀 fix 다수 + Text IR v2 (`GlyphRun` / `GlyphOutline` variant, rhwp-python 미소비) + HWP3 ch=9 탭 spec 정합 + skia-safe `0.93.1``0.97.0` binary-cache. macOS PNG headless hang (상류 [#823](https://github.com/edwardkim/rhwp/issues/823)) 은 v0.7.12 에서도 미해결 — 별도 issue 진행.
27+
- `Cargo.toml``version` `0.6.0``0.6.1`. `pyproject.toml``dynamic = ["version"]` 으로 자동 추종.
28+
- `[project.optional-dependencies] examples``pillow>=10` 추가 — 07 예제의 dimension 디코드 검증 옵션. 미설치 시 graceful degrade (PNG magic + 길이만).
29+
30+
### Notes
31+
32+
- v0.6.0 publish 누락의 회복 경로 — `[0.6.0]` historical record 보존 + v0.6.1 = v0.6.0 표면 + 본 PATCH 변경. SemVer 측 단조 증가 (PyPI 는 게시되지 않은 v0.6.0 의 부재를 허용).
33+
- `tests/type_check_errors.py` 의 의도된 pyright 에러 4건 + `test-without-extras` job 의 expected skip count 6 변동 없음.
34+
1035
## [0.6.0] — 2026-05-10
1136

1237
MINOR release. 페이지 PNG 렌더링 표면을 추가하여 VLM (Vision-Language Model — Claude / GPT-4V / Gemini Vision 등) 의 시각 입력 시나리오를 지원한다. 상류 `rhwp` v0.7.10 (PR #599 PNG 게이트웨이) 의 `SkiaLayerRenderer::render_raster_with_options` 위 thin wrapper — `Document.render_png(page) -> bytes` / `render_all_png()` / `export_png(out_dir)` 3 메서드 + 모듈-level `arender_png(path, page)` async + MCP 도구 `render_page_png` (fastmcp `ImageContent` 출고) 신규. `[png]` extras 분리 없이 default wheel 통합 (Cargo `native-skia` feature 항상 활성화 — skia binary 약 30 MB 추가) — `pip install rhwp-python` 만으로 즉시 사용 가능. 추가만 있고 v0.5.x 의 SVG / PDF / IR / MCP 표면은 모두 보존 (additive only), schema (`"1.1"`) 유지.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rhwp-python"
3-
version = "0.6.0"
3+
version = "0.6.1"
44
edition = "2021"
55
# ^ rust-version 미명시 — 상위 rhwp crate 정책(stable Rust, MSRV unclaimed) 준수.
66
# PyO3 0.28 이 Rust 1.83+ 요구하지만, 이는 README 에 문서로 안내

benches/bench_gil.py

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
"""GIL 해제 효과 측정 — 단일 vs 멀티스레드 parse / render_pdf 처리 시간.
1+
"""GIL 해제 효과 측정 — 단일 vs 멀티스레드 parse / render_pdf / render_png 처리 시간.
22
3-
`py.detach` 를 적용한 `parse()` `render_pdf()` 가 `ThreadPoolExecutor` 에서
4-
실제 병렬 실행되는지 (GIL 해제 작동) 확인.
3+
`py.detach` 를 적용한 `parse()` / `render_pdf()` / `render_png()` 가
4+
`ThreadPoolExecutor` 에서 실제 병렬 실행되는지 (GIL 해제 작동) 확인.
55
66
`#[pyclass(unsendable)]` 제약: `Document` 객체는 생성된 스레드에서만 유효.
7-
벤치는 각 워커가 parse → 추출 / render_pdf → bytes 까지 완결 후 int 반환 (현업 패턴).
7+
벤치는 각 워커가 parse → 추출 / render_pdf → bytes / render_png → bytes 까지
8+
완결 후 int 반환 (현업 패턴).
9+
10+
옵션:
11+
--json 결과를 stdin 친화 JSON 으로 출력 (drift 추적 / ADR 첨부용)
812
"""
913

14+
import argparse
15+
import json
1016
import os
1117
import time
1218
from concurrent.futures import ThreadPoolExecutor
@@ -33,6 +39,13 @@ def pdf_task(path: str) -> int:
3339
return len(pdf)
3440

3541

42+
def png_task(path: str) -> int:
43+
# ^ parse + render_png(page=0) 를 한 워커에서 처리. bytes 길이만 반환
44+
doc = rhwp.parse(path)
45+
png = doc.render_png(0)
46+
return len(png)
47+
48+
3649
def bench(task, file_list: list[str], workers: int, repeats: int) -> float:
3750
times = []
3851
for _ in range(repeats):
@@ -47,49 +60,85 @@ def bench(task, file_list: list[str], workers: int, repeats: int) -> float:
4760
return min(times)
4861

4962

50-
def main() -> None:
51-
files = [
52-
str(SAMPLES / "aift.hwp"),
53-
str(SAMPLES / "table-vpos-01.hwpx"),
54-
str(SAMPLES / "tac-img-02.hwpx"),
55-
]
56-
parse_workload = files * 3 # ^ 9 태스크 (3 파일 × 3회 반복)
63+
def _run_section(task, file_list, worker_list, repeats):
64+
rows = []
65+
baseline = bench(task, file_list, workers=1, repeats=repeats)
66+
rows.append({"workers": 1, "seconds": baseline, "speedup": 1.0})
67+
for w in worker_list:
68+
t = bench(task, file_list, workers=w, repeats=repeats)
69+
rows.append({"workers": w, "seconds": t, "speedup": baseline / t})
70+
return rows
5771

58-
print(f"시스템 코어 수: {os.cpu_count()}")
59-
print(f"rhwp 버전: {rhwp.version()} / rhwp core: {rhwp.rhwp_core_version()}")
72+
73+
def _print_table(title: str, subtitle: str, rows: list[dict], task_count: int) -> None:
6074
print()
6175
print("=" * 72)
62-
print("Parse 벤치마크 — 9개 파일 (aift + table-vpos + tac-img, 각 3회)")
76+
print(title)
77+
print(subtitle)
6378
print("=" * 72)
6479
print(f"{'워커 수':<12} {'처리 시간':<15} {'단일 대비':<15} {'이상적 가속':<15}")
6580
print("-" * 72)
66-
67-
baseline = bench(parse_task, parse_workload, workers=1, repeats=3)
68-
print(f"{'1 (순차)':<12} {f'{baseline * 1000:.0f}ms':<15} {'1.00x':<15} {'1.00x':<15}")
69-
70-
for workers in [2, 4, 8]:
71-
t = bench(parse_task, parse_workload, workers=workers, repeats=3)
72-
speedup = baseline / t
73-
ideal = min(workers, len(parse_workload))
81+
for r in rows:
82+
ideal = min(r["workers"], task_count)
83+
label = "1 (순차)" if r["workers"] == 1 else str(r["workers"])
7484
print(
75-
f"{workers:<12} {f'{t * 1000:.0f}ms':<15} "
76-
f"{f'{speedup:.2f}x':<15} {f'{ideal:.0f}x (이상치)':<15}"
85+
f"{label:<12} {f'{r['seconds'] * 1000:.0f}ms':<15} "
86+
f"{f'{r['speedup']:.2f}x':<15} {f'{ideal:.0f}x (이상치)':<15}"
7787
)
7888

79-
print()
80-
print("=" * 72)
81-
print("PDF 렌더링 벤치마크 — 3개 문서 (parse + render_pdf 워커 내 완결)")
82-
print("=" * 72)
83-
print(f"{'워커 수':<12} {'처리 시간':<15} {'단일 대비':<15}")
84-
print("-" * 72)
8589

86-
pdf_baseline = bench(pdf_task, files, workers=1, repeats=2)
87-
print(f"{'1 (순차)':<12} {f'{pdf_baseline * 1000:.0f}ms':<15} {'1.00x':<15}")
90+
def main() -> None:
91+
parser = argparse.ArgumentParser(description=__doc__)
92+
parser.add_argument(
93+
"--json", action="store_true", help="결과를 JSON 으로 stdout 에 dump"
94+
)
95+
args = parser.parse_args()
8896

89-
for workers in [2, 3]:
90-
t = bench(pdf_task, files, workers=workers, repeats=2)
91-
speedup = pdf_baseline / t
92-
print(f"{workers:<12} {f'{t * 1000:.0f}ms':<15} {f'{speedup:.2f}x':<15}")
97+
files = [
98+
str(SAMPLES / "aift.hwp"),
99+
str(SAMPLES / "table-vpos-01.hwpx"),
100+
str(SAMPLES / "tac-img-02.hwpx"),
101+
]
102+
parse_workload = files * 3 # ^ 9 태스크 (3 파일 × 3회 반복)
103+
104+
parse_rows = _run_section(parse_task, parse_workload, [2, 4, 8], repeats=3)
105+
pdf_rows = _run_section(pdf_task, files, [2, 3], repeats=2)
106+
png_rows = _run_section(png_task, files, [2, 3], repeats=2)
107+
108+
if args.json:
109+
payload = {
110+
"system": {
111+
"cpu_count": os.cpu_count(),
112+
"rhwp_version": rhwp.version(),
113+
"rhwp_core_version": rhwp.rhwp_core_version(),
114+
},
115+
"parse": {"task_count": len(parse_workload), "rows": parse_rows},
116+
"pdf": {"task_count": len(files), "rows": pdf_rows},
117+
"png": {"task_count": len(files), "rows": png_rows},
118+
}
119+
print(json.dumps(payload, indent=2, ensure_ascii=False))
120+
return
121+
122+
print(f"시스템 코어 수: {os.cpu_count()}")
123+
print(f"rhwp 버전: {rhwp.version()} / rhwp core: {rhwp.rhwp_core_version()}")
124+
_print_table(
125+
"Parse 벤치마크 — 9개 파일 (aift + table-vpos + tac-img, 각 3회)",
126+
"",
127+
parse_rows,
128+
len(parse_workload),
129+
)
130+
_print_table(
131+
"PDF 렌더링 벤치마크 — 3개 문서 (parse + render_pdf 워커 내 완결)",
132+
"",
133+
pdf_rows,
134+
len(files),
135+
)
136+
_print_table(
137+
"PNG 렌더링 벤치마크 — 3개 문서 (parse + render_png(0) 워커 내 완결)",
138+
"",
139+
png_rows,
140+
len(files),
141+
)
93142

94143

95144
if __name__ == "__main__":

examples/07_render_png.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""HWP/HWPX 페이지를 PNG 로 렌더링하는 예제 (VLM 입력).
2+
3+
사용법:
4+
python examples/07_render_png.py path/to/file.hwp
5+
python examples/07_render_png.py path/to/file.hwp --page 2 --scale 2.0
6+
python examples/07_render_png.py path/to/file.hwp --all
7+
python examples/07_render_png.py path/to/file.hwp --all --scale 1.5 --output-dir ./out
8+
9+
설치:
10+
pip install "rhwp-python[examples]"
11+
"""
12+
13+
import io
14+
from pathlib import Path as PathLibPath
15+
16+
import rhwp
17+
import typer
18+
19+
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
20+
21+
22+
def _describe(data: bytes) -> str:
23+
size_kb = len(data) / 1024
24+
magic_ok = data.startswith(PNG_MAGIC)
25+
parts = [f"{size_kb:.1f} KB", "PNG magic OK" if magic_ok else "PNG magic FAIL"]
26+
try:
27+
from PIL import Image
28+
29+
img = Image.open(io.BytesIO(data))
30+
parts.append(f"{img.size[0]}×{img.size[1]}")
31+
except ImportError:
32+
parts.append("Pillow 미설치 (dimension 검증 생략)")
33+
return ", ".join(parts)
34+
35+
36+
def main(
37+
path: PathLibPath = typer.Argument(..., help="HWP 또는 HWPX 파일 경로"),
38+
page: int = typer.Option(0, "--page", "-p", help="0-based 페이지 인덱스 (단일 모드)"),
39+
all_pages: bool = typer.Option(False, "--all", help="전 페이지 일괄 렌더링 (--page 무시)"),
40+
scale: float = typer.Option(1.0, "--scale", "-s", help="픽셀 너비/높이 배율 (default 1.0)"),
41+
max_pixels: int | None = typer.Option(
42+
None, "--max-pixels", help="DoS 가드 픽셀 상한 (default 8192×8192)"
43+
),
44+
output_dir: PathLibPath = typer.Option(
45+
PathLibPath("./render_output"), "--output-dir", "-o", help="출력 디렉토리"
46+
),
47+
prefix: str = typer.Option("page", "--prefix", help="PNG 파일명 접두사"),
48+
) -> None:
49+
"""HWP/HWPX 를 파싱한 뒤 PNG 로 렌더링.
50+
51+
단일 페이지 (기본): `--page` 인덱스 한 장을 `{prefix}.png` 로 저장.
52+
전체 페이지 (`--all`): `{prefix}_{NNN}.png` 패턴으로 저장 (1-based 0-padded 3자리).
53+
`--scale` 또는 `--max-pixels` 가 기본값과 다르면 page 단위 루프로 처리 (export_png 가 두 인자 미수령).
54+
"""
55+
if not path.exists():
56+
typer.echo(f"파일이 없습니다: {path}", err=True)
57+
raise typer.Exit(code=1)
58+
59+
output_dir.mkdir(parents=True, exist_ok=True)
60+
61+
typer.echo(f"파싱 중: {path}")
62+
doc = rhwp.parse(str(path))
63+
typer.echo(f" 페이지 수: {doc.page_count}")
64+
65+
if all_pages:
66+
typer.echo(f"\n[PNG, all pages] {output_dir}/{prefix}*.png (scale={scale})")
67+
custom_opts = scale != 1.0 or max_pixels is not None
68+
if custom_opts:
69+
for p in range(doc.page_count):
70+
data = doc.render_png(p, scale=scale, max_pixels=max_pixels)
71+
if doc.page_count == 1:
72+
out = output_dir / f"{prefix}.png"
73+
else:
74+
out = output_dir / f"{prefix}_{p + 1:03d}.png"
75+
out.write_bytes(data)
76+
typer.echo(f" {out} ({_describe(data)})")
77+
else:
78+
paths = doc.export_png(str(output_dir), prefix=prefix)
79+
for p in paths:
80+
size_kb = PathLibPath(p).stat().st_size / 1024
81+
typer.echo(f" {p} ({size_kb:.1f} KB)")
82+
else:
83+
if page >= doc.page_count:
84+
typer.echo(f"--page {page} 가 페이지 수 {doc.page_count} 를 초과합니다.", err=True)
85+
raise typer.Exit(code=1)
86+
out = output_dir / f"{prefix}.png"
87+
typer.echo(f"\n[PNG, page {page}] {out} (scale={scale}, max_pixels={max_pixels})")
88+
data = doc.render_png(page, scale=scale, max_pixels=max_pixels)
89+
out.write_bytes(data)
90+
typer.echo(f" {_describe(data)}")
91+
92+
typer.echo(f"\n완료. 결과물: {output_dir}/")
93+
94+
95+
if __name__ == "__main__":
96+
typer.run(main)

examples/README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
## 사전 준비
77

88
```bash
9-
# 01 ~ 06 예제 전부 한 방에 설치 (typer + langchain-core + text-splitters + fastmcp)
9+
# 01 ~ 07 예제 전부 한 방에 설치 (typer + langchain-core + text-splitters + fastmcp + pillow)
1010
pip install "rhwp-python[examples]"
1111
```
1212

1313
> 06 의 `chunks` MCP 도구는 `langchain-text-splitters` 가 필요한데 위 한 줄에 포함됨.
14+
> 07 의 디코드 dimension 검증에는 Pillow 가 필요한데 위 한 줄에 포함됨 (미설치 시
15+
> PNG magic + 길이만 출력하는 graceful degrade).
1416
> 통합 레이어만 필요하면 (예제 러너 없이 직접 `HwpLoader` 사용) `pip install "rhwp-python[langchain]"` 만으로 충분하다.
1517
1618
## 스크립트
@@ -95,6 +97,30 @@ in-process round-trip — 7 도구 (`parse_hwp_summary` / `extract_text` / `get_
9597
옵션:
9698
- `--skip-chunks` : `[mcp-chunks]` extras 미설치 환경에서 chunks 호출 스킵
9799

100+
### 7. 페이지 PNG 렌더링 (VLM 입력) — `07_render_png.py`
101+
102+
```bash
103+
python examples/07_render_png.py path/to/your/file.hwp
104+
python examples/07_render_png.py path/to/your/file.hwp --page 2 --scale 2.0
105+
python examples/07_render_png.py path/to/your/file.hwp --all
106+
python examples/07_render_png.py path/to/your/file.hwp --all --scale 1.5 -o ./out
107+
```
108+
109+
`Document.render_png(page)` / `Document.export_png(dir)` 두 표면을 시연. 단일 페이지
110+
모드 (기본) 는 한 장을 `{prefix}.png` 로 저장 — VLM (Claude Vision / GPT-4V / Gemini)
111+
입력용으로 가장 흔한 형태. `--all` 모드는 전 페이지를 `{prefix}_{NNN}.png` 로 저장한다
112+
(`--scale` / `--max-pixels` 가 기본값과 다르면 page 단위 루프, 아니면 `export_png`
113+
일괄 호출 — 두 API 의 trade-off 학습 포인트). Pillow 가 설치돼 있으면 디코드 dimension
114+
까지 출력, 미설치 시 PNG magic + 길이만 출력 (graceful degrade).
115+
116+
옵션:
117+
- `--page / -p INT` : 0-based 페이지 인덱스 (기본 0). `--all` 지정 시 무시
118+
- `--all` : 전 페이지 일괄 렌더링
119+
- `--scale / -s FLOAT` : 픽셀 너비/높이 배율 (기본 1.0). VLM 해상도 ↑ 시 `2.0` ~ `3.0`
120+
- `--max-pixels INT` : DoS 가드 픽셀 상한 (기본 8192×8192 = 67_108_864)
121+
- `--output-dir / -o PATH` : 출력 디렉토리 (기본 `./render_output`)
122+
- `--prefix TEXT` : PNG 파일명 접두사 (기본 `page`)
123+
98124
## 릴리스 전 실제 HWP 검증
99125

100-
릴리스 직전 **본인의 업무 HWP 파일 3종 (일반 문서 / 장문 / HWPX)** 으로 여섯 스크립트를 순서대로 돌려 출력을 육안 확인한다. 한컴오피스 뷰어로 연 원본과 대조해 섹션/문단/페이지 수치, SVG/PDF 렌더, IR 의 block/table 구조, LangChain Document 매핑, MCP 도구 7 종이 깨지지 않는지 본다.
126+
릴리스 직전 **본인의 업무 HWP 파일 3종 (일반 문서 / 장문 / HWPX)** 으로 일곱 스크립트를 순서대로 돌려 출력을 육안 확인한다. 한컴오피스 뷰어로 연 원본과 대조해 섹션/문단/페이지 수치, SVG/PDF/PNG 렌더, IR 의 block/table 구조, LangChain Document 매핑, MCP 도구 7 종이 깨지지 않는지 본다.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@ cli-chunks = [
6060
"langchain-core>=0.2",
6161
"langchain-text-splitters>=0.2",
6262
]
63-
# ^ examples 는 01~06 예제 스크립트 일괄 실행용 우산 extras — typer + langchain-core +
64-
# text-splitters + fastmcp (06 MCP 데모) 합집합. v0.5.0+ 는 fastmcp v3 (jlowin) 포함.
63+
# ^ examples 는 01~07 예제 스크립트 일괄 실행용 우산 extras — typer + langchain-core +
64+
# text-splitters + fastmcp (06 MCP 데모) + pillow (07 PNG dimension 검증) 합집합.
65+
# v0.5.0+ 는 fastmcp v3 (jlowin), v0.6.1+ 는 pillow 포함.
6566
examples = [
6667
"typer>=0.12",
6768
"langchain-core>=0.2",
6869
"langchain-text-splitters>=0.2",
6970
"fastmcp>=3,<4",
71+
"pillow>=10",
7072
]
7173
# ^ rhwp-mcp MCP 서버 (v0.5.0+). standalone fastmcp v3 (jlowin) — 2026-05 기준
7274
# 현업 표준 (MCP 서버 약 70% 사용). 공식 mcp SDK 안의 FastMCP v1 은 frozen 상태고

0 commit comments

Comments
 (0)