Skip to content

fix(server): anticipated tool errors keep their text under mcp >= 2.1 (#294) - #295

Merged
CryptoJones merged 3 commits into
mainfrom
fix/294-anticipated-tool-errors
Sep 8, 2026
Merged

fix(server): anticipated tool errors keep their text under mcp >= 2.1 (#294)#295
CryptoJones merged 3 commits into
mainfrom
fix/294-anticipated-tool-errors

Conversation

@CryptoJones

Copy link
Copy Markdown
Owner

Fixes #294. Unblocks CI for every open PR, including #289.

What was wrong

pyproject.toml allows mcp>=2.0.0,<3.0 and CI installs with pip install -e .[dev], so it resolves mcp 2.1.1, while the lock (and every local venv) sat on 2.0.0. In 2.1.x the SDK keeps the text of a deliberate ToolError but treats any other exception as a crash and hands the client only Error executing tool <name>. omind's tools let their domain failures escape as NoteError / NoteConflictError / ValueError, so every anticipated message vanished: missing note, unsafe name, bad graph argument, and the stale-version conflict whose text is what tells an agent to re-read before writing. Five test_server.py assertions went red on every OS and Python version.

The fix

  • server.py: every @mcp.tool registration now routes through one _anticipated wrapper that re-raises NoteError, NoteConflictError, and ValueError as a deliberate ToolError (cause chained). A genuine crash (an OSError, a bug) stays masked and logged with its traceback, exactly as the SDK intends. This answers the question the issue raised: the detail is still reachable by callers, on purpose.
  • uv.lock: mcp 2.0.0 → 2.1.1 so a local run sees what CI sees. Nothing else in the lock moved.
  • New regression test pins the behaviour for all three anticipated types and asserts a crash is not exposed.

Verified

  • tests/test_server.py: 26 passed under both mcp 2.0.0 (old lock) and 2.1.1 (overlay + new lock).
  • Full sandboxed suite (scripts/test.sh): 1019 passed, 1 skipped.
  • ruff check . clean, mypy src clean (strict).

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

🤖 Generated with Claude Code

https://claude.ai/code/session_011T5zwA8x2zqEghR2aq86hc

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 7301c08f-4384-4655-a2cd-300c4983d191


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…#294)

mcp 2.1.x hands the client a bare "Error executing tool <name>" for any
exception that is not a deliberate ToolError. omind's tools let their domain
failures escape as NoteError / NoteConflictError / ValueError, so every
anticipated message — a missing note, an unsafe name, a bad graph argument,
and the stale-version conflict that tells an agent to re-read before writing
— vanished, and five test_server assertions went red on every PR.

Re-raise those failures as ToolError at the tool boundary (every registration
routes through one wrapper); a real crash stays masked as the SDK intends.
Move uv.lock to mcp 2.1.1 so a local run sees what CI sees.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011T5zwA8x2zqEghR2aq86hc
@CryptoJones
CryptoJones force-pushed the fix/294-anticipated-tool-errors branch from 301539a to db3c303 Compare September 8, 2026 00:59
With the mcp 2.1 masking fixed, the test matrix went green everywhere
except both Windows jobs — which had been failing on their own the whole
time, invisible because #294 reddened all eight.

rollup_journals took an exclusive lock on each daily and then re-opened
it by path to tally it. POSIX flock is advisory so the second open
succeeds; msvcrt.locking is MANDATORY, so Windows answered
PermissionError: [Errno 13] on a file this process itself had locked.
That arrived with the 2026-08-27 locking hardening — correct on POSIX,
untested on Windows.

It now reads through the descriptor it already holds. That is not just a
Windows workaround: it closes a correctness gap everywhere, because the
tally now counts exactly the bytes the lock protects instead of whatever
a second open happens to see. The fd takes filelock.BINARY so the CRT's
text mode can't rewrite bytes underneath it.

test_resolve_finds_a_cli_outside_path moved home with HOME alone, but
expanduser() reads USERPROFILE on Windows, so home never moved and the
fake CLI was never found. A test bug; conftest._isolate_home already
sets both vars and documents exactly this.

1,024 tests, ruff and mypy green, now under mcp 2.1.1 locally so a local
run finally sees what CI sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxtwuoRxP75PdR6NAmMS3
Comment thread src/omind/journal.py
held: dict[Path, int] = {}
for _, path in dated_paths:
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
fd = os.open(path, os.O_RDWR | os.O_CREAT | filelock.BINARY, 0o600)
Reading through the held descriptor fixed the first Windows failure and
exposed the next one: Windows refuses to rename or unlink a file anyone
still holds open, so the archive step died with [WinError 32] on the
very descriptors the rollup was deliberately holding across
tally -> write -> rename.

Releasing them before the archive is not a hole in what that hold was
protecting. On Windows an appending hook's own open handle is itself
what blocks the rename, so a daily cannot be moved out from under a live
append. POSIX has no such property — an open fd doesn't obstruct a
rename there, only the lock closes the window — so POSIX keeps holding
across the rename exactly as before.

Both release paths now go through one _release() helper that clears the
list, so the finally can't double-close a descriptor number the OS has
already handed back out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxtwuoRxP75PdR6NAmMS3
@CryptoJones
CryptoJones merged commit c856248 into main Sep 8, 2026
17 checks passed
@CryptoJones
CryptoJones deleted the fix/294-anticipated-tool-errors branch September 8, 2026 01:17
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.

CI red on every PR: mcp 2.1.1 masks ToolError messages, breaking 5 test_server.py assertions

2 participants