Skip to content

Migrate to MCP Python SDK 2.x #2

Description

@vinnybod

plugin.yaml pins mcp>=1.2,<2 because the SDK's 2.0.0 release breaks the plugin outright — build_mcp_app raises AttributeError: 'Server' object has no attribute 'list_tools', so the plugin fails to enable. The bound keeps us on 1.x; this issue is to move off it.

Scoped against a real install of mcp==2.0.0 (throwaway venv, migrated shape executed), not read off the migration guide.

What breaks

The low-level Server's decorator API is gone. Handlers are constructor keyword arguments now, with different signatures and result types:

v1 v2
@server.list_tools()() -> list[types.Tool] on_list_tools=(ctx, params) -> types.ListToolsResult
@server.call_tool()(name, arguments) -> list[types.TextContent] on_call_tool=(ctx, params) -> types.CallToolResult

That is the whole break. It is confined to mcp_server.py::build_mcp_app:

async def _list_tools(request_ctx, params):
    return types.ListToolsResult(tools=[
        types.Tool(name=s.name, description=s.description, inputSchema=s.json_schema)
        for s in registry.list_tools()
    ])

async def _call_tool(request_ctx, params):
    result = await run_in_threadpool(
        registry.call_tool, params.name, params.arguments, ctx)
    return types.CallToolResult(
        content=[types.TextContent(type="text", text=result)])

server = Server("empire-mcp", on_list_tools=_list_tools, on_call_tool=_call_tool)

Verified under 2.0.0: the Starlette app builds and both handlers return the correct result types.

What does not change

  • _streamable.py — nothing. StreamableHTTPSessionManager(app=…, stateless=True), handle_request(), and run() all survive. Worth noting because the file's own comment predicts the opposite ("the ONLY place that touches version-specific API ... a version bump changes one file") — that comment should be corrected or dropped as part of this work.
  • types.Tool(..., inputSchema=...). The field is input_schema in v2, but the inputSchema alias still populates it.
  • types.TextContent(type="text", text=...) and the from mcp import types import (mcp.types stays a permanent alias for the split-out mcp-types package).
  • McpServerThread — pure uvicorn/threading, no SDK contact.
  • The test suite. Every list_tools/call_tool reference in tests/ is our own ToolRegistry, not the SDK, and test_mcp_server.py exercises only thread lifecycle. No test edits are required — which is also the reason this needs manual verification (see below).
  • Python floor. mcp 2.0 requires >=3.10; Empire pins >=3.13,<3.15.

The guide's warning that "tool handler exceptions no longer become CallToolResult(is_error=True)" does not apply to us: ToolRegistry.call_tool catches every exception and returns a string, so nothing propagates out of a handler.

The actual decision

This is a hard cut, not a widening. The decorators do not exist in 2.x and the on_* kwargs do not exist in 1.x, so supporting both means carrying a shim. python_deps and the README's poetry add line move from mcp>=1.2,<2 to mcp>=2,<3, and every operator has to upgrade the SDK inside Empire's own environment.

Verification needed

The suite does not exercise the wire protocol, so green tests will not prove this works. Before merging: a live external MCP client (e.g. Claude Desktop pointed at http://127.0.0.1:2323/mcp/) issuing a tools/list and a tools/call, plus a PluginTask row landing for the call.

Related

Broadening the MCP bind guard in mcp.py::on_start from except RuntimeError to except Exception was deliberately left out of the 7.0 conformance PR. Its motivating case is exactly this incompatibility — an SDK mismatch raising AttributeError out of build_mcp_app means the plugin fails to enable instead of degrading to chat-bridge-only as designed. Migrating removes that particular trigger, but the guard is still worth having for the general case.


This issue was written by Claude with my guidance and reviewed by me before posting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions