- pre_parse_json leaves a string alone when json.loads refuses it with
something other than JSONDecodeError (over-long integer, deep nesting),
so validation rejects it as a bad argument instead of it surfacing as a
crash with a traceback per request.
- convert_result skips output-schema validation for a returned
CallToolResult(is_error=True); an error result has no structured content
to check, and the author's message now reaches the client as written.
- read_resource checks that Resource.read() returned str or bytes, so a
mistyped custom resource is logged as a crash and answered with -32603
rather than "Invalid request parameters" with no log record.
- Docs and examples that still said "raise any exception and the model
reads it" now say ToolError; deprecated.md lists the deprecated
FuncMetadata helper; docstrings spell out the MCPError carve-out and the
nested-crash __cause__.
- Two tests tightened: the prompt argument-validation test proves the body
never ran, and the invalid-types check asserts on validate_arguments.
- Log rejected tool arguments with %r: pydantic's error locations can
include caller-supplied dict keys, which must not break onto new log
lines.
- Build CompleteResult inside the completion adapter's try, so a handler
returning the wrong type is logged as a crash and answered with the
generic -32603 rather than "Invalid request parameters".
- On the legacy resolver path, a malformed ElicitResult from a
non-conformant client no longer has its pydantic text repeated back.
- Add FuncMetadata.call_fn() for calling with already-validated
arguments and use it from Tool.run; call_fn_with_arg_validation()
becomes a deprecated wrapper (MCPDeprecationWarning, removal in 3.0).
- Docstring and docs wording: MCPError carve-outs, nested crash message,
ResourceError in the imports and resource paragraph, the exact
MCPDeprecationWarning path a traceback prints.
A tool that crashed used to send the exception's own text to the client
as "Error executing tool <name>: <str(exc)>". That text can describe
server internals (or, for an output-schema failure, echo the tool's
return value), so a crash now reads just "Error executing tool <name>".
ToolError, ResourceError, and argument-validation messages still reach
the model unchanged, since those are the anticipated failures it can act
on. Closes the tool half of the leak that resources already avoided and
that prompts stopped doing earlier in this branch.
Related tidy-ups in the same direction:
- a crashing @mcp.completion() handler is logged once and answered with
-32603 "Error completing argument <name>" instead of str(exc)
- the legacy resolver path reports a malformed elicitation answer as a
ToolError, matching what the input_required path already did
- the INFO line for rejected arguments names the fields, not the values
Docs now teach ToolError as the way to talk to the model and describe a
plain exception as a crash the model sees generically; examples that
relied on ValueError text reaching the client raise ToolError instead.
A custom argument validator that raises something other than
ValidationError escaped Tool.run unwrapped, losing the "Error executing
tool" prefix and the UnexpectedToolError type. It is now wrapped as a
crash, and an MCPError raised there still passes through.
A ResourceError (usually ResourceNotFoundError from ctx.read_resource)
that escapes a tool body is now classified like a ToolError, since it is
the same anticipated outcome resources/read logs at INFO. An
UnexpectedResourceError escaping a tool stays a crash.
MCPServer.read_resource is now the single place a resource crash is
wrapped (plus create_resource for templates), so the built-in Resource
types let the original exception propagate to direct callers.
Also: trimmed raise-site comments in favour of the exception docstrings,
reworded the ToolError and ResourceError docstrings, documented the
FunctionResource/FileResource.read change in migration.md, corrected the
uri-templates tip and example, and pinned the new cases in tests
(including a wire test for ResourceNotFoundError from a static resource).
A crashing tool used to leave no server-side trace: _handle_call_tool
turned the exception into an is_error result before the dispatcher
boundary could log it, so a KeyError('id') reached the model as "'id'"
and its traceback existed nowhere. Resources logged once and prompts
twice. Tool.run also re-wrapped a deliberate ToolError, so nothing
downstream could tell an anticipated failure from a crash.
Tool.run now validates arguments first (a schema rejection is a plain
ToolError chained to the ValidationError) and runs the body under an
except ladder that keeps the distinction in the type: a deliberate
ToolError stays a ToolError, anything else becomes the new
UnexpectedToolError. Both keep the "Error executing tool X: " text, so
results are byte-identical. Resources get the matching
UnexpectedResourceError, raised by whichever layer first sees the
foreign exception so __cause__ is always the original.
_log_handler_exception in server.py is the one place tools and
resources are logged: INFO without a traceback for ToolError and
ResourceError (deliberate, unknown name, bad arguments, not found),
ERROR with the traceback for anything else. get_prompt stops logging,
leaving the dispatcher boundary's record as the only one.
ResourceError raised from a static resource now passes through to the
client as it already did from a template.