Docstrings, section headers and assertion messages drop the record number,
milestone or step that introduced the behaviour, and incident accounts are
restated as the present-tense failure shape. A dead pointer to a design doc
that is not in the repo is removed.
Renamed to the property they check:
test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught
test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld
test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker
test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file
test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with
test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note
Fixture and example ids stay: "note 5" in a supersession chain, "rule 156"
in an illustrative question, `#2275` as the hex-versus-citation case,
`#3244` as the example of a bare number, fixed dates in date-filter tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen more test files: docstrings state the failure each guard defends
as a present-tense shape instead of the incident that found it, and issue,
milestone and rule numbers leave comments and assertion messages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDK 2.x passes on only a ToolError's text. Any other exception becomes
UnexpectedToolError("Error executing tool X") and its message stays on the
server. ValueError is how every Scribe tool refuses — what was refused, why,
what to do instead — so every refusal reached the agent bare, and it could
only retry blind. Seen live: tune_retrieval(actor="operator") and
judge_menu(verdicts=[]) both answered "Error executing tool …" and nothing
else.
StrictArgsMCPServer.call_tool re-raises an UnexpectedToolError caused by a
ValueError as a ToolError carrying the message, in the SDK's own
"Error executing tool X: <reason>" shape. Other exceptions are crashes and
stay masked. The stale comment claiming the SDK returns ValueError text is
corrected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- server.py: `mcp.server.mcpserver.MCPServer`; StrictArgsFastMCP becomes
StrictArgsMCPServer, whose call_tool takes and forwards v2's `context`
and raises ToolError (the SDK logs anything else as an unexpected crash;
the message reaches the caller either way).
- stateless_http and transport_security moved from the constructor to
`streamable_http_app(...)` in mount_mcp, with their reasons.
- Per-request user identity is unchanged: the contextvar set around the
ASGI call reaches the handler on both v2 paths (legacy stateless spawns
from the request task; the 2026-07-28 modern path opens its task group
inside the request).
- pyproject: mcp[cli]>=2.2, no ceiling (installs are --locked). uv.lock
regenerated with --upgrade-package mcp in the ci-python image: only mcp
and its own dependencies moved (106 → 110 packages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FastMCP validates tool arguments with a pydantic model whose extra-field
policy is 'ignore', so create_note(content=...) — a plausible near-miss
for body=, primed by add_task_log's content — ran successfully, stored
body: '', and left a record embedding/search cannot see. Two real notes
were persisted body-less before the pattern was noticed; create_task
only 'worked' because those calls happened to use the right name.
StrictArgsFastMCP rejects any tool call carrying arguments the tool does
not declare, before dispatch, with a did-you-mean hint when one is close
and the declared list when none is. Applied at the dispatch seam so
every tool gets the guarantee — an error the caller sees once beats data
half-written forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>