mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Migrate to MCP Python SDK v2 (#4437)
This commit is contained in:
parent
1eedd1f6f1
commit
3522a98766
283 changed files with 6484 additions and 3387 deletions
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import mcp.types
|
||||
import mcp_types
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -161,17 +161,17 @@ class TestFormatToolSignature:
|
|||
required: list[str] | None = None,
|
||||
output_schema: dict[str, Any] | None = None,
|
||||
description: str | None = None,
|
||||
) -> mcp.types.Tool:
|
||||
) -> mcp_types.Tool:
|
||||
input_schema: dict[str, Any] = {"type": "object"}
|
||||
if properties is not None:
|
||||
input_schema["properties"] = properties
|
||||
if required is not None:
|
||||
input_schema["required"] = required
|
||||
return mcp.types.Tool(
|
||||
return mcp_types.Tool(
|
||||
name=name,
|
||||
description=description,
|
||||
inputSchema=input_schema,
|
||||
outputSchema=output_schema,
|
||||
input_schema=input_schema,
|
||||
output_schema=output_schema,
|
||||
)
|
||||
|
||||
def test_no_params(self):
|
||||
|
|
@ -546,7 +546,7 @@ class TestFormatCallResult:
|
|||
"""structured_content (raw dict) is used for display, not data (which may
|
||||
be a non-serializable dataclass)."""
|
||||
result = CallToolResult(
|
||||
content=[mcp.types.TextContent(type="text", text="ok")],
|
||||
content=[mcp_types.TextContent(type="text", text="ok")],
|
||||
structured_content={"key": "value"},
|
||||
meta=None,
|
||||
data=object(), # non-serializable on purpose
|
||||
|
|
@ -561,7 +561,7 @@ class TestFormatCallResult:
|
|||
self, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
result = CallToolResult(
|
||||
content=[mcp.types.TextContent(type="text", text="[red]x[/red]\x1b[2J")],
|
||||
content=[mcp_types.TextContent(type="text", text="[red]x[/red]\x1b[2J")],
|
||||
structured_content=None,
|
||||
meta=None,
|
||||
data=None,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import mcp.types
|
||||
import mcp_types
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -148,9 +148,9 @@ class TestSerializeTransport:
|
|||
|
||||
class TestToolFunctionSource:
|
||||
def test_required_param(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="greet",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {"name": {"type": "string", "description": "Who"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
|
|
@ -162,9 +162,9 @@ class TestToolFunctionSource:
|
|||
assert "_call_tool('greet', {'name': name})" in source
|
||||
|
||||
def test_optional_param(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="search",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"limit": {"type": "integer", "description": "Max results"},
|
||||
|
|
@ -178,9 +178,9 @@ class TestToolFunctionSource:
|
|||
assert "= None" in source
|
||||
|
||||
def test_param_with_default(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="fetch",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL"},
|
||||
"timeout": {
|
||||
|
|
@ -197,18 +197,18 @@ class TestToolFunctionSource:
|
|||
assert "= 30" in source
|
||||
|
||||
def test_no_params(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="ping",
|
||||
inputSchema={"properties": {}},
|
||||
input_schema={"properties": {}},
|
||||
)
|
||||
source = _tool_function_source(tool)
|
||||
assert "async def ping(" in source
|
||||
assert "_call_tool('ping', {})" in source
|
||||
|
||||
def test_preserves_underscores(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="get_forecast",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
|
|
@ -217,18 +217,18 @@ class TestToolFunctionSource:
|
|||
assert "async def get_forecast(" in source
|
||||
|
||||
def test_sanitizes_tool_name(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="my.tool/v2",
|
||||
inputSchema={"properties": {}},
|
||||
input_schema={"properties": {}},
|
||||
)
|
||||
source = _tool_function_source(tool)
|
||||
assert "async def my_tool_v2(" in source
|
||||
assert "name='my.tool/v2'" in source
|
||||
|
||||
def test_sanitizes_param_name(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="fetch",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {"content-type": {"type": "string", "description": "CT"}},
|
||||
"required": ["content-type"],
|
||||
},
|
||||
|
|
@ -238,10 +238,10 @@ class TestToolFunctionSource:
|
|||
assert "'content-type': content_type" in source
|
||||
|
||||
def test_description_in_docstring(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="greet",
|
||||
description="Say hello to someone.",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
|
|
@ -250,10 +250,10 @@ class TestToolFunctionSource:
|
|||
assert "'''Say hello to someone.'''" in source
|
||||
|
||||
def test_description_with_quotes(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="fetch",
|
||||
description="Fetch data from 'source' API.",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {"url": {"type": "string"}},
|
||||
"required": ["url"],
|
||||
},
|
||||
|
|
@ -265,10 +265,10 @@ class TestToolFunctionSource:
|
|||
compile(source, "<test>", "exec")
|
||||
|
||||
def test_array_of_strings_parameter(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="tag_items",
|
||||
description="Tag multiple items.",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"item_id": {"type": "string"},
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
|
|
@ -285,10 +285,10 @@ class TestToolFunctionSource:
|
|||
compile(source, "<test>", "exec")
|
||||
|
||||
def test_complex_object_parameter(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="create_user",
|
||||
description="Create a user.",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"metadata": {
|
||||
|
|
@ -318,10 +318,10 @@ class TestToolFunctionSource:
|
|||
compile(source, "<test>", "exec")
|
||||
|
||||
def test_nested_array_parameter(self):
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="batch_process",
|
||||
description="Process batches.",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"batches": {
|
||||
"type": "array",
|
||||
|
|
@ -346,9 +346,9 @@ class TestToolFunctionSource:
|
|||
|
||||
def test_complex_type_with_default(self):
|
||||
"""Test that complex types with defaults are JSON-serialized."""
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="configure",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
|
|
@ -367,9 +367,9 @@ class TestToolFunctionSource:
|
|||
|
||||
def test_name_collision_detection(self):
|
||||
"""Test that parameter name collisions are detected."""
|
||||
tool = mcp.types.Tool(
|
||||
tool = mcp_types.Tool(
|
||||
name="test",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"content-type": {"type": "string"},
|
||||
"content_type": {"type": "string"},
|
||||
|
|
@ -409,22 +409,22 @@ class TestDeriveServerName:
|
|||
|
||||
|
||||
class TestGenerateCliScript:
|
||||
def _make_tools(self) -> list[mcp.types.Tool]:
|
||||
def _make_tools(self) -> list[mcp_types.Tool]:
|
||||
return [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="greet",
|
||||
description="Say hello",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Who to greet"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
),
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="add_numbers",
|
||||
description="Add two numbers",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"a": {"type": "integer", "description": "First number"},
|
||||
"b": {"type": "integer", "description": "Second number"},
|
||||
|
|
@ -507,10 +507,10 @@ class TestGenerateCliScript:
|
|||
|
||||
def test_compiles_with_unusual_names(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="my.tool/v2",
|
||||
description="A tool with dots and slashes",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"properties": {
|
||||
"content-type": {"type": "string", "description": "CT"},
|
||||
},
|
||||
|
|
@ -771,10 +771,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_tool_sections(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="greet",
|
||||
description="Say hello",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Who to greet"}
|
||||
|
|
@ -794,9 +794,9 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_frontmatter_with_tools_starts_at_column_zero(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="greet",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
),
|
||||
]
|
||||
content = generate_skill_content("weather", "cli.py", tools)
|
||||
|
|
@ -804,10 +804,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_optional_param(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="search",
|
||||
description="Search things",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
|
|
@ -824,10 +824,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_complex_json_param(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="create",
|
||||
description="Create item",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
|
|
@ -844,10 +844,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_no_params_tool(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="ping",
|
||||
description="Ping the server",
|
||||
inputSchema={"type": "object", "properties": {}},
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
),
|
||||
]
|
||||
content = generate_skill_content("test", "cli.py", tools)
|
||||
|
|
@ -863,10 +863,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_pipe_in_description_escaped(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="test",
|
||||
description="Test",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {"type": "string", "description": "a|b|c"},
|
||||
|
|
@ -879,10 +879,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_union_type_pipes_escaped(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="test",
|
||||
description="Test",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"val": {"type": ["string", "null"]},
|
||||
|
|
@ -896,10 +896,10 @@ class TestGenerateSkillContent:
|
|||
|
||||
def test_boolean_param_no_value_placeholder(self):
|
||||
tools = [
|
||||
mcp.types.Tool(
|
||||
mcp_types.Tool(
|
||||
name="run",
|
||||
description="Run something",
|
||||
inputSchema={
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"verbose": {"type": "boolean", "description": "Verbose output"},
|
||||
|
|
|
|||
|
|
@ -296,14 +296,14 @@ class TestV1ServerAsync:
|
|||
"""Test that v1 server uses async stdio method."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -320,7 +320,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_stdio_async", new_callable=AsyncMock
|
||||
SDKServer, "run_stdio_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(str(test_file), transport="stdio")
|
||||
run_mock.assert_called_once()
|
||||
|
|
@ -329,14 +329,14 @@ async def async_echo(text: str) -> str:
|
|||
"""Test that v1 server uses async http method."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock
|
||||
SDKServer, "run_streamable_http_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(str(test_file), transport="http")
|
||||
run_mock.assert_called_once()
|
||||
|
|
@ -362,14 +362,14 @@ async def async_echo(text: str) -> str:
|
|||
"""Test that v1 server uses async streamable-http method."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -386,7 +386,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock
|
||||
SDKServer, "run_streamable_http_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(str(test_file), transport="streamable-http")
|
||||
run_mock.assert_called_once()
|
||||
|
|
@ -395,14 +395,14 @@ async def async_echo(text: str) -> str:
|
|||
"""Test that v1 server uses async sse method."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_sse_async", new_callable=AsyncMock
|
||||
SDKServer, "run_sse_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(str(test_file), transport="sse")
|
||||
run_mock.assert_called_once()
|
||||
|
|
@ -428,14 +428,14 @@ async def async_echo(text: str) -> str:
|
|||
"""Test that v1 server uses streamable-http by default."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -452,7 +452,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock
|
||||
SDKServer, "run_streamable_http_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(str(test_file))
|
||||
run_mock.assert_called_once()
|
||||
|
|
@ -461,14 +461,14 @@ async def async_echo(text: str) -> str:
|
|||
"""Test that v1 server receives host/port settings."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from mcp.server.mcpserver import MCPServer as SDKServer
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
|
||||
# Create a v1 FastMCP server file with both sync and async tools
|
||||
test_file = tmp_path / "v1_server.py"
|
||||
test_file.write_text("""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.mcpserver import MCPServer as FastMCP
|
||||
|
||||
mcp = FastMCP("V1Server")
|
||||
|
||||
|
|
@ -485,7 +485,7 @@ async def async_echo(text: str) -> str:
|
|||
|
||||
# Mock the async run method
|
||||
with patch.object(
|
||||
FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock
|
||||
SDKServer, "run_streamable_http_async", new_callable=AsyncMock
|
||||
) as run_mock:
|
||||
await run_command(
|
||||
str(test_file), transport="http", host="0.0.0.0", port=9000
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue