modelcontextprotocol / modelcontextprotocol/python-sdk
In-process callback failures break tool calls and notification sends
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 24.3k
- Forks
- 4k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 31
Description
Initial Checks
- I confirm that I am using the newest release of the 2.x line.
- I confirm that I searched the issue tracker before opening this issue.
Release line
2.x (current stable)
Description
Client(server) uses DirectDispatcher. If a progress callback raises, that exception crosses back into server tool execution, so a tool that otherwise returns successfully fails with UnexpectedToolError. DirectDispatcher also lets an inbound notification handler exception escape back to the sender's notify() call.
The stream-backed JSONRPCDispatcher logs and contains both callback failures. I expected the in-process path to keep the same boundary: log the callback failure, but let the protocol operation continue.
I have a small patch and regression tests for both paths and would like to fix this if maintainers want an outside pull request.
I used GitHub Copilot CLI to inspect the dispatcher paths, write the patch, and draft this report. I verified the reproduction and tests.
Example Code
import anyio
from mcp import Client
from mcp.server.mcpserver import Context, MCPServer
server = MCPServer("callback-repro")
@server.tool()
async def work(context: Context) -> str:
await context.report_progress(1, 1)
return "done"
async def broken_progress(progress: float, total: float | None, message: str | None) -> None:
raise RuntimeError("consumer failed")
async def main() -> None:
async with Client(server, mode="2026-07-28") as client:
result = await client.call_tool("work", progress_callback=broken_progress)
print(result.content[0].text)
anyio.run(main)
Current main ends with:
UnexpectedToolError: Error executing tool work
The tool should print done. The callback failure should still be logged.
Verification
With only the regression tests applied to clean main:
uv run --frozen pytest tests/shared/test_dispatcher.py \
-k 'progress_callback_exception_does_not_fail_request or notification_handler_exception_does_not_reach_sender' -q
# 2 failed, 2 passed. Both DirectDispatcher cases failed.
With the two callback guards applied:
uv run --frozen pytest tests/shared/test_dispatcher.py \
-k 'progress_callback_exception_does_not_fail_request or notification_handler_exception_does_not_reach_sender' -q
# 4 passed
Python & MCP Python SDK
Python 3.12.13
mcp 2.1.1
mcp 2.1.2.dev4+d060b36e at d060b36e1d095ef6e93e07ba5d59bb69b2ad449a
macOS
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the DirectDispatcher paths and compare their callback-failure handling with the stream-backed JSONRPCDispatcher. Run tests/shared/test_dispatcher.py with the two named -k cases to reproduce the failures. Done means both DirectDispatcher cases pass, successful tool calls and notification sends continue, and callback failures are logged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100