mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
fix: add version to components (#3458)
This commit is contained in:
parent
30308332bb
commit
f08205789e
3 changed files with 241 additions and 1 deletions
|
|
@ -224,6 +224,7 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
|
|||
tool = Tool.from_function(
|
||||
obj,
|
||||
name=meta.name,
|
||||
version=meta.version,
|
||||
title=meta.title,
|
||||
description=meta.description,
|
||||
icons=meta.icons,
|
||||
|
|
@ -248,6 +249,7 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
|
|||
fn=obj,
|
||||
uri_template=meta.uri,
|
||||
name=meta.name,
|
||||
version=meta.version,
|
||||
title=meta.title,
|
||||
description=meta.description,
|
||||
icons=meta.icons,
|
||||
|
|
@ -263,6 +265,7 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
|
|||
fn=obj,
|
||||
uri=meta.uri,
|
||||
name=meta.name,
|
||||
version=meta.version,
|
||||
title=meta.title,
|
||||
description=meta.description,
|
||||
icons=meta.icons,
|
||||
|
|
@ -279,6 +282,7 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
|
|||
prompt = Prompt.from_function(
|
||||
obj,
|
||||
name=meta.name,
|
||||
version=meta.version,
|
||||
title=meta.title,
|
||||
description=meta.description,
|
||||
icons=meta.icons,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.resources.template import FunctionResourceTemplate
|
||||
from fastmcp.prompts.prompt import Prompt
|
||||
from fastmcp.resources.resource import Resource
|
||||
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
|
||||
from fastmcp.server.providers.filesystem_discovery import (
|
||||
discover_and_import,
|
||||
discover_files,
|
||||
|
|
@ -10,6 +12,7 @@ from fastmcp.server.providers.filesystem_discovery import (
|
|||
import_module_from_file,
|
||||
)
|
||||
from fastmcp.tools import FunctionTool
|
||||
from fastmcp.tools.tool import Tool
|
||||
|
||||
|
||||
class TestDiscoverFiles:
|
||||
|
|
@ -351,3 +354,134 @@ def bad_function():
|
|||
failed_path = tmp_path / "bad.py"
|
||||
assert failed_path in result.failed_files
|
||||
assert "nonexistent_module_xyz123" in result.failed_files[failed_path]
|
||||
|
||||
|
||||
class TestExtractComponentsVersion:
|
||||
"""Tests for version propagation in extract_components."""
|
||||
|
||||
def test_extract_tool_preserves_version(self, tmp_path: Path):
|
||||
"""Tools discovered from files should have their version attribute set."""
|
||||
tool_file = tmp_path / "versioned_tool.py"
|
||||
tool_file.write_text(
|
||||
"""\
|
||||
from fastmcp.tools import tool
|
||||
|
||||
@tool(version="1.0", description="v1")
|
||||
def greet_v1(name: str) -> str:
|
||||
return f"Hi {name}"
|
||||
|
||||
@tool(version="2.0", description="v2")
|
||||
def greet_v2(name: str) -> str:
|
||||
return f"Hey {name}"
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(tool_file)
|
||||
components = extract_components(module)
|
||||
|
||||
tools = [c for c in components if isinstance(c, Tool)]
|
||||
assert len(tools) == 2
|
||||
|
||||
versions = {t.version for t in tools}
|
||||
assert versions == {"1.0", "2.0"}
|
||||
|
||||
def test_extract_resource_preserves_version(self, tmp_path: Path):
|
||||
"""Resources discovered from files should have their version attribute set."""
|
||||
resource_file = tmp_path / "versioned_resource.py"
|
||||
resource_file.write_text(
|
||||
"""\
|
||||
from fastmcp.resources import resource
|
||||
|
||||
@resource("data://config", version="1.0", name="config", description="v1 config")
|
||||
def config_v1() -> str:
|
||||
return '{"theme": "light"}'
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(resource_file)
|
||||
components = extract_components(module)
|
||||
|
||||
resources = [c for c in components if isinstance(c, Resource)]
|
||||
assert len(resources) == 1
|
||||
assert resources[0].version == "1.0"
|
||||
|
||||
def test_extract_resource_template_preserves_version(self, tmp_path: Path):
|
||||
"""Resource templates discovered from files should have their version set."""
|
||||
template_file = tmp_path / "versioned_template.py"
|
||||
template_file.write_text(
|
||||
"""\
|
||||
from fastmcp.resources import resource
|
||||
|
||||
@resource("users://{user_id}/profile", version="2.0", description="v2 profile")
|
||||
def get_profile(user_id: str) -> dict:
|
||||
return {"id": user_id}
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(template_file)
|
||||
components = extract_components(module)
|
||||
|
||||
templates = [c for c in components if isinstance(c, ResourceTemplate)]
|
||||
assert len(templates) == 1
|
||||
assert templates[0].version == "2.0"
|
||||
|
||||
def test_extract_prompt_preserves_version(self, tmp_path: Path):
|
||||
"""Prompts discovered from files should have their version attribute set."""
|
||||
prompt_file = tmp_path / "versioned_prompt.py"
|
||||
prompt_file.write_text(
|
||||
"""\
|
||||
from fastmcp.prompts import prompt
|
||||
|
||||
@prompt(name="summarize", version="1.0", description="v1 prompt")
|
||||
def summarize_v1(text: str) -> str:
|
||||
return f"Summarize: {text}"
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(prompt_file)
|
||||
components = extract_components(module)
|
||||
|
||||
prompts = [c for c in components if isinstance(c, Prompt)]
|
||||
assert len(prompts) == 1
|
||||
assert prompts[0].version == "1.0"
|
||||
|
||||
def test_discovered_tool_meta_includes_version(self, tmp_path: Path):
|
||||
"""get_meta() should include version for tools discovered via filesystem."""
|
||||
tool_file = tmp_path / "meta_tool.py"
|
||||
tool_file.write_text(
|
||||
"""\
|
||||
from fastmcp.tools import tool
|
||||
|
||||
@tool(name="echo", version="3.0", description="Echo tool")
|
||||
def echo(msg: str) -> str:
|
||||
return msg
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(tool_file)
|
||||
components = extract_components(module)
|
||||
|
||||
tool = components[0]
|
||||
meta = tool.get_meta()
|
||||
assert meta["fastmcp"]["version"] == "3.0"
|
||||
|
||||
def test_unversioned_components_have_no_version(self, tmp_path: Path):
|
||||
"""Components without version should have version=None."""
|
||||
tool_file = tmp_path / "no_version_tool.py"
|
||||
tool_file.write_text(
|
||||
"""\
|
||||
from fastmcp.tools import tool
|
||||
|
||||
@tool(description="No version")
|
||||
def my_tool(x: str) -> str:
|
||||
return x
|
||||
"""
|
||||
)
|
||||
|
||||
module = import_module_from_file(tool_file)
|
||||
components = extract_components(module)
|
||||
|
||||
assert len(components) == 1
|
||||
assert components[0].version is None
|
||||
meta = components[0].get_meta()
|
||||
assert "version" not in meta["fastmcp"]
|
||||
|
|
|
|||
|
|
@ -419,3 +419,105 @@ def charge(amount: float) -> str:
|
|||
assert len(tools_list) == 2
|
||||
names = {t.name for t in tools_list}
|
||||
assert names == {"greet", "charge"}
|
||||
|
||||
|
||||
class TestFileSystemProviderVersioning:
|
||||
"""Tests for version propagation through FileSystemProvider."""
|
||||
|
||||
async def test_versioned_tool_via_provider(self, tmp_path: Path):
|
||||
"""FileSystemProvider should preserve tool version in list_tools output."""
|
||||
(tmp_path / "versioned.py").write_text(
|
||||
"""\
|
||||
from fastmcp.tools import tool
|
||||
|
||||
@tool(version="1.0", description="v1 greet")
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
"""
|
||||
)
|
||||
|
||||
provider = FileSystemProvider(tmp_path)
|
||||
mcp = FastMCP("TestServer", providers=[provider])
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "greet"
|
||||
meta = tools[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["version"] == "1.0"
|
||||
|
||||
async def test_versioned_resource_via_provider(self, tmp_path: Path):
|
||||
"""FileSystemProvider should preserve resource version."""
|
||||
(tmp_path / "versioned_resource.py").write_text(
|
||||
"""\
|
||||
from fastmcp.resources import resource
|
||||
|
||||
@resource("data://config", version="2.0", name="config", description="v2 config")
|
||||
def config() -> str:
|
||||
return '{"theme": "dark"}'
|
||||
"""
|
||||
)
|
||||
|
||||
provider = FileSystemProvider(tmp_path)
|
||||
mcp = FastMCP("TestServer", providers=[provider])
|
||||
|
||||
async with Client(mcp) as client:
|
||||
resources = await client.list_resources()
|
||||
assert len(resources) == 1
|
||||
assert resources[0].name == "config"
|
||||
meta = resources[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["version"] == "2.0"
|
||||
|
||||
async def test_versioned_prompt_via_provider(self, tmp_path: Path):
|
||||
"""FileSystemProvider should preserve prompt version."""
|
||||
(tmp_path / "versioned_prompt.py").write_text(
|
||||
"""\
|
||||
from fastmcp.prompts import prompt
|
||||
|
||||
@prompt(name="summarize", version="1.0", description="v1 prompt")
|
||||
def summarize(text: str) -> str:
|
||||
return f"Summarize: {text}"
|
||||
"""
|
||||
)
|
||||
|
||||
provider = FileSystemProvider(tmp_path)
|
||||
mcp = FastMCP("TestServer", providers=[provider])
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
assert len(prompts) == 1
|
||||
assert prompts[0].name == "summarize"
|
||||
meta = prompts[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["version"] == "1.0"
|
||||
|
||||
async def test_multiple_tool_versions_via_provider(self, tmp_path: Path):
|
||||
"""FileSystemProvider should handle multiple versions of the same tool."""
|
||||
(tmp_path / "multi_version.py").write_text(
|
||||
"""\
|
||||
from fastmcp.tools import tool
|
||||
|
||||
@tool(name="add", version="1.0", description="v1 add")
|
||||
def add_v1(x: int, y: int) -> int:
|
||||
return x + y
|
||||
|
||||
@tool(name="add", version="2.0", description="v2 add with z")
|
||||
def add_v2(x: int, y: int, z: int = 0) -> int:
|
||||
return x + y + z
|
||||
"""
|
||||
)
|
||||
|
||||
provider = FileSystemProvider(tmp_path)
|
||||
mcp = FastMCP("TestServer", providers=[provider])
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
add_tools = [t for t in tools if t.name == "add"]
|
||||
# list_tools deduplicates to the highest version
|
||||
assert len(add_tools) == 1
|
||||
meta = add_tools[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["version"] == "2.0"
|
||||
assert meta["fastmcp"]["versions"] == ["2.0", "1.0"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue