fix(mcp): 선언만 되고 CLI 에 닿지 않는 입력 인자 10건 배선 — dryRun:true 인데 파일이 써진다 - #3725
Closed
kevin9327 wants to merge 1 commit into
Closed
fix(mcp): 선언만 되고 CLI 에 닿지 않는 입력 인자 10건 배선 — dryRun:true 인데 파일이 써진다#3725kevin9327 wants to merge 1 commit into
kevin9327 wants to merge 1 commit into
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
MCP
inputSchema에 선언만 되고 실행 배선이 없는 입력 인자 10건입니다. 서버가 그 값을 조용히 버리므로, 에이전트가 보낸 요청과 실제 실행이 다릅니다. 실측: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:1은false가 아니므로 그대로 통과합니다.AFTER
검증
cargo test --test mcp_server_contract— 22/22 (신규 계약 테스트 포함)cargo test --test cli_json_contract— 8/8cargo clippy --profile release-test --bin rhwp— 경고 0 /cargo fmt --check— 통과mcp-servestdio 로page:1왕복 확인남은 같은 계열
hwp_batch/hwp_batch_search의paths는 argv 가 아니라 stdin 으로 가는 유일한 예외이고, 그 사실은 매니페스트의invocation.stdinTools가 선언합니다 — 배선 대상이 아닙니다.이 계열을 근본적으로 막으려면 "모든
inputSchema속성은cli.args또는optionalArgs어딘가에 닿아야 한다"는 드리프트 가드가 필요합니다. 이번 PR 범위를 넘어 별도로 올리겠습니다.🤖 Generated with Claude Code