Skip to content

Commit 5382c8c

Browse files
committed
[SEP-1575] changes to introduce Tool Versioning
1 parent 3e798bf commit 5382c8c

File tree

9 files changed

+736
-23
lines changed

9 files changed

+736
-23
lines changed

examples/test_versioning.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test script for tool versioning functionality.
4+
5+
This script demonstrates the new tool versioning features implemented according to SEP-1575.
6+
"""
7+
8+
import asyncio
9+
from mcp.server.fastmcp import FastMCP
10+
from mcp.server.fastmcp.utilities.versioning import (
11+
parse_version,
12+
compare_versions,
13+
satisfies_constraint,
14+
find_best_version,
15+
validate_tool_requirements,
16+
VersionConstraintError,
17+
)
18+
19+
20+
# Test version parsing and comparison
21+
def test_version_parsing():
22+
"""Test version parsing functionality."""
23+
print("Testing version parsing...")
24+
25+
# Test valid versions
26+
assert parse_version("1.2.3") == (1, 2, 3, None)
27+
assert parse_version("2.0.0-alpha.1") == (2, 0, 0, "alpha.1")
28+
assert parse_version("0.1.0-beta") == (0, 1, 0, "beta")
29+
30+
# Test version comparison
31+
assert compare_versions("1.2.3", "1.2.4") == -1
32+
assert compare_versions("2.0.0", "1.9.9") == 1
33+
assert compare_versions("1.2.3", "1.2.3") == 0
34+
assert compare_versions("1.2.3", "1.2.3-alpha") == 1 # Stable > prerelease
35+
36+
print("✓ Version parsing tests passed")
37+
38+
39+
def test_constraint_satisfaction():
40+
"""Test constraint satisfaction functionality."""
41+
print("Testing constraint satisfaction...")
42+
43+
# Test exact version
44+
assert satisfies_constraint("1.2.3", "1.2.3") == True
45+
assert satisfies_constraint("1.2.4", "1.2.3") == False
46+
47+
# Test caret (^) - allows non-breaking updates
48+
assert satisfies_constraint("1.2.3", "^1.2.3") == True
49+
assert satisfies_constraint("1.3.0", "^1.2.3") == True
50+
assert satisfies_constraint("2.0.0", "^1.2.3") == False
51+
52+
# Test tilde (~) - allows patch-level updates
53+
assert satisfies_constraint("1.2.3", "~1.2.3") == True
54+
assert satisfies_constraint("1.2.4", "~1.2.3") == True
55+
assert satisfies_constraint("1.3.0", "~1.2.3") == False
56+
57+
# Test comparison operators
58+
assert satisfies_constraint("1.2.3", ">=1.2.0") == True
59+
assert satisfies_constraint("1.1.9", ">=1.2.0") == False
60+
assert satisfies_constraint("1.2.3", "<1.3.0") == True
61+
assert satisfies_constraint("1.3.0", "<1.3.0") == False
62+
63+
print("✓ Constraint satisfaction tests passed")
64+
65+
66+
def test_version_selection():
67+
"""Test best version selection."""
68+
print("Testing version selection...")
69+
70+
available_versions = ["1.0.0", "1.1.0", "1.2.0", "2.0.0-alpha.1", "2.0.0"]
71+
72+
# Test caret constraint
73+
best = find_best_version(available_versions, "^1.0.0")
74+
assert best == "1.2.0" # Latest in 1.x range
75+
76+
# Test tilde constraint
77+
best = find_best_version(available_versions, "~1.1.0")
78+
assert best == "1.1.0" # Exact match for patch level
79+
80+
# Test exact version
81+
best = find_best_version(available_versions, "2.0.0")
82+
assert best == "2.0.0"
83+
84+
# Test no match
85+
best = find_best_version(available_versions, "^3.0.0")
86+
assert best is None
87+
88+
print("✓ Version selection tests passed")
89+
90+
91+
def test_tool_requirements_validation():
92+
"""Test tool requirements validation."""
93+
print("Testing tool requirements validation...")
94+
95+
available_tools = {
96+
"weather": ["1.0.0", "1.1.0", "2.0.0"],
97+
"calculator": ["1.0.0", "1.0.1", "1.1.0"],
98+
}
99+
100+
# Test valid requirements
101+
requirements = {
102+
"weather": "^1.0.0",
103+
"calculator": "~1.0.0"
104+
}
105+
106+
selected = validate_tool_requirements(requirements, available_tools)
107+
assert selected["weather"] == "1.1.0" # Latest in 1.x range
108+
assert selected["calculator"] == "1.0.1" # Latest patch in 1.0.x range
109+
110+
# Test unsatisfied requirement
111+
requirements = {
112+
"weather": "^3.0.0"
113+
}
114+
115+
try:
116+
validate_tool_requirements(requirements, available_tools)
117+
assert False, "Should have raised VersionConstraintError"
118+
except VersionConstraintError:
119+
pass # Expected
120+
121+
print("✓ Tool requirements validation tests passed")
122+
123+
124+
# Create a simple FastMCP server with versioned tools
125+
def create_test_server():
126+
"""Create a test server with versioned tools."""
127+
server = FastMCP("test-server")
128+
129+
def get_weather_v1(location: str) -> str:
130+
"""Get weather for a location (v1)."""
131+
return f"Weather in {location}: Sunny, 72°F (v1.0.0)"
132+
133+
def get_weather_v1_1(location: str) -> str:
134+
"""Get weather for a location (v1.1)."""
135+
return f"Weather in {location}: Partly cloudy, 75°F (v1.1.0)"
136+
137+
def get_weather_v2(location: str) -> str:
138+
"""Get weather for a location (v2)."""
139+
return f"Weather in {location}: Clear skies, 78°F (v2.0.0)"
140+
141+
def calculate_v1(expression: str) -> float:
142+
"""Calculate a simple expression (v1)."""
143+
return eval(expression) # Simple implementation for demo
144+
145+
server.add_tool(get_weather_v1, version="1.0.0")
146+
server.add_tool(get_weather_v1_1, version="1.1.0")
147+
server.add_tool(get_weather_v2, version="2.0.0")
148+
server.add_tool(calculate_v1, version="1.0.0")
149+
150+
return server
151+
152+
153+
async def test_server_versioning():
154+
"""Test server versioning functionality."""
155+
print("Testing server versioning...")
156+
157+
server = create_test_server()
158+
159+
# Test listing tools (should show latest versions)
160+
tools = server._tool_manager.list_tools()
161+
tool_names = [t.name for t in tools]
162+
print(f"Available tools: {tool_names}")
163+
assert "get_weather_v1" in tool_names
164+
assert "calculate_v1" in tool_names
165+
166+
# Test getting specific version
167+
weather_v1 = server._tool_manager.get_tool("get_weather_v1", "1.0.0")
168+
assert weather_v1 is not None
169+
assert weather_v1.version == "1.0.0"
170+
171+
# Test getting latest version
172+
weather_latest = server._tool_manager.get_tool("get_weather_v1")
173+
assert weather_latest is not None
174+
assert weather_latest.version == "1.0.0" # Only one version for this tool
175+
176+
# Test available versions
177+
versions = server._tool_manager.get_available_versions("get_weather_v1")
178+
assert "1.0.0" in versions
179+
180+
print("✓ Server versioning tests passed")
181+
182+
183+
async def main():
184+
"""Run all tests."""
185+
print("Running tool versioning tests...\n")
186+
187+
test_version_parsing()
188+
print()
189+
190+
test_constraint_satisfaction()
191+
print()
192+
193+
test_version_selection()
194+
print()
195+
196+
test_tool_requirements_validation()
197+
print()
198+
199+
await test_server_versioning()
200+
print()
201+
202+
print("🎉 All tests passed! Tool versioning implementation is working correctly.")
203+
204+
205+
if __name__ == "__main__":
206+
asyncio.run(main())
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating tool versioning functionality in MCP.
4+
5+
This example shows how to:
6+
1. Create tools with different versions
7+
2. Use version constraints in tool calls
8+
3. Handle version conflicts and errors
9+
"""
10+
11+
import asyncio
12+
from mcp.server.fastmcp import FastMCP
13+
from mcp.server.fastmcp.exceptions import ToolError
14+
from mcp.types import UNSATISFIED_TOOL_VERSION
15+
16+
17+
def create_versioned_server():
18+
"""Create a server with multiple versions of tools."""
19+
server = FastMCP("versioned-tools-server")
20+
21+
# Weather tool versions
22+
@server.tool(version="1.0.0")
23+
def get_weather_v1(location: str) -> str:
24+
"""Get basic weather information (v1.0.0)."""
25+
return f"Weather in {location}: Sunny, 72°F (Basic API v1.0.0)"
26+
27+
@server.tool(version="1.1.0")
28+
def get_weather_v1_1(location: str) -> str:
29+
"""Get weather with humidity (v1.1.0)."""
30+
return f"Weather in {location}: Partly cloudy, 75°F, Humidity: 65% (Enhanced API v1.1.0)"
31+
32+
@server.tool(version="2.0.0")
33+
def get_weather_v2(location: str) -> str:
34+
"""Get detailed weather with forecast (v2.0.0)."""
35+
return f"Weather in {location}: Clear skies, 78°F, Humidity: 60%, Forecast: Sunny tomorrow (Advanced API v2.0.0)"
36+
37+
# Calculator tool versions
38+
@server.tool(version="1.0.0")
39+
def calculate_v1(expression: str) -> float:
40+
"""Basic calculator (v1.0.0)."""
41+
try:
42+
return eval(expression)
43+
except Exception as e:
44+
raise ValueError(f"Invalid expression: {e}")
45+
46+
@server.tool(version="1.1.0")
47+
def calculate_v1_1(expression: str) -> dict:
48+
"""Calculator with detailed output (v1.1.0)."""
49+
try:
50+
result = eval(expression)
51+
return {
52+
"result": result,
53+
"expression": expression,
54+
"type": type(result).__name__
55+
}
56+
except Exception as e:
57+
raise ValueError(f"Invalid expression: {e}")
58+
59+
return server
60+
61+
62+
async def demonstrate_versioning():
63+
"""Demonstrate various versioning scenarios."""
64+
print("🚀 Tool Versioning Demonstration\n")
65+
66+
server = create_versioned_server()
67+
68+
# 1. List available tools and their versions
69+
print("1. Available Tools:")
70+
tools = server._tool_manager.list_tools()
71+
for tool in tools:
72+
print(f" - {tool.name} (version: {tool.version})")
73+
print()
74+
75+
# 2. Show available versions for each tool
76+
print("2. Available Versions:")
77+
for tool_name in ["get_weather_v1", "calculate_v1"]:
78+
versions = server._tool_manager.get_available_versions(tool_name)
79+
print(f" - {tool_name}: {versions}")
80+
print()
81+
82+
# 3. Demonstrate tool calls without version requirements (uses latest)
83+
print("3. Tool Calls Without Version Requirements (Latest Version):")
84+
try:
85+
result = await server.call_tool("get_weather_v1", {"location": "New York"})
86+
print(f" Weather result: {result}")
87+
88+
result = await server.call_tool("calculate_v1", {"expression": "2 + 3 * 4"})
89+
print(f" Calculator result: {result}")
90+
except Exception as e:
91+
print(f" Error: {e}")
92+
print()
93+
94+
# 4. Demonstrate tool calls with version requirements
95+
print("4. Tool Calls With Version Requirements:")
96+
try:
97+
# Use caret constraint (^1.0.0) - allows non-breaking updates
98+
result = await server.call_tool(
99+
"get_weather_v1",
100+
{"location": "San Francisco"},
101+
tool_requirements={"get_weather_v1": "^1.0.0"}
102+
)
103+
print(f" Weather with ^1.0.0: {result}")
104+
105+
# Use tilde constraint (~1.0.0) - allows only patch updates
106+
result = await server.call_tool(
107+
"calculate_v1",
108+
{"expression": "10 / 2"},
109+
tool_requirements={"calculate_v1": "~1.0.0"}
110+
)
111+
print(f" Calculator with ~1.0.0: {result}")
112+
113+
except Exception as e:
114+
print(f" Error: {e}")
115+
print()
116+
117+
# 5. Demonstrate version conflict handling
118+
print("5. Version Conflict Handling:")
119+
try:
120+
# Try to use a version that doesn't exist
121+
result = await server.call_tool(
122+
"get_weather_v1",
123+
{"location": "Chicago"},
124+
tool_requirements={"get_weather_v1": "^3.0.0"} # No v3.x exists
125+
)
126+
print(f" Unexpected success: {result}")
127+
except ToolError as e:
128+
if hasattr(e, 'code') and e.code == UNSATISFIED_TOOL_VERSION:
129+
print(f" ✓ Correctly caught version conflict: {e}")
130+
else:
131+
print(f" Unexpected error: {e}")
132+
except Exception as e:
133+
print(f" Unexpected error: {e}")
134+
print()
135+
136+
# 6. Demonstrate exact version specification
137+
print("6. Exact Version Specification:")
138+
try:
139+
result = await server.call_tool(
140+
"get_weather_v1",
141+
{"location": "Boston"},
142+
tool_requirements={"get_weather_v1": "1.0.0"} # Exact version
143+
)
144+
print(f" Weather with exact 1.0.0: {result}")
145+
146+
result = await server.call_tool(
147+
"calculate_v1",
148+
{"expression": "5 ** 2"},
149+
tool_requirements={"calculate_v1": "1.1.0"} # Exact version
150+
)
151+
print(f" Calculator with exact 1.1.0: {result}")
152+
153+
except Exception as e:
154+
print(f" Error: {e}")
155+
print()
156+
157+
print("✅ Versioning demonstration completed!")
158+
159+
160+
async def main():
161+
"""Run the demonstration."""
162+
await demonstrate_versioning()
163+
164+
165+
if __name__ == "__main__":
166+
asyncio.run(main())

0 commit comments

Comments
 (0)