Skip to content

Commit a9cbdff

Browse files
committed
compatibility tests
1 parent 4d50565 commit a9cbdff

File tree

3 files changed

+277
-1
lines changed

3 files changed

+277
-1
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: MCP Version Compatibility Testing
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
schedule:
9+
# Run weekly to catch new MCP versions
10+
- cron: '0 0 * * 0'
11+
workflow_dispatch:
12+
13+
jobs:
14+
discover-versions:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
mcp-versions: ${{ steps.get-versions.outputs.versions }}
18+
19+
steps:
20+
- name: Get available MCP versions
21+
id: get-versions
22+
run: |
23+
# Get all available versions from PyPI
24+
versions=$(pip index versions mcp 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | sort -V)
25+
26+
# Filter to versions >= 1.0.0 and format as JSON array
27+
filtered_versions=()
28+
for version in $versions; do
29+
major=$(echo $version | cut -d. -f1)
30+
minor=$(echo $version | cut -d. -f2)
31+
patch=$(echo $version | cut -d. -f3)
32+
33+
# Include if major >= 1
34+
if [ "$major" -ge 1 ]; then
35+
filtered_versions+=("\"$version\"")
36+
fi
37+
done
38+
39+
# Create JSON array
40+
json_array="[$(IFS=,; echo "${filtered_versions[*]}")]"
41+
echo "Found MCP versions: $json_array"
42+
echo "versions=$json_array" >> $GITHUB_OUTPUT
43+
44+
test-compatibility:
45+
runs-on: ubuntu-latest
46+
needs: discover-versions
47+
strategy:
48+
matrix:
49+
mcp-version: ${{ fromJson(needs.discover-versions.outputs.mcp-versions) }}
50+
fail-fast: false
51+
52+
steps:
53+
- uses: actions/checkout@v4
54+
55+
- name: Install uv
56+
uses: astral-sh/setup-uv@v4
57+
with:
58+
version: "latest"
59+
60+
- name: Set up Python 3.12
61+
uses: actions/setup-python@v5
62+
with:
63+
python-version: '3.12'
64+
65+
- name: Install dependencies with MCP ${{ matrix.mcp-version }}
66+
run: |
67+
uv sync
68+
uv pip install mcp==${{ matrix.mcp-version }}
69+
70+
- name: Run full test suite with MCP ${{ matrix.mcp-version }}
71+
run: |
72+
echo "Running full test suite with MCP version ${{ matrix.mcp-version }}"
73+
uv run pytest tests/ -v
74+
75+
report-compatibility:
76+
runs-on: ubuntu-latest
77+
needs: [discover-versions, test-compatibility]
78+
if: always()
79+
80+
steps:
81+
- uses: actions/checkout@v4
82+
83+
- name: Create compatibility report
84+
run: |
85+
echo "# MCP Version Compatibility Report" > compatibility-report.md
86+
echo "" >> compatibility-report.md
87+
echo "This report shows the compatibility status of MCPCat with different MCP versions." >> compatibility-report.md
88+
echo "" >> compatibility-report.md
89+
echo "Generated on: $(date)" >> compatibility-report.md
90+
echo "" >> compatibility-report.md
91+
echo "## Tested Versions" >> compatibility-report.md
92+
echo "MCP versions tested: ${{ needs.discover-versions.outputs.mcp-versions }}" >> compatibility-report.md
93+
echo "Python version: 3.12" >> compatibility-report.md
94+
echo "" >> compatibility-report.md
95+
echo "## Test Coverage" >> compatibility-report.md
96+
echo "- FastMCP server compatibility" >> compatibility-report.md
97+
echo "- Low-level Server compatibility" >> compatibility-report.md
98+
echo "- Both implementations tested with is_compatible_server function" >> compatibility-report.md
99+
echo "" >> compatibility-report.md
100+
echo "See the GitHub Actions run for detailed results." >> compatibility-report.md
101+
102+
- name: Upload compatibility report
103+
uses: actions/upload-artifact@v4
104+
with:
105+
name: mcp-compatibility-report
106+
path: compatibility-report.md
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Comprehensive MCP Version Compatibility Tests."""
2+
3+
import pytest
4+
5+
from mcpcat.modules.compatibility import is_compatible_server
6+
7+
8+
def create_fastmcp_server():
9+
"""Create a FastMCP server instance for testing."""
10+
try:
11+
from tests.test_utils.todo_server import create_todo_server
12+
return create_todo_server()
13+
except ImportError:
14+
return None
15+
16+
17+
def create_low_level_server():
18+
"""Create a low-level MCP server instance for testing."""
19+
try:
20+
from tests.test_utils.todo_server import create_low_level_todo_server
21+
return create_low_level_todo_server()
22+
except ImportError:
23+
return None
24+
25+
26+
class TestMCPVersionCompatibility:
27+
"""Test MCP version compatibility functions."""
28+
29+
def test_fastmcp_compatibility_with_current_version(self):
30+
"""Test FastMCP compatibility with currently installed MCP version."""
31+
server = create_fastmcp_server()
32+
assert server is not None, "Should be able to create FastMCP server"
33+
34+
# Test compatibility
35+
is_compatible = is_compatible_server(server)
36+
assert isinstance(is_compatible, bool), "Compatibility check should return boolean"
37+
38+
# With current version, should be compatible
39+
assert is_compatible, "FastMCP server should be compatible with current MCP version"
40+
41+
def test_server_compatibility_with_current_version(self):
42+
"""Test low-level Server compatibility with currently installed MCP version."""
43+
server = create_low_level_server()
44+
assert server is not None, "Should be able to create low-level server"
45+
46+
# Test compatibility
47+
is_compatible = is_compatible_server(server)
48+
assert isinstance(is_compatible, bool), "Compatibility check should return boolean"
49+
50+
# With current version, should be compatible
51+
assert is_compatible, "Low-level server should be compatible with current MCP version"
52+
53+
def test_compatibility_functions_exist(self):
54+
"""Test that all compatibility functions are available and work."""
55+
# Test that we can import and use the compatibility functions
56+
from mcpcat.modules.compatibility import (
57+
is_compatible_server,
58+
is_fastmcp_server,
59+
has_neccessary_attributes
60+
)
61+
62+
# Test with FastMCP server
63+
fastmcp_server = create_fastmcp_server()
64+
if fastmcp_server:
65+
assert is_fastmcp_server(fastmcp_server), "Should detect FastMCP server"
66+
assert has_neccessary_attributes(fastmcp_server), "FastMCP should have necessary attributes"
67+
assert is_compatible_server(fastmcp_server), "FastMCP should be compatible"
68+
69+
# Test with low-level server
70+
low_level_server = create_low_level_server()
71+
if low_level_server:
72+
assert not is_fastmcp_server(low_level_server), "Should not detect low-level server as FastMCP"
73+
assert has_neccessary_attributes(low_level_server), "Low-level server should have necessary attributes"
74+
assert is_compatible_server(low_level_server), "Low-level server should be compatible"

tests/test_utils/todo_server.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Todo server implementation for testing."""
22

33

4-
from mcp.server import FastMCP
4+
from mcp.server import FastMCP, Server
55

66

77
class Todo:
@@ -61,3 +61,99 @@ def complete_todo(id: str) -> str:
6161
}
6262

6363
return server
64+
65+
66+
def create_low_level_todo_server() -> Server:
67+
"""Create a low-level MCP server for testing."""
68+
from mcp.types import Tool, TextContent
69+
70+
server = Server("todo-server-lowlevel")
71+
72+
todos: list[Todo] = []
73+
next_id = 1
74+
75+
# Define tools
76+
add_todo_tool = Tool(
77+
name="add_todo",
78+
description="Add a new todo item",
79+
inputSchema={
80+
"type": "object",
81+
"properties": {
82+
"text": {
83+
"type": "string",
84+
"description": "The todo item text"
85+
}
86+
},
87+
"required": ["text"]
88+
}
89+
)
90+
91+
list_todos_tool = Tool(
92+
name="list_todos",
93+
description="List all todo items",
94+
inputSchema={
95+
"type": "object",
96+
"properties": {}
97+
}
98+
)
99+
100+
complete_todo_tool = Tool(
101+
name="complete_todo",
102+
description="Mark a todo item as completed",
103+
inputSchema={
104+
"type": "object",
105+
"properties": {
106+
"id": {
107+
"type": "string",
108+
"description": "The todo item ID"
109+
}
110+
},
111+
"required": ["id"]
112+
}
113+
)
114+
115+
@server.list_tools()
116+
async def list_tools():
117+
return [add_todo_tool, list_todos_tool, complete_todo_tool]
118+
119+
@server.call_tool()
120+
async def call_tool(name: str, arguments: dict):
121+
nonlocal next_id
122+
123+
if name == "add_todo":
124+
text = arguments["text"]
125+
todo = Todo(str(next_id), text)
126+
todos.append(todo)
127+
next_id += 1
128+
return [TextContent(
129+
type="text",
130+
text=f'Added todo: "{text}" with ID {todo.id}'
131+
)]
132+
133+
elif name == "list_todos":
134+
if not todos:
135+
return [TextContent(type="text", text="No todos found")]
136+
137+
todo_list = []
138+
for todo in todos:
139+
status = "✓" if todo.completed else "○"
140+
todo_list.append(f"{todo.id}: {todo.text} {status}")
141+
142+
return [TextContent(type="text", text="\n".join(todo_list))]
143+
144+
elif name == "complete_todo":
145+
todo_id = arguments["id"]
146+
for todo in todos:
147+
if todo.id == todo_id:
148+
todo.completed = True
149+
return [TextContent(
150+
type="text",
151+
text=f'Completed todo: "{todo.text}"'
152+
)]
153+
154+
raise ValueError(f"Todo with ID {todo_id} not found")
155+
156+
else:
157+
raise ValueError(f"Unknown tool: {name}")
158+
159+
return server

0 commit comments

Comments
 (0)