mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 05:54:19 +02:00
Validate version metadata to reject non-scalar types (#3437)
* Validate version metadata to reject non-scalar types (#3422) 🤖 Generated with Claude Code * Reject bool values in version coercion
This commit is contained in:
parent
fadb630142
commit
9ec4e7ae1b
3 changed files with 68 additions and 5 deletions
|
|
@ -79,7 +79,7 @@ from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting
|
|||
from fastmcp.tools.function_tool import FunctionTool
|
||||
from fastmcp.tools.tool import Tool, ToolResult
|
||||
from fastmcp.tools.tool_transform import ToolTransformConfig
|
||||
from fastmcp.utilities.components import FastMCPComponent
|
||||
from fastmcp.utilities.components import FastMCPComponent, _coerce_version
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import FastMCPBaseModel, NotSet, NotSetT
|
||||
from fastmcp.utilities.versions import (
|
||||
|
|
@ -221,7 +221,7 @@ class FastMCP(
|
|||
name: str | None = None,
|
||||
instructions: str | None = None,
|
||||
*,
|
||||
version: str | None = None,
|
||||
version: str | int | float | None = None,
|
||||
website_url: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
auth: AuthProvider | None = None,
|
||||
|
|
@ -308,7 +308,7 @@ class FastMCP(
|
|||
](
|
||||
fastmcp=self,
|
||||
name=name or self.generate_name(),
|
||||
version=version or fastmcp.__version__,
|
||||
version=_coerce_version(version) or fastmcp.__version__,
|
||||
instructions=instructions,
|
||||
website_url=website_url,
|
||||
icons=icons,
|
||||
|
|
|
|||
|
|
@ -43,13 +43,20 @@ def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
|
|||
return set(maybe_set)
|
||||
|
||||
|
||||
def _coerce_version(v: str | int | None) -> str | None:
|
||||
"""Coerce version to string, accepting int or str.
|
||||
def _coerce_version(v: str | int | float | None) -> str | None:
|
||||
"""Coerce version to string, accepting int, float, or str.
|
||||
|
||||
Raises TypeError for non-scalar types (list, dict, set, etc.).
|
||||
Raises ValueError if version contains '@' (used as key delimiter).
|
||||
"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, bool):
|
||||
raise TypeError(f"Version must be a string, int, or float, got bool: {v!r}")
|
||||
if not isinstance(v, (str, int, float)):
|
||||
raise TypeError(
|
||||
f"Version must be a string, int, or float, got {type(v).__name__}: {v!r}"
|
||||
)
|
||||
version = str(v)
|
||||
if "@" in version:
|
||||
raise ValueError(
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from mcp.types import TextContent
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools import Tool
|
||||
from fastmcp.utilities.versions import (
|
||||
VersionKey,
|
||||
compare_versions,
|
||||
|
|
@ -256,3 +260,55 @@ class TestComponentVersioning:
|
|||
prompt = await mcp.get_prompt("greet")
|
||||
assert prompt is not None
|
||||
assert prompt.version == "2.0"
|
||||
|
||||
|
||||
class TestVersionValidation:
|
||||
"""Tests for version type validation in components and server."""
|
||||
|
||||
async def test_fastmcp_version_int_coerced(self):
|
||||
"""FastMCP(version=42) should coerce to string '42'."""
|
||||
mcp = FastMCP(version=42)
|
||||
assert mcp._mcp_server.version == "42"
|
||||
|
||||
async def test_fastmcp_version_float_coerced(self):
|
||||
"""FastMCP(version=1.5) should coerce to string."""
|
||||
mcp = FastMCP(version=1.5)
|
||||
assert mcp._mcp_server.version == "1.5"
|
||||
|
||||
async def test_tool_version_list_rejected(self):
|
||||
"""Tool with version=[1, 2] should raise TypeError."""
|
||||
with pytest.raises(TypeError, match="Version must be a string"):
|
||||
Tool(
|
||||
name="t",
|
||||
version=cast(str, [1, 2]),
|
||||
parameters={"type": "object"},
|
||||
)
|
||||
|
||||
async def test_tool_version_dict_rejected(self):
|
||||
"""Tool with version={'major': 1} should raise TypeError."""
|
||||
with pytest.raises(TypeError, match="Version must be a string"):
|
||||
Tool(
|
||||
name="t",
|
||||
version=cast(str, {"major": 1}),
|
||||
parameters={"type": "object"},
|
||||
)
|
||||
|
||||
async def test_fastmcp_version_list_rejected(self):
|
||||
"""FastMCP(version=[1, 2]) should raise TypeError."""
|
||||
with pytest.raises(TypeError, match="Version must be a string"):
|
||||
FastMCP(version=cast(str, [1, 2]))
|
||||
|
||||
async def test_fastmcp_version_dict_rejected(self):
|
||||
"""FastMCP(version={'v': 1}) should raise TypeError."""
|
||||
with pytest.raises(TypeError, match="Version must be a string"):
|
||||
FastMCP(version=cast(str, {"v": 1}))
|
||||
|
||||
async def test_fastmcp_version_true_rejected(self):
|
||||
"""FastMCP(version=True) should raise TypeError, not coerce to 'True'."""
|
||||
with pytest.raises(TypeError, match="got bool"):
|
||||
FastMCP(version=cast(str, True))
|
||||
|
||||
async def test_fastmcp_version_false_rejected(self):
|
||||
"""FastMCP(version=False) should raise TypeError, not coerce to 'False'."""
|
||||
with pytest.raises(TypeError, match="got bool"):
|
||||
FastMCP(version=cast(str, False))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue