From f61d44a33ffad92daaa5f94205996192cdefeaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arribas=20Lopera?= Date: Wed, 30 Sep 2026 16:59:01 +0200 Subject: [PATCH] fix: support MCP Python SDK 2.x (FastMCP renamed to MCPServer) - Migrate src/mcp_server.py to mcp>=2: from mcp.server.mcpserver import MCPServer - Bound dependency to mcp>=2,<3 in pyproject.toml, ci.yml and release.yml - Add offline in-process initialize-handshake regression test Root cause: the unbounded mcp>=1.0.0 dependency resolved to SDK 2.x, where FastMCP was removed/renamed to MCPServer, so published installs crashed at startup with ModuleNotFoundError: No module named 'mcp.server.fastmcp'. --- .github/workflows/ci.yml | 6 ++- .github/workflows/release.yml | 2 +- CHANGELOG.md | 9 ++++ README.md | 2 +- pyproject.toml | 4 +- src/__init__.py | 4 +- src/mcp_server.py | 4 +- tests/test_mcp_handshake.py | 87 +++++++++++++++++++++++++++++++++++ 8 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 tests/test_mcp_handshake.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1639c7..3a9354d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: | python -m pip install --upgrade pip # Install MCP dependency first - pip install mcp>=1.0.0 + pip install 'mcp>=2,<3' # Then install the package in development mode pip install -e . @@ -43,6 +43,10 @@ jobs: mypy src/ --ignore-missing-imports || echo "Type check completed" continue-on-error: true + - name: Run MCP handshake regression test + run: | + python tests/test_mcp_handshake.py + - name: Test imports and basic functionality run: | python -c "from src.mcp_server import main; print('โœ… MCP server imports successfully')" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb412af..fe3431f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: python -m pip install --upgrade pip pip install build wheel twine # Install MCP dependency for version check - pip install mcp>=1.0.0 + pip install 'mcp>=2,<3' - name: Verify version matches tag run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index bbee688..8a4367d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.4] - 2026-09-30 + +### Fixed +- **MCP SDK 2.x Compatibility**: Fixed startup crash with the current MCP Python SDK + - `uvx codex-bridge` died with `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` on mcp>=2 + - Root cause: pyproject.toml declared `mcp>=1.0.0` with no upper bound while the code imported `FastMCP`, which was renamed to `MCPServer` in SDK 2.x + - Migrated to the 2.x SDK: `from mcp.server.mcpserver import MCPServer` with `"mcp>=2,<3"` dependency bound + - Added an automated regression test that runs a real MCP initialize handshake and lists the three tools in-process + ## [1.2.3] - 2025-12-28 ### Fixed diff --git a/README.md b/README.md index e07796c..57ec120 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A lightweight MCP (Model Context Protocol) server that enables AI coding assista - **Simple MCP Tools**: Two core functions for basic queries and file analysis - **Stateless Operation**: No sessions, caching, or complex state management - **Production Ready**: Robust error handling with configurable timeouts (default: 90 seconds) -- **Minimal Dependencies**: Only requires `mcp>=1.0.0` and Codex CLI +- **Minimal Dependencies**: Only requires `mcp>=2,<3` and Codex CLI - **Easy Deployment**: Support for both uvx and traditional pip installation - **Universal MCP Compatibility**: Works with any MCP-compatible AI coding assistant diff --git a/pyproject.toml b/pyproject.toml index ce146e8..7a111cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codex-bridge" -version = "1.2.3" +version = "1.2.4" description = "Lightweight MCP server bridging Claude Code to OpenAI Codex via official CLI" readme = "README.md" license = "MIT" @@ -24,7 +24,7 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "mcp>=1.0.0", + "mcp>=2,<3", ] [project.scripts] diff --git a/src/__init__.py b/src/__init__.py index 19c9766..bede11c 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,9 +1,9 @@ """ MCP Codex Assistant - Simple CLI bridge to OpenAI Codex. -Version 1.2.3 - Windows UTF-8 encoding fix for international character support. +Version 1.2.4 - MCP SDK 2.x compatibility (FastMCP renamed to MCPServer). """ from .mcp_server import main -__version__ = "1.2.3" +__version__ = "1.2.4" __all__ = ["main"] \ No newline at end of file diff --git a/src/mcp_server.py b/src/mcp_server.py index 70aac83..64b7802 100644 --- a/src/mcp_server.py +++ b/src/mcp_server.py @@ -24,9 +24,9 @@ import time from typing import Dict, List, Optional, Union -from mcp.server.fastmcp import FastMCP +from mcp.server.mcpserver import MCPServer -mcp = FastMCP("codex-assistant") +mcp = MCPServer("codex-assistant") def _is_windows() -> bool: diff --git a/tests/test_mcp_handshake.py b/tests/test_mcp_handshake.py new file mode 100644 index 0000000..fa5933a --- /dev/null +++ b/tests/test_mcp_handshake.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Regression test for the MCP SDK 2.x migration (FastMCP -> MCPServer) + +Runs an offline, in-process MCP initialize handshake against the real server +object from src.mcp_server and lists its tools. No network access and no +Codex CLI invocation is performed. +""" + +import asyncio +import pathlib +import sys +import time + +# Add parent directory to path for imports +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) + +from mcp import Client + +from src.mcp_server import mcp + +EXPECTED_TOOLS = { + "consult_codex", + "consult_codex_with_stdin", + "consult_codex_batch", +} + +TIMEOUT_SECONDS = 30.0 + +handshake_ok = True + +def check(label: str, condition: bool, detail: str = "") -> None: + """Record one assertion result and print it.""" + global handshake_ok + if condition: + print(f"โœ… {label}") + else: + handshake_ok = False + print(f"โŒ {label}" + (f": {detail}" if detail else "")) + + +async def run_handshake() -> None: + """Complete a real initialize handshake and list the server's tools.""" + print("๐Ÿงช Running in-process MCP initialize handshake...") + async with Client(mcp, mode="legacy") as client: + server_info = client.server_info + print(f"๐Ÿงช serverInfo: name={server_info.name!r} version={server_info.version!r}") + print(f"๐Ÿงช protocol version: {client.protocol_version}") + check("handshake succeeded with serverInfo.name == 'codex-assistant'", + server_info.name == "codex-assistant", f"got {server_info.name!r}") + + tools_result = await client.list_tools() + tool_names = {tool.name for tool in tools_result.tools} + print(f"๐Ÿงช Listed tools: {sorted(tool_names)}") + check("tool list is exactly the expected three consult tools", + tool_names == EXPECTED_TOOLS, + f"expected {sorted(EXPECTED_TOOLS)}, got {sorted(tool_names)}") + + +def main() -> None: + """Run the handshake regression test with a hard timeout.""" + print("๐Ÿš€ Starting MCP handshake regression test") + print("=" * 50) + + start = time.monotonic() + try: + asyncio.run(asyncio.wait_for(run_handshake(), timeout=TIMEOUT_SECONDS)) + except Exception as exc: # noqa: BLE001 - script-style test, report and fail + global handshake_ok + handshake_ok = False + print(f"โŒ Handshake failed: {exc!r}") + + elapsed = time.monotonic() - start + print(f"๐Ÿงช Elapsed: {elapsed:.2f}s") + check(f"handshake completed well under 60 seconds (took {elapsed:.2f}s)", + elapsed < 60.0) + + if handshake_ok: + print("๐ŸŽ‰ MCP handshake regression test passed!") + sys.exit(0) + else: + print("๐Ÿ’ฅ MCP handshake regression test failed.") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file