thin mcp: declare ToolAnnotations so approval-gating hosts can pass (#86) - #103
Conversation
Root-caused from #86's triage: codex raises a per-call approval elicitation for MCP tools without annotations (mcp_tool_call_approval_exec), cancels them outright in headless runs ("user cancelled MCP tool call"), and its interactive approval dialog is where the reported crash lives — the server itself returns review(mode=strict)'s run_id in 161 ms. Every tool now declares truthful hints: `review` is the only tool that mutates state (reserves a Strict run) or reaches the network (readOnlyHint=false, destructiveHint=false, openWorldHint=true); the other six are readOnlyHint=true. The mcp extra floor moves 1.2 -> 1.8, the first version verified to carry ToolAnnotations and the decorator kwarg. The fake FastMCP in the tests records tool kwargs and fakes mcp.types; a new guardrail test pins the read-only surface. codex host doc gains a "known behavior" section for the approval dialog and the headless auto-cancel; SPEC page re-verified with the new invariant. Refs #86. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0165QsXAeLVPrKfj8Yk5zns3
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad8c3cda58
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| read_only = ToolAnnotations( | ||
| readOnlyHint=True, idempotentHint=True, openWorldHint=False) |
There was a problem hiding this comment.
Do not mark reconciling poll tools read-only
When an unfinished run's owner and child have died, both get_review_result and get_review_status call CopilotMCP.get_result/get_status, which invoke reconcile_if_dead and rewrite run_status.json to interrupted (and even ordinary polls create/truncate the status lock file). Reusing this readOnlyHint=True annotation therefore lets approval-gating hosts auto-approve tools that can mutate durable run state, contradicting the annotation's security contract; either make polling observational or give these two tools non-read-only annotations.
Useful? React with 👍 / 👎.
|
Behavioral e2e on codex-cli 0.145.0 — the annotations demonstrably flip the outcome. Same isolated
So this is not only for future annotation-aware versions: 0.145.0 already auto-approves |
What
Declares MCP
ToolAnnotationson all seven thin-server tools, so hosts that gateun-annotated tools behind per-call approval can auto-approve the read-only surface:
reviewvalidate_direct_review,get_review_status,get_review_result,update_knowledge,doc_search,doc_readreviewalso carriesdestructiveHint=false. The[mcp]extra floor moves1.2 → 1.8—the first version verified (installed and checked) to ship
ToolAnnotationsand thedecorator kwarg; tested against 1.29.0.
Why (from #86's triage)
The reported "Strict mode crashes codex" reproduces on our side as a codex-side approval
flow, not a server fault:
review(mode=strict)returns itsrun_idin 161 ms; theserver is healthy (and its durable-run reconciliation behaved correctly when the owner
died mid-test).
codex exec(0.145.0), the same call fails instantly withuser cancelled MCP tool call;RUST_LOGdebug shows codex raisingmcp_tool_call_approval_exec-…for the un-annotated tool and auto-resolving it asCancelunder headlessapproval: never. Interactively, that same flow is an approvaldialog — where the reported crash lives, before the server does any work.
Truthful annotations let annotation-aware codex versions auto-approve the six read-only
tools;
reviewstays honestly non-read-only.Also
doc/guide/hosts/codex.md: new "已知行为:MCP 工具审批" section documenting theinteractive approval dialog and the headless auto-cancel (plus the existing
startup_timeout_sec=120installer setting for slow firstuvxstartup).doc/architecture/SPEC/thin_mcp_server.md: re-verified (2026-08-25) with the newinvariant — hints must stay truthful; marking a writing tool read-only is worse than not
marking it.
mcp.types; a new guardrailtest pins the annotation surface. Verified against real FastMCP too (
list_tools()round-trip).
Verification
pytest test/test_thin_mcp_server.py test/test_thin_mcp.pyand the full offline suite — greentools/check_doc_links.py,tools/check_doc_citations.py— OKtools/check_spec_freshness.py --strict— this branch adds no stale page(
thin_mcp_serverre-verified; the 11 stale pages it reports are pre-existing onmain,identical with this change stashed)
Refs #86 (needs-info from the reporter still pending for their exact codex version/crash text).
🤖 Generated with Claude Code
https://claude.ai/code/session_0165QsXAeLVPrKfj8Yk5zns3