Skip to content

Client bugs: elicitation scalar return, resource dict return, raise_on_error+task, Client.new() shared state, prompt error message #3856

@strawgate

Description

@strawgate

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    too-longExcessively verbose or unedited LLM output. Condense before triage.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions