Skip to content

feat(wiki): expose the Path B draft tools over MCP (#579) - #581

Merged
cdeust merged 1 commit into
mainfrom
feat/expose-wiki-draft-tools-579
Sep 16, 2026
Merged

cdeust merged 1 commit into
mainfrom
feat/expose-wiki-draft-tools-579

Conversation

@cdeust

@cdeust cdeust commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

wiki_get_draft and wiki_refine_draft (Path B of the wiki pipeline, ADR-0467) existed as handlers with no registration: mcp_server/tool_registry_wiki.py registered ten other wiki tools and no commit in the repository's history ever registered these two, so no client could call them and wiki_get_draft's own instructions told the caller to use a tool tools/list never advertised.

They now register from mcp_server/tool_registry_wiki_drafts.py, wired into __main__'s merged_schemas() and register_all(). The pair lives in its own module because tool_registry_wiki.py stands at 266 lines against the 300-line cap and two registrations in that file's style cost about sixty (ADR-1066). Reading a draft is READ_ONLY; submitting refined prose is NON_IDEMPOTENT_WRITE, since each call inserts an audit memo; both are interactive in handlers/latency_class.py. Neither joins LEAN_TOOL_NAMES, so the lean profile is unchanged.

The standalone count moves from 52 to 54, and from 55 to 57 with both upstream integrations: the pinned tests_py/test_main.py::test_standalone_baseline_is_54_tools, docs/mcp-tools.md, the seven other documents scripts/check_doc_claims.py scans, and the docker_smoke.sh floor.

Closes #579

Type of change

  • New feature (non-breaking change which adds functionality)

Test plan

  • New tests_py/handlers/test_wiki_draft_tools.py: both names on list_tools() with their annotations; the listing arguments (list_pending, kind, limit) and the content arguments (lead, sections, rationale, the synth_model default) reaching the handlers through mcp.call_tool; and one wiki_refine_draft call landing in a SQLite store.
  • tests_py/handlers/test_tool_schema_parity.py already requires every registered wrapper's parameters to equal its handler's inputSchema properties exactly, and now covers these two.
  • Full suite: 8658 passed, 8 skipped. The 5 errors are tests_py/scripts/test_installer_* (added by [bug] deps idempotence guard replaces entries whose name is not their distribution's on every install #575), which shell out to python -m pip in the venv; this machine's uv venv ships no pip (No module named pip). They touch none of the files in this diff.
  • ruff check, ruff format --check: clean. pyright mcp_server/ in the CI typecheck environment built from uv.lock: 0 errors. scripts/check_doc_claims.py: OK. scripts/check_craftsmanship.py --base origin/main: OK.
  • Mutation note: dropping either register call fails the surface test and the pinned count; swapping the two annotations fails the annotation assertions; dropping any wrapper parameter fails the schema-parity test.

Audit notes

  • The pinned baseline test kept 45 lines of membership assertions. Renamed, the method would have counted as new debt against the craftsmanship gate, which only lets the baseline shrink. The tool-name sets are now module constants (_NO_UPSTREAM_TOOLS, _EXTRACTED_TOOLS), the membership check is its own test, and the baseline loses one entry.

Coding-standards compliance

  • §2.2 Layer dependency direction preserved (the new module is a registry beside the others, importing handlers only).
  • §4.1 / §4.2 New module is 91 lines; no method over the caps.
  • §8 No numeric constant added.
  • §9 The dead code this closes is the whole reason for the change.

Breaking changes

None. Two tools appear on tools/list that were never there; nothing is renamed or removed.

Reviewer checklist

  • CHANGELOG.md updated under Unreleased / Added.
  • Documentation updated (docs/mcp-tools.md catalogue entry and counts, ADR-1066).
  • No secrets or PII in the diff.
  • CI passes on the latest commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

wiki_get_draft and wiki_refine_draft existed as handlers since ADR-0467
with no registration, so no client could call them and wiki_get_draft's
own instructions named a tool tools/list never advertised. They now
register from mcp_server/tool_registry_wiki_drafts.py, a module of their
own so tool_registry_wiki.py stays inside the 300-line cap (ADR-1066).

Reading a draft is read-only, submitting refined prose is a
non-idempotent write (one audit memo per call), and both are
interactive-class. The standalone tool count moves from 52 to 54, 57
with both upstream integrations; every document stating a count, the
pinned test name and the docker-smoke floor move with it.

The pinned baseline test no longer carries its membership assertions:
under its new name the method would have been new debt against the
craftsmanship gate, so the tool-name sets are module constants and the
membership check is its own test.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
@cdeust

cdeust commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

Summary

Registers the two pre-existing Path B handlers (wiki_get_draft, wiki_refine_draft) as MCP tools from a new mcp_server/tool_registry_wiki_drafts.py, wired into merged_schemas()/register_all(), with matching annotations, docs, and tool-count updates across 10+ files. Closes #579 by choosing "register" over "remove", as the issue required. Mergeable as-is.

Stakes classification (Move 7)

High. The diff modifies the public MCP tool surface (two tools become externally callable for the first time) and totals 507 changed lines (424 additions, 83 deletions), over the 400-line advisory threshold. I applied the full set of Moves 1 through 6 with runtime checks, not just static reading.

Move 0, ledger and seen-defect check

No formal "Completion Ledger" heading, but the PR's Test Plan and Audit Notes sections enumerate every new code path (wrapper registration, argument forwarding, one SQLite end-to-end call) with a named test and a mutation note, matching this repo's established PR convention; the immediately preceding merged PR #580 uses the same format. My own re-enumeration of the diff's paths found nothing unmapped: the only new logic is two wrapper functions with no branches; handler_get and handler_refine bodies are untouched, confirmed via git diff origin/main..HEAD -- mcp_server/handlers/wiki_refine.py, which shows annotations and docstrings only.

One claim needed correction. The PR states the 5 errors are tests_py/scripts/test_installer_*. Running the full suite here (UV_NO_SYNC=1 uv run pytest -q) produced 11 errors, not 5: 6 in test_installer_rerun_keeps_entries.py, 5 in test_installer_upgrade_single_version.py. The substance of the claim checks out: .venv/bin/python -m pip --version returns "No module named pip" in this worktree's venv, all 11 errors are pip-install subprocess failures inside those two files, and neither file is touched by this diff (git status --short stayed clean throughout my checks). Since the root cause is independently verified external to the diff, a typecheck-only venv with no pip module rather than a dismissed in-scope defect, I did not apply Move 0's REFUSED short-circuit. The "5" in the PR body should read "11" for accuracy; treated as a non-blocking nit.

Verification performed

  1. Registration, runtime. Built a server with register_all(mcp, codebase=False, prd=False) and called list_tools(): both tools present, 54 total, correct annotations (wiki_get_draft read_only_hint=True; wiki_refine_draft read_only_hint=False, idempotent_hint=False). merged_schemas() also carries both; total is 57 with both upstream flags on.
  2. Mutation testing, in a scratch copy, restored afterward with git status --short clean. Dropping _register_wiki_refine_draft(mcp) from register() fails 7 tests (test_wiki_draft_tools.py x3, test_main.py x4), including the pinned test_standalone_baseline_is_54_tools. Swapping the two annotation presets (READ_ONLY and NON_IDEMPOTENT_WRITE) in wiki_refine.py fails test_both_path_b_tools_are_registered on read_only_hint. Both mutations are caught; the new tests are not vacuous.
  3. Wrapper correctness. mcp_server/tool_registry_wiki_drafts.py lines 36-90. Wrapper parameter sets and defaults match the handler schemas exactly (limit=20, list_pending=False, synth_model="claude_refine_v1"), and tests_py/handlers/test_tool_schema_parity.py already enforces this generically for every registered tool (widened by [bug] recall MCP tool does not expose tags_any, tags_all, include_low_signal, cross_domain, sa_mode #559), so this isn't resting solely on my manual comparison.
  4. None-for-optional behavior. wiki_refine.py lines 279-310 (handler_refine) and pg_store_wiki_drafts.py lines 90-137 (update_draft): every optional field is only added to the SET clause when it is not None, so omitting title, lead, sections, or frontmatter is a true no-op for those columns, and passing None explicitly behaves identically to omission. Not new in this PR; handler_refine's body is unchanged by this diff, per the Move 0 section above.
  5. wiki_refine_draft called with only draft_id. synth_model has a non-None default ("claude_refine_v1") both in the wrapper and in handler_refine's own args.get("synth_model", "claude_refine_v1"), so update_draft will set synth_model and the call will insert a refined_llm audit memo even though no content changed. This is pre-existing handler behavior, unchanged by this diff, now reachable by real clients for the first time. Not harmful: no data loss, confidence stays untouched per ADR-1065, and "updated" correctly reports the DB write. It does mean a no-op-looking call still writes an audit row and overwrites synth_model. Worth a follow-up issue if the maintainer wants that call shape to be a true no-op; non-blocking, out of this PR's stated scope of register-or-remove per [bug] wiki_get_draft and wiki_refine_draft (Path B) are defined but never registered as MCP tools #579.
  6. Count consistency. scripts/check_doc_claims.py: OK. scripts/check_craftsmanship.py --base origin/main: OK. ruff check and ruff format --check: clean. .venv/bin/python -m pyright mcp_server/: 0 errors, 0 warnings. pytest tests_py/test_main.py tests_py/test_tool_profiles.py tests_py/handlers/: all passed. Full suite pytest -q: 8658 passed, 8 skipped, 11 errors (installer/pip related, covered above). Grepped for stale "52"/"55" tool-count claims outside CHANGELOG history entries and test fixtures, which legitimately keep historical numbers; found none live.
  7. Layer check. tool_registry_wiki_drafts.py imports only mcp_server.handlers.wiki_refine, tool_meta, and tool_error_handler, matching every sibling tool_registry*.py module. No boundary violation.
  8. SOLID audit. No new conditionals, no interface widening beyond the two intentional new tools, no concrete-infra-in-core issue. The registry module has one responsibility: register these two tools.
  9. Complexity. New module is 91 lines, no method near the 40-line cap. Craftsmanship gate is clean, including the .craftsmanship-baseline.json shrink, one entry removed, matching the test_main.py method that was split and renamed, not gamed; verified the removed entry corresponds to the renamed test method and the baseline's other ~5700 lines are untouched.
  10. Security and hygiene. No new I/O surface beyond what handler_get and handler_refine already had; SQL is parameterized via psycopg placeholders in pg_store_wiki_drafts.py. No secrets. Commit is a single conventional feat PR closing one issue, no scope creep.

Issues

Blocking: none.

Non-blocking:

  • PR body should read 11 errors, not 5 (see Move 0 section above); doesn't change the conclusion.
  • mcp_server/handlers/wiki_refine.py lines 266-334 (handler_refine, pre-existing, not touched by this diff): a wiki_refine_draft call with only draft_id still writes synth_model and inserts a refined_llm audit memo despite no content change, because synth_model always has a non-None default. Consider filing a follow-up issue if the maintainer wants that call shape to be a true no-op now that the tool is externally callable.

Hand-offs

None needed. No root-cause fix, no structural decomposition, no formal-correctness or threat-modeling gap, and no performance claim to verify.

Verdict

APPROVE

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.

[bug] wiki_get_draft and wiki_refine_draft (Path B) are defined but never registered as MCP tools

1 participant