Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .

Expand All @@ -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')"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,7 +24,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0.0",
"mcp>=2,<3",
]

[project.scripts]
Expand Down
4 changes: 2 additions & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 2 additions & 2 deletions src/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
87 changes: 87 additions & 0 deletions tests/test_mcp_handshake.py
Original file line number Diff line number Diff line change
@@ -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()
Loading