Five client-side bugs found during a systematic audit. All confirmed with real code, no mocks.
1. Elicitation handler returning scalar T fails for str/int/float/bool
The ElicitationHandler type signature says the return type is T | dict | ElicitResult[T]. For ctx.elicit("msg", str), returning "Alice" (a valid T) crashes with "Elicitation responses must be serializable as a JSON object (dict)."
@mcp.tool
async def ask(ctx: Context) -> str:
result = await ctx.elicit("Name?", response_type=str)
return result.data
async def handler(msg, rtype, params, ctx):
return "Alice" # valid T per type signature — crashes
Root cause: create_elicitation_callback in client/elicitation.py calls to_jsonable_python(result.content) and rejects non-dict results. For scalar types, the internal ScalarElicitationType wrapper is never auto-unwrapped on the return path.
2. Resource returning dict/int/None crashes despite docstring promising auto-serialization
@mcp.resource("data://info")
def info(): return {"key": "value"}
# McpError: contents must be str, bytes, or list[ResourceContent], got dict
FunctionResource docstring says "other types will be converted to JSON" but ResourceResult.__init__ only accepts str | bytes | list[ResourceContent].
3. raise_on_error=True silently ignored when task=True
task = await client.call_tool("fail", task=True, raise_on_error=True)
result = await task
# result.is_error == True — no exception raised
The task=True branch in call_tool doesn't forward raise_on_error to _call_tool_as_task or _parse_call_tool_result.
4. Client.new() shares mutable task state with original
c1 = Client(mcp)
c2 = c1.new()
c1._submitted_task_ids.add("task-abc")
"task-abc" in c2._submitted_task_ids # True — shared reference
copy.copy() is shallow. _task_registry (dict) and _submitted_task_ids (set) are shared references. The docstring says "fresh session state" but task tracking state isn't fresh.
5. Prompt error drops original exception message
Tool errors include the cause: "Error calling tool 'name': original message". Prompt errors drop it: "Error rendering prompt name." (no cause).
@mcp.prompt()
def bad():
raise ValueError("CHECK CONFIG AT /etc/app.conf")
# Client sees: "Error rendering prompt bad." — original message lost
Root cause: function_prompt.py:337 wraps as PromptError(f"Error rendering prompt {self.name}.") without including {e}.
🤖 Generated with Claude Code
Five client-side bugs found during a systematic audit. All confirmed with real code, no mocks.
1. Elicitation handler returning scalar
Tfails forstr/int/float/boolThe
ElicitationHandlertype signature says the return type isT | dict | ElicitResult[T]. Forctx.elicit("msg", str), returning"Alice"(a validT) crashes with "Elicitation responses must be serializable as a JSON object (dict)."Root cause:
create_elicitation_callbackinclient/elicitation.pycallsto_jsonable_python(result.content)and rejects non-dict results. For scalar types, the internalScalarElicitationTypewrapper is never auto-unwrapped on the return path.2. Resource returning dict/int/None crashes despite docstring promising auto-serialization
FunctionResourcedocstring says "other types will be converted to JSON" butResourceResult.__init__only acceptsstr | bytes | list[ResourceContent].3.
raise_on_error=Truesilently ignored whentask=TrueThe
task=Truebranch incall_tooldoesn't forwardraise_on_errorto_call_tool_as_taskor_parse_call_tool_result.4.
Client.new()shares mutable task state with originalcopy.copy()is shallow._task_registry(dict) and_submitted_task_ids(set) are shared references. The docstring says "fresh session state" but task tracking state isn't fresh.5. Prompt error drops original exception message
Tool errors include the cause:
"Error calling tool 'name': original message". Prompt errors drop it:"Error rendering prompt name."(no cause).Root cause:
function_prompt.py:337wraps asPromptError(f"Error rendering prompt {self.name}.")without including{e}.🤖 Generated with Claude Code