From 135eb12b8118cc606d6922ca11f65c7f6617e712 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Wed, 26 Aug 2026 13:36:20 +0200 Subject: [PATCH 1/2] Move to the MCP Python SDK 2 (1.1.0) Requires mcp>=2,<3. The lowlevel Server takes its handlers as constructor arguments and returns full result types; a failing tool is still answered as an is_error result rather than the JSON-RPC error mcp 2 would send. Streamable HTTP is served through the SDK's own app factory, the proxy client uses streamable_http_client (two streams, no session-id callback), Tool fields are snake_case, and result parsing accepts is_error beside isError. The examples build an MCPServer instead of FastMCP. The documented one-liner to serve the codemode server never worked with the lowlevel Server (its run() needs streams): run_server is exported and the README and docs use it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HczQyfKTwmuaGJdVkg4f6m --- README.md | 4 +- agent_codemode/__init__.py | 2 + agent_codemode/__version__.py | 2 +- agent_codemode/composition/executor.py | 5 +- agent_codemode/discovery/codegen.py | 5 +- agent_codemode/proxy/mcp_client.py | 9 ++- agent_codemode/server.py | 91 +++++++++++++++----------- docs/docs/index.mdx | 4 +- docs/docs/integrations/index.mdx | 4 +- docs/docs/skills/index.mdx | 5 +- examples/patterns/codemode_example.py | 5 +- examples/simple/example_mcp_server.py | 4 +- examples/skills/example_mcp_server.py | 4 +- pyproject.toml | 5 +- tests/conftest.py | 22 ------- 15 files changed, 87 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 4e586d6..0d318b9 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ agent = Agent( Expose the programmatic tools as an MCP server for any MCP-compatible client: ```python -from agent_codemode import codemode_server, configure_server +from agent_codemode import configure_server, run_server from agent_codemode import ToolRegistry, MCPServerConfig, CodeModeConfig # Create and configure registry with MCP servers to compose @@ -331,7 +331,7 @@ config = CodeModeConfig( ) configure_server(config=config, registry=registry) -codemode_server.run() +run_server() # stdio; run_server(transport="streamable-http", host=..., port=...) for HTTP ``` Or start with command line: diff --git a/agent_codemode/__init__.py b/agent_codemode/__init__.py index 79965cd..c6f8178 100644 --- a/agent_codemode/__init__.py +++ b/agent_codemode/__init__.py @@ -49,6 +49,7 @@ from .proxy.meta_tools import MetaToolProvider from .server import configure as configure_server from .server import mcp as codemode_server +from .server import run as run_server from .toolset import PYDANTIC_AI_AVAILABLE, CodemodeToolset from .types import ( CodeModeConfig, @@ -90,6 +91,7 @@ "configure_server", "parallel", "retry", + "run_server", "run_with_timeout", "setup_skills_directory", # Helpers (from agent_skills) diff --git a/agent_codemode/__version__.py b/agent_codemode/__version__.py index 801f68a..f130d71 100644 --- a/agent_codemode/__version__.py +++ b/agent_codemode/__version__.py @@ -4,4 +4,4 @@ """Agent Codemode.""" -__version__ = "1.0.1" +__version__ = "1.1.0" diff --git a/agent_codemode/composition/executor.py b/agent_codemode/composition/executor.py index 959474e..3cee979 100644 --- a/agent_codemode/composition/executor.py +++ b/agent_codemode/composition/executor.py @@ -702,7 +702,10 @@ async def call_tool(tool_name: str, arguments: dict[str, Any]) -> Any: is_error = False if isinstance(result, dict): - is_error = result.get("isError", False) + is_error = result.get("isError", result.get("is_error", False)) + elif hasattr(result, "is_error"): + # A ``CallToolResult`` of mcp 2, whose fields are snake_case. + is_error = result.is_error elif hasattr(result, "isError"): is_error = result.isError diff --git a/agent_codemode/discovery/codegen.py b/agent_codemode/discovery/codegen.py index 7e43819..8d1051a 100644 --- a/agent_codemode/discovery/codegen.py +++ b/agent_codemode/discovery/codegen.py @@ -133,7 +133,10 @@ async def call_tool(tool_name: str, arguments: dict[str, Any]) -> Any: # Check for error response is_error = False if isinstance(result, dict): - is_error = result.get("isError", False) + is_error = result.get("isError", result.get("is_error", False)) + elif hasattr(result, "is_error"): + # A ``CallToolResult`` of mcp 2, whose fields are snake_case. + is_error = result.is_error elif hasattr(result, "isError"): is_error = result.isError diff --git a/agent_codemode/proxy/mcp_client.py b/agent_codemode/proxy/mcp_client.py index 036aa4c..c0741d2 100644 --- a/agent_codemode/proxy/mcp_client.py +++ b/agent_codemode/proxy/mcp_client.py @@ -124,11 +124,14 @@ async def _get_http_session(self): return self._http_session from mcp.client.session import ClientSession - from mcp.client.streamable_http import streamablehttp_client + from mcp.client.streamable_http import streamable_http_client - http_ctx = streamablehttp_client(self.url) + # mcp 2: the transport yields the two streams only; the session id + # callback of mcp 1 is gone, and headers or timeouts would be set on + # an ``httpx2.AsyncClient`` passed as ``http_client``. + http_ctx = streamable_http_client(self.url) self._http_ctx = http_ctx - read_stream, write_stream, _get_session_id = await http_ctx.__aenter__() + read_stream, write_stream = await http_ctx.__aenter__() http_session = ClientSession(read_stream, write_stream) self._http_session = http_session await http_session.__aenter__() diff --git a/agent_codemode/server.py b/agent_codemode/server.py index f265fd5..36402df 100644 --- a/agent_codemode/server.py +++ b/agent_codemode/server.py @@ -25,7 +25,7 @@ import anyio import mcp.types as types -from mcp.server.lowlevel import Server +from mcp.server import Server, ServerRequestContext from .composition.executor import CodeModeExecutor from .discovery.registry import ToolRegistry @@ -33,8 +33,8 @@ logger = logging.getLogger(__name__) -# Create the MCP server -mcp = Server("codemode") +# The MCP server is built at the end of this module, once its handlers +# exist: mcp 2's lowlevel ``Server`` takes them as constructor arguments. # Global instances (configured at startup) _registry: Optional[ToolRegistry] = None @@ -119,7 +119,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name=name, description=description, - inputSchema=parameters, + input_schema=parameters, ) ) @@ -129,7 +129,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="save_skill", description="Save a reusable skill (code-based tool composition).", - inputSchema={ + input_schema={ "type": "object", "required": ["name", "code", "description"], "properties": { @@ -144,7 +144,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="run_skill", description="Execute a saved skill.", - inputSchema={ + input_schema={ "type": "object", "required": ["name"], "properties": { @@ -156,7 +156,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="list_skills", description="List available skills.", - inputSchema={ + input_schema={ "type": "object", "properties": { "tags": {"type": "array", "items": {"type": "string"}}, @@ -166,7 +166,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="delete_skill", description="Delete a saved skill.", - inputSchema={ + input_schema={ "type": "object", "required": ["name"], "properties": { @@ -177,7 +177,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="get_execution_history", description="Get recent tool execution history.", - inputSchema={ + input_schema={ "type": "object", "properties": { "limit": {"type": "integer", "default": 10}, @@ -187,7 +187,7 @@ def _build_tools() -> list[types.Tool]: types.Tool( name="add_mcp_server", description="Add a new MCP server to discover tools from.", - inputSchema={ + input_schema={ "type": "object", "required": ["name"], "properties": { @@ -588,25 +588,47 @@ async def handle_add_mcp_server(arguments: dict[str, Any]) -> dict[str, Any]: # ============================================================================= -@mcp.list_tools() -async def list_tools() -> list[types.Tool]: +async def list_tools( + ctx: ServerRequestContext, params: types.PaginatedRequestParams | None +) -> types.ListToolsResult: """Return the list of available tools.""" config = _config or CodeModeConfig() if config.allow_direct_tool_calls: - return TOOLS - return [tool for tool in TOOLS if tool.name != "call_tool"] + return types.ListToolsResult(tools=TOOLS) + return types.ListToolsResult(tools=[tool for tool in TOOLS if tool.name != "call_tool"]) + +async def call_tool( + ctx: ServerRequestContext, params: types.CallToolRequestParams +) -> types.CallToolResult: + """Handle tool calls. -@mcp.call_tool() -async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]: - """Handle tool calls.""" - handler = TOOL_HANDLERS.get(name) + A failure is answered as a result with ``is_error`` set, the way mcp 1's + decorator did it: mcp 2's lowlevel server turns an exception raised here + into a JSON-RPC error instead, which an agent cannot read as a tool + outcome. + """ + handler = TOOL_HANDLERS.get(params.name) if handler is None: - raise ValueError(f"Unknown tool: {name}") + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")], + is_error=True, + ) - result = await handler(arguments) + try: + result = await handler(params.arguments or {}) + except Exception as e: # every failure is reported to the agent + logger.debug("Tool %s failed", params.name, exc_info=e) + return types.CallToolResult( + content=[types.TextContent(type="text", text=str(e))], + is_error=True, + ) json_str = json.dumps(result, indent=2) - return [types.TextContent(type="text", text=json_str)] + return types.CallToolResult(content=[types.TextContent(type="text", text=json_str)]) + + +# Create the MCP server +mcp = Server("codemode", on_list_tools=list_tools, on_call_tool=call_tool) # ============================================================================= @@ -629,26 +651,15 @@ def run(transport: str = "stdio", host: str = "127.0.0.1", port: int = 8000) -> if transport == "streamable-http": import uvicorn - from mcp.server.streamable_http import StreamableHTTPServerTransport - from starlette.applications import Starlette - from starlette.routing import Route - async def handle_mcp(request): - transport_ctx: Any = StreamableHTTPServerTransport( - "/mcp", request.scope, request.receive, request._send - ) - async with transport_ctx as transport: - await mcp.run( - transport.read_stream, - transport.write_stream, - mcp.create_initialization_options(), - ) - - starlette_app = Starlette( - debug=True, - routes=[ - Route("/mcp", endpoint=handle_mcp, methods=["POST"]), - ], + # Stateless, as before: each request is served on a transport of its + # own. ``host`` is passed on because the SDK enables DNS-rebinding + # protection, with a localhost-only allowlist, when it is a loopback + # address — a server bound elsewhere must not inherit that list. + starlette_app = mcp.streamable_http_app( + streamable_http_path="/mcp", + stateless_http=True, + host=host, ) uvicorn.run(starlette_app, host=host, port=port) diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index 47f87e2..149d53c 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -247,11 +247,11 @@ See [Integrations](./integrations/index.mdx) for complete documentation. **Exposed as an MCP Server:** ```python -from agent_codemode import codemode_server, configure_server +from agent_codemode import configure_server, run_server # Configure with your registry configure_server(config=config, registry=registry) -codemode_server.run() +run_server() ``` Or run directly: diff --git a/docs/docs/integrations/index.mdx b/docs/docs/integrations/index.mdx index d03fa1f..78b0f7e 100644 --- a/docs/docs/integrations/index.mdx +++ b/docs/docs/integrations/index.mdx @@ -136,13 +136,13 @@ python -m agent_codemode.server Or programmatically: ```python -from agent_codemode import codemode_server, configure_server, ToolRegistry, MCPServerConfig +from agent_codemode import configure_server, run_server, ToolRegistry, MCPServerConfig registry = ToolRegistry() registry.add_server(MCPServerConfig(name="filesystem", command="npx", args=["@anthropic-ai/mcp-server-filesystem"])) configure_server(registry=registry) -codemode_server.run() +run_server() ``` The server exposes tools for: diff --git a/docs/docs/skills/index.mdx b/docs/docs/skills/index.mdx index a3afa68..336d41b 100644 --- a/docs/docs/skills/index.mdx +++ b/docs/docs/skills/index.mdx @@ -353,8 +353,7 @@ When running Agent Codemode as an MCP server, skills are exposed through dedicat - **`run_skill`**: Execute a saved skill by name ```python -from agent_codemode import codemode_server, configure_server -from agent_codemode import CodeModeConfig +from agent_codemode import CodeModeConfig, configure_server, run_server config = CodeModeConfig( skills_path="./skills", @@ -362,7 +361,7 @@ config = CodeModeConfig( ) configure_server(config=config) -codemode_server.run() +run_server() ``` ## Pydantic AI Integration diff --git a/examples/patterns/codemode_example.py b/examples/patterns/codemode_example.py index 6f5e851..8267484 100644 --- a/examples/patterns/codemode_example.py +++ b/examples/patterns/codemode_example.py @@ -158,8 +158,9 @@ async def _server(): # Configure with MCP servers configure_server() - # Run the MCP server (uses FastMCP under the hood) - codemode_server.run() + # Serve it over stdio (the MCP SDK's lowlevel Server under the hood) + from agent_codemode.server import run + run() Or from the command line: diff --git a/examples/simple/example_mcp_server.py b/examples/simple/example_mcp_server.py index 5249a58..0d127bd 100644 --- a/examples/simple/example_mcp_server.py +++ b/examples/simple/example_mcp_server.py @@ -13,10 +13,10 @@ from pathlib import Path from typing import Optional -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from typing_extensions import TypedDict -mcp = FastMCP("example-mcp-server") +mcp = MCPServer("example-mcp-server") # Base directory for file operations - defaults to /tmp if CWD is not writable _BASE_DIR: Path | None = None diff --git a/examples/skills/example_mcp_server.py b/examples/skills/example_mcp_server.py index 5249a58..0d127bd 100644 --- a/examples/skills/example_mcp_server.py +++ b/examples/skills/example_mcp_server.py @@ -13,10 +13,10 @@ from pathlib import Path from typing import Optional -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from typing_extensions import TypedDict -mcp = FastMCP("example-mcp-server") +mcp = MCPServer("example-mcp-server") # Base directory for file operations - defaults to /tmp if CWD is not writable _BASE_DIR: Path | None = None diff --git a/pyproject.toml b/pyproject.toml index 31c6a2d..719258a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,10 @@ classifiers = [ dependencies = [ "agent-skills", "code-sandboxes", - "mcp[cli]>=1.10.1,<2", + # The MCP Python SDK 2: FastMCP became MCPServer, the lowlevel Server takes + # its handlers as constructor arguments, and clients come with httpx2. See + # https://py.sdk.modelcontextprotocol.io/v2/migration/ + "mcp[cli]>=2,<3", "pydantic>=2.0", "httpx>=0.24", ] diff --git a/tests/conftest.py b/tests/conftest.py index 68d264e..a6ce79e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,28 +9,6 @@ import pytest -def _rebuild_fastmcp_settings() -> None: - """Resolve the forward reference in FastMCP's ``Settings`` model. - - ``mcp.server.fastmcp.server.Settings.lifespan`` is annotated with - ``FastMCP``, which is defined further down the same module, and upstream - never calls ``model_rebuild()``. Recent pydantic-settings releases warn - (``IncompleteFieldDefinitionWarning``) when such a model is instantiated, - and this suite turns warnings into errors, so collection fails as soon as - anything constructs a FastMCP server. Rebuilding the model once resolves - the reference for real instead of muting the warning. - """ - try: - from mcp.server.fastmcp.server import Settings - except ImportError: # pragma: no cover - mcp layout changed - return - if not getattr(Settings, "__pydantic_complete__", True): - Settings.model_rebuild() - - -_rebuild_fastmcp_settings() - - @pytest.fixture def skills_dir(tmp_path: Path) -> Path: """Create a temporary skills directory.""" From 660418d764316f9367a20ab80f256d5b2f8e1907 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Thu, 27 Aug 2026 10:06:37 +0200 Subject: [PATCH 2/2] fix: docs --- docs/docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index afa9761..9498c98 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -161,7 +161,7 @@ module.exports = { }, { label: 'Bluesky', - href: 'https://assets.datalayer.tech/logos-social-grey/youtube.svg', + href: 'https://assets.datalayer.tech/logos-social-grey/bluesky.svg', }, { label: 'LinkedIn',