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.
plugin.yamlpinsmcp>=1.2,<2because the SDK's 2.0.0 release breaks the plugin outright —build_mcp_appraisesAttributeError: '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:@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.CallToolResultThat is the whole break. It is confined to
mcp_server.py::build_mcp_app: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(), andrun()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 isinput_schemain v2, but theinputSchemaalias still populates it.types.TextContent(type="text", text=...)and thefrom mcp import typesimport (mcp.typesstays a permanent alias for the split-outmcp-typespackage).McpServerThread— pure uvicorn/threading, no SDK contact.list_tools/call_toolreference intests/is our ownToolRegistry, not the SDK, andtest_mcp_server.pyexercises only thread lifecycle. No test edits are required — which is also the reason this needs manual verification (see below).mcp2.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_toolcatches 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_depsand the README'spoetry addline move frommcp>=1.2,<2tomcp>=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 atools/listand atools/call, plus aPluginTaskrow landing for the call.Related
Broadening the MCP bind guard in
mcp.py::on_startfromexcept RuntimeErrortoexcept Exceptionwas deliberately left out of the 7.0 conformance PR. Its motivating case is exactly this incompatibility — an SDK mismatch raisingAttributeErrorout ofbuild_mcp_appmeans 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.