Skip to content

fix(mcp): 선언만 되고 CLI 에 닿지 않는 입력 인자 10건 배선 — dryRun:true 인데 파일이 써진다 - #3725

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-args-wiring
Closed

fix(mcp): 선언만 되고 CLI 에 닿지 않는 입력 인자 10건 배선 — dryRun:true 인데 파일이 써진다#3725
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-args-wiring

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

요약

MCP inputSchema선언만 되고 실행 배선이 없는 입력 인자 10건입니다. 서버가 그 값을 조용히 버리므로, 에이전트가 보낸 요청과 실제 실행이 다릅니다. 실측:

hwp_export_structure {mode:"outline"}  →  mode=clause, nodeCount=15      (CLI --mode outline 은 mode=outline, nodeCount=0)
hwp_export_text      {page:1}          →  16쪽 전부                       (page 를 무시)
hwp_replace_text     {dryRun:true, output:"…/out.hwpx"}
                                       →  dryRun=false 로 실행되고 4.1MB 파일이 저장소 루트에 생성됨

dryRun 이 가장 나쁩니다 — 파일을 만들지 말라고 보낸 요청이 파일을 만들고, output 마저 버려져 산출물이 호출자가 지정하지 않은 자리에 떨어집니다.

배선된 인자: output·dryRun(fill_fields·replace_text·set_cell), page(export_text), mode(export_structure), threads(batch 2종).

함정 — presence 판정으로는 부족하다

optionalArgs 확장은 args.get(key).is_none() 으로 "왔는가"만 봤습니다. --dry-run 처럼 값이 없는 스위치args 에 자리표시자가 없고 when 만으로 배선되므로, 이 규칙 그대로 dryRun 을 이으면 dryRun:false 에도 --dry-run 이 붙습니다 — 끄라고 보낸 요청이 켜는 요청이 되어, 편집 도구가 조용히 아무것도 쓰지 않는 정반대 결함이 됩니다.

그래서 확장 지점 한 곳에 규칙을 둡니다: JSON false·null 은 "그 축을 쓰지 않음"으로 읽고 인자를 만들지 않습니다. 특정 키 예외가 아니라 불리언 스위치 배선 전반의 규칙이라, 앞으로 추가되는 스위치도 같은 판정을 받습니다. page:0·threads:1false 가 아니므로 그대로 통과합니다.

AFTER

hwp_export_text {page:1}  →  pages 길이: 1 | page 번호: [1]

검증

  • cargo test --test mcp_server_contract22/22 (신규 계약 테스트 포함)
  • cargo test --test cli_json_contract — 8/8
  • cargo clippy --profile release-test --bin rhwp — 경고 0 / cargo fmt --check — 통과
  • 실기: mcp-serve stdio 로 page:1 왕복 확인

남은 같은 계열

hwp_batch/hwp_batch_searchpaths 는 argv 가 아니라 stdin 으로 가는 유일한 예외이고, 그 사실은 매니페스트의 invocation.stdinTools 가 선언합니다 — 배선 대상이 아닙니다.

이 계열을 근본적으로 막으려면 "모든 inputSchema 속성은 cli.args 또는 optionalArgs 어딘가에 닿아야 한다"는 드리프트 가드가 필요합니다. 이번 PR 범위를 넘어 별도로 올리겠습니다.

🤖 Generated with Claude Code

`inputSchema` 에 선언된 인자가 `cli.args` 자리표시자에도 `cli.optionalArgs` 에도
없으면 MCP 서버는 그 인자를 **조용히 버리고 성공을 보고**한다. 에이전트는 스키마를
읽고 인자를 보냈으니 반영됐다고 믿는다. 가장 위험한 형태:

    hwp_replace_text { dryRun: true, output: "<지정경로>" }
    → 응답 "dryRun": false, "output": "in_replaced.hwp"
    → 지정 경로가 아니라 **서버 CWD 에 파일이 실제로 기록됨**

"쓰지 마라"가 "써라"가 되고, 어디에 썼는지도 응답과 다르다. 되돌릴 수 없는 축에서
계약만 거짓말하는 셈이다.

## 배선한 인자 10건

- hwp_export_text.page → `-p {page}` (전체 6쪽이 오던 것이 1쪽으로)
- hwp_export_structure.mode → `--mode {mode}` (항상 auto 결과만 오던 것)
- hwp_fill_fields / hwp_replace_text / hwp_set_cell 의 output → `-o {output}`
- 같은 3종의 dryRun → `--dry-run`
- hwp_batch.threads / hwp_batch_search.threads → `--threads {threads}`

## presence 플래그의 false 처리 (mcp_serve.rs)

`optionalArgs` 는 `args.get(key).is_none()` 으로만 걸러 **`dryRun: false` 도 존재로
세어 `--dry-run` 을 주입**했다. 값 없는 presence 플래그에서 이는 끄라는 요청을 켜는
요청으로 뒤집는다. JSON 의 `false`/`null` 은 "그 축을 쓰지 않음" 으로 읽는다.

## 재발 차단

`every_declared_input_property_is_wired_to_the_cli` — 선언된 모든 입력 속성이
`cli.args` 자리표시자이거나 `optionalArgs.when` 이어야 한다. argv 가 아닌 축으로
가는 것(batch 의 paths, password)만 근거와 함께 NON_ARGV_PROPERTIES 에 등재한다.
기존 드리프트 가드는 **도구 이름만** 대조해 이 계급을 통째로 못 봤다.

이 가드를 배선 수정 전 매니페스트에 돌리면 정확히 10건을 지목하며 실패한다(확인함).
`boolean_false_does_not_inject_a_presence_flag` 는 false 경로를 따로 못 박는다.

검증: mcp_server_contract 8/8(신규 2 포함), cli_json_contract 22/22,
mcp_password_contract 4/4, batch_axes_contract·digest_macro_contract green,
clippy 0, fmt clean. 실측 왕복은 3축 모두 재확인(page 6→1, mode auto→outline,
dryRun true 시 미기록·false 시 지정 경로 기록).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants