mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
1438 lines
49 KiB
Python
1438 lines
49 KiB
Python
import base64
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from mcp import McpError
|
|
from mcp.types import (
|
|
BlobResourceContents,
|
|
EmbeddedResource,
|
|
ImageContent,
|
|
TextContent,
|
|
TextResourceContents,
|
|
)
|
|
from pydantic import AnyUrl, BaseModel
|
|
from typing_extensions import TypedDict
|
|
|
|
from fastmcp import Client, FastMCP
|
|
from fastmcp.client.transports import FastMCPTransport
|
|
from fastmcp.exceptions import ToolError
|
|
from fastmcp.prompts.prompt import PromptMessage, PromptResult
|
|
from fastmcp.resources import FileResource
|
|
from fastmcp.resources.resource import FunctionResource
|
|
from fastmcp.utilities.tests import temporary_settings
|
|
from fastmcp.utilities.types import Audio, File, Image
|
|
|
|
|
|
def _normalize_anyof_order(schema):
|
|
"""Normalize the order of items in anyOf arrays for consistent comparison."""
|
|
if isinstance(schema, dict):
|
|
if "anyOf" in schema:
|
|
# Sort anyOf items by their string representation for consistent ordering
|
|
schema = schema.copy()
|
|
schema["anyOf"] = sorted(schema["anyOf"], key=str)
|
|
# Recursively normalize nested objects
|
|
return {k: _normalize_anyof_order(v) for k, v in schema.items()}
|
|
elif isinstance(schema, list):
|
|
return [_normalize_anyof_order(item) for item in schema]
|
|
return schema
|
|
|
|
|
|
class PersonTypedDict(TypedDict):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
class PersonModel(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
@dataclass
|
|
class PersonDataclass:
|
|
name: str
|
|
age: int
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_server():
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def add(x: int, y: int) -> int:
|
|
return x + y
|
|
|
|
@mcp.tool
|
|
def list_tool() -> list[str | int]:
|
|
return ["x", 2]
|
|
|
|
@mcp.tool
|
|
def error_tool() -> None:
|
|
raise ValueError("Test error")
|
|
|
|
@mcp.tool
|
|
def image_tool(path: str) -> Image:
|
|
return Image(path)
|
|
|
|
@mcp.tool
|
|
def audio_tool(path: str) -> Audio:
|
|
return Audio(path)
|
|
|
|
@mcp.tool
|
|
def file_tool(path: str) -> File:
|
|
return File(path)
|
|
|
|
@mcp.tool
|
|
def mixed_content_tool() -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
return [
|
|
TextContent(type="text", text="Hello"),
|
|
ImageContent(type="image", data="abc", mimeType="application/octet-stream"),
|
|
EmbeddedResource(
|
|
type="resource",
|
|
resource=BlobResourceContents(
|
|
blob=base64.b64encode(b"abc").decode(),
|
|
mimeType="application/octet-stream",
|
|
uri=AnyUrl("file:///test.bin"),
|
|
),
|
|
),
|
|
]
|
|
|
|
@mcp.tool(output_schema=None)
|
|
def mixed_list_fn(image_path: str) -> list:
|
|
return [
|
|
"text message",
|
|
Image(image_path),
|
|
{"key": "value"},
|
|
TextContent(type="text", text="direct content"),
|
|
]
|
|
|
|
@mcp.tool(output_schema=None)
|
|
def mixed_audio_list_fn(audio_path: str) -> list:
|
|
return [
|
|
"text message",
|
|
Audio(audio_path),
|
|
{"key": "value"},
|
|
TextContent(type="text", text="direct content"),
|
|
]
|
|
|
|
@mcp.tool(output_schema=None)
|
|
def mixed_file_list_fn(file_path: str) -> list:
|
|
return [
|
|
"text message",
|
|
File(file_path),
|
|
{"key": "value"},
|
|
TextContent(type="text", text="direct content"),
|
|
]
|
|
|
|
@mcp.tool
|
|
def file_text_tool() -> File:
|
|
# Return a File with text data and text/plain format
|
|
return File(data=b"hello world", format="plain")
|
|
|
|
return mcp
|
|
|
|
|
|
class TestTools:
|
|
async def test_add_tool_exists(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
tools = await client.list_tools()
|
|
assert "add" in [t.name for t in tools]
|
|
|
|
async def test_list_tools(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
assert len(await client.list_tools()) == 11
|
|
|
|
async def test_call_tool_mcp(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
result = await client.call_tool_mcp("add", {"x": 1, "y": 2})
|
|
assert isinstance(result.content[0], TextContent)
|
|
assert result.content[0].text == "3"
|
|
assert result.structuredContent == {"result": 3}
|
|
|
|
async def test_call_tool(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
result = await client.call_tool("add", {"x": 1, "y": 2})
|
|
assert isinstance(result.content[0], TextContent)
|
|
assert result.content[0].text == "3"
|
|
assert result.structured_content == {"result": 3}
|
|
assert result.data == 3
|
|
|
|
async def test_call_tool_error(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
with pytest.raises(Exception):
|
|
await client.call_tool("error_tool", {})
|
|
|
|
async def test_call_tool_error_as_client_raw(self):
|
|
"""Test raising and catching errors from a tool."""
|
|
mcp = FastMCP()
|
|
client = Client(transport=FastMCPTransport(mcp))
|
|
|
|
@mcp.tool
|
|
def error_tool():
|
|
raise ValueError("Test error")
|
|
|
|
async with client:
|
|
with pytest.raises(Exception) as excinfo:
|
|
await client.call_tool("error_tool", {})
|
|
assert "Error calling tool 'error_tool'" in str(excinfo.value)
|
|
|
|
async def test_tool_returns_list(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
result = await client.call_tool("list_tool", {})
|
|
# Adjacent non-MCP list items are combined into single content block
|
|
assert len(result.content) == 1
|
|
assert isinstance(result.content[0], TextContent)
|
|
assert result.content[0].text == '["x",2]'
|
|
assert result.data == ["x", 2]
|
|
|
|
async def test_file_text_tool(self, tool_server: FastMCP):
|
|
async with Client(tool_server) as client:
|
|
result = await client.call_tool("file_text_tool", {})
|
|
assert len(result.content) == 1
|
|
embedded = result.content[0]
|
|
assert isinstance(embedded, EmbeddedResource)
|
|
resource = embedded.resource
|
|
assert isinstance(resource, TextResourceContents)
|
|
assert resource.mimeType == "text/plain"
|
|
assert resource.text == "hello world"
|
|
|
|
|
|
class TestToolTags:
|
|
def create_server(self, include_tags=None, exclude_tags=None):
|
|
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
|
|
|
@mcp.tool(tags={"a", "b"})
|
|
def tool_1() -> int:
|
|
return 1
|
|
|
|
@mcp.tool(tags={"b", "c"})
|
|
def tool_2() -> int:
|
|
return 2
|
|
|
|
return mcp
|
|
|
|
async def test_include_tags_all_tools(self):
|
|
mcp = self.create_server(include_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert {t.name for t in tools} == {"tool_1", "tool_2"}
|
|
|
|
async def test_include_tags_some_tools(self):
|
|
mcp = self.create_server(include_tags={"a", "z"})
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert {t.name for t in tools} == {"tool_1"}
|
|
|
|
async def test_exclude_tags_all_tools(self):
|
|
mcp = self.create_server(exclude_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert {t.name for t in tools} == set()
|
|
|
|
async def test_exclude_tags_some_tools(self):
|
|
mcp = self.create_server(exclude_tags={"a", "z"})
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert {t.name for t in tools} == {"tool_2"}
|
|
|
|
async def test_exclude_precedence(self):
|
|
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert {t.name for t in tools} == {"tool_2"}
|
|
|
|
async def test_call_included_tool(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
result_1 = await client.call_tool("tool_1", {})
|
|
assert result_1.data == 1
|
|
|
|
with pytest.raises(ToolError, match="Unknown tool"):
|
|
await client.call_tool("tool_2", {})
|
|
|
|
async def test_call_excluded_tool(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(ToolError, match="Unknown tool"):
|
|
await client.call_tool("tool_1", {})
|
|
|
|
result_2 = await client.call_tool("tool_2", {})
|
|
assert result_2.data == 2
|
|
|
|
|
|
class TestToolEnabled:
|
|
async def test_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
# Tool is enabled by default
|
|
tools = await mcp.get_tools()
|
|
assert any(t.name == "sample_tool" for t in tools)
|
|
|
|
# Disable via server
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
|
|
# Tool should not be in list when disabled
|
|
tools = await mcp.get_tools()
|
|
assert not any(t.name == "sample_tool" for t in tools)
|
|
|
|
# Re-enable via server
|
|
mcp.enable(keys=["tool:sample_tool"])
|
|
tools = await mcp.get_tools()
|
|
assert any(t.name == "sample_tool" for t in tools)
|
|
|
|
async def test_tool_disabled_via_server(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
# Disable the tool via server
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert len(tools) == 0
|
|
|
|
with pytest.raises(ToolError, match="Unknown tool"):
|
|
await client.call_tool("sample_tool", {"x": 5})
|
|
|
|
async def test_tool_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
# Disable then re-enable
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
mcp.enable(keys=["tool:sample_tool"])
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert len(tools) == 1
|
|
|
|
async def test_tool_toggle_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
assert len(tools) == 0
|
|
|
|
with pytest.raises(ToolError, match="Unknown tool"):
|
|
await client.call_tool("sample_tool", {"x": 5})
|
|
|
|
async def test_get_tool_and_disable(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
tool = await mcp.get_tool(name="sample_tool")
|
|
assert tool is not None
|
|
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.list_tools()
|
|
assert len(result) == 0
|
|
|
|
with pytest.raises(ToolError, match="Unknown tool"):
|
|
await client.call_tool("sample_tool", {"x": 5})
|
|
|
|
async def test_cant_call_disabled_tool(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
mcp.disable(keys=["tool:sample_tool"])
|
|
|
|
with pytest.raises(Exception, match="Unknown tool"):
|
|
async with Client(mcp) as client:
|
|
await client.call_tool("sample_tool", {"x": 5})
|
|
|
|
|
|
class TestResource:
|
|
async def test_text_resource(self):
|
|
mcp = FastMCP()
|
|
|
|
def get_text():
|
|
return "Hello, world!"
|
|
|
|
resource = FunctionResource(
|
|
uri=AnyUrl("resource://test"), name="test", fn=get_text
|
|
)
|
|
mcp.add_resource(resource)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource(AnyUrl("resource://test"))
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert result[0].text == "Hello, world!"
|
|
|
|
async def test_binary_resource(self):
|
|
mcp = FastMCP()
|
|
|
|
def get_binary():
|
|
return b"Binary data"
|
|
|
|
resource = FunctionResource(
|
|
uri=AnyUrl("resource://binary"),
|
|
name="binary",
|
|
fn=get_binary,
|
|
mime_type="application/octet-stream",
|
|
)
|
|
mcp.add_resource(resource)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource(AnyUrl("resource://binary"))
|
|
assert isinstance(result[0], BlobResourceContents)
|
|
assert result[0].blob == base64.b64encode(b"Binary data").decode()
|
|
|
|
async def test_file_resource_text(self, tmp_path: Path):
|
|
mcp = FastMCP()
|
|
|
|
# Create a text file
|
|
text_file = tmp_path / "test.txt"
|
|
text_file.write_text("Hello from file!")
|
|
|
|
resource = FileResource(
|
|
uri=AnyUrl("file://test.txt"), name="test.txt", path=text_file
|
|
)
|
|
mcp.add_resource(resource)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource(AnyUrl("file://test.txt"))
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert result[0].text == "Hello from file!"
|
|
|
|
async def test_file_resource_binary(self, tmp_path: Path):
|
|
mcp = FastMCP()
|
|
|
|
# Create a binary file
|
|
binary_file = tmp_path / "test.bin"
|
|
binary_file.write_bytes(b"Binary file data")
|
|
|
|
resource = FileResource(
|
|
uri=AnyUrl("file://test.bin"),
|
|
name="test.bin",
|
|
path=binary_file,
|
|
mime_type="application/octet-stream",
|
|
)
|
|
mcp.add_resource(resource)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource(AnyUrl("file://test.bin"))
|
|
assert isinstance(result[0], BlobResourceContents)
|
|
assert result[0].blob == base64.b64encode(b"Binary file data").decode()
|
|
|
|
async def test_resource_with_annotations(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource(
|
|
"http://example.com/data",
|
|
name="test",
|
|
annotations={
|
|
"httpMethod": "GET",
|
|
"Cache-Control": "max-age=3600",
|
|
},
|
|
)
|
|
def get_data() -> str:
|
|
return "Hello, world!"
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert len(resources) == 1
|
|
|
|
resource = resources[0]
|
|
assert str(resource.uri) == "http://example.com/data"
|
|
|
|
assert resource.annotations is not None
|
|
assert hasattr(resource.annotations, "httpMethod")
|
|
assert getattr(resource.annotations, "httpMethod") == "GET"
|
|
assert hasattr(resource.annotations, "Cache-Control")
|
|
assert getattr(resource.annotations, "Cache-Control") == "max-age=3600"
|
|
|
|
|
|
class TestResourceTags:
|
|
def create_server(self, include_tags=None, exclude_tags=None):
|
|
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
|
|
|
@mcp.resource("resource://1", tags={"a", "b"})
|
|
def resource_1() -> str:
|
|
return "1"
|
|
|
|
@mcp.resource("resource://2", tags={"b", "c"})
|
|
def resource_2() -> str:
|
|
return "2"
|
|
|
|
return mcp
|
|
|
|
async def test_include_tags_all_resources(self):
|
|
mcp = self.create_server(include_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert {r.name for r in resources} == {"resource_1", "resource_2"}
|
|
|
|
async def test_include_tags_some_resources(self):
|
|
mcp = self.create_server(include_tags={"a", "z"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert {r.name for r in resources} == {"resource_1"}
|
|
|
|
async def test_exclude_tags_all_resources(self):
|
|
mcp = self.create_server(exclude_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert {r.name for r in resources} == set()
|
|
|
|
async def test_exclude_tags_some_resources(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert {r.name for r in resources} == {"resource_2"}
|
|
|
|
async def test_exclude_precedence(self):
|
|
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert {r.name for r in resources} == {"resource_2"}
|
|
|
|
async def test_read_included_resource(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource(AnyUrl("resource://1"))
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert result[0].text == "1"
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://2"))
|
|
|
|
async def test_read_excluded_resource(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://1"))
|
|
|
|
|
|
class TestResourceEnabled:
|
|
async def test_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
resources = await mcp.get_resources()
|
|
assert any(str(r.uri) == "resource://data" for r in resources)
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
resources = await mcp.get_resources()
|
|
assert not any(str(r.uri) == "resource://data" for r in resources)
|
|
|
|
mcp.enable(keys=["resource:resource://data"])
|
|
|
|
resources = await mcp.get_resources()
|
|
assert any(str(r.uri) == "resource://data" for r in resources)
|
|
|
|
async def test_resource_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert len(resources) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://data"))
|
|
|
|
async def test_resource_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
resources = await mcp.get_resources()
|
|
assert not any(str(r.uri) == "resource://data" for r in resources)
|
|
|
|
mcp.enable(keys=["resource:resource://data"])
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert len(resources) == 1
|
|
|
|
async def test_resource_toggle_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
assert len(resources) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://data"))
|
|
|
|
async def test_get_resource_and_disable(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
resource = await mcp.get_resource("resource://data")
|
|
assert resource is not None
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.list_resources()
|
|
assert len(result) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://data"))
|
|
|
|
async def test_cant_read_disabled_resource(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data")
|
|
def sample_resource() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["resource:resource://data"])
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
async with Client(mcp) as client:
|
|
await client.read_resource(AnyUrl("resource://data"))
|
|
|
|
|
|
class TestResourceTemplatesTags:
|
|
def create_server(self, include_tags=None, exclude_tags=None):
|
|
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
|
|
|
@mcp.resource("resource://1/{param}", tags={"a", "b"})
|
|
def template_resource_1(param: str) -> str:
|
|
return f"Template resource 1: {param}"
|
|
|
|
@mcp.resource("resource://2/{param}", tags={"b", "c"})
|
|
def template_resource_2(param: str) -> str:
|
|
return f"Template resource 2: {param}"
|
|
|
|
return mcp
|
|
|
|
async def test_include_tags_all_resources(self):
|
|
mcp = self.create_server(include_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resource_templates()
|
|
assert {r.name for r in resources} == {
|
|
"template_resource_1",
|
|
"template_resource_2",
|
|
}
|
|
|
|
async def test_include_tags_some_resources(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resource_templates()
|
|
assert {r.name for r in resources} == {"template_resource_1"}
|
|
|
|
async def test_exclude_tags_all_resources(self):
|
|
mcp = self.create_server(exclude_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resource_templates()
|
|
assert {r.name for r in resources} == set()
|
|
|
|
async def test_exclude_tags_some_resources(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resource_templates()
|
|
assert {r.name for r in resources} == {"template_resource_2"}
|
|
|
|
async def test_exclude_takes_precedence_over_include(self):
|
|
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resource_templates()
|
|
assert {r.name for r in resources} == {"template_resource_2"}
|
|
|
|
async def test_read_resource_template_includes_tags(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.read_resource("resource://1/x")
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert result[0].text == "Template resource 1: x"
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource("resource://2/x")
|
|
|
|
async def test_read_resource_template_excludes_tags(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource("resource://1/x")
|
|
|
|
result = await client.read_resource("resource://2/x")
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert result[0].text == "Template resource 2: x"
|
|
|
|
|
|
class TestResourceTemplateEnabled:
|
|
async def test_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
templates = await mcp.get_resource_templates()
|
|
assert any(t.uri_template == "resource://{param}" for t in templates)
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
templates = await mcp.get_resource_templates()
|
|
assert not any(t.uri_template == "resource://{param}" for t in templates)
|
|
|
|
mcp.enable(keys=["template:resource://{param}"])
|
|
|
|
templates = await mcp.get_resource_templates()
|
|
assert any(t.uri_template == "resource://{param}" for t in templates)
|
|
|
|
async def test_template_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
async with Client(mcp) as client:
|
|
templates = await client.list_resource_templates()
|
|
assert len(templates) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://test"))
|
|
|
|
async def test_template_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
templates = await mcp.get_resource_templates()
|
|
assert not any(t.uri_template == "resource://{param}" for t in templates)
|
|
|
|
mcp.enable(keys=["template:resource://{param}"])
|
|
|
|
async with Client(mcp) as client:
|
|
templates = await client.list_resource_templates()
|
|
assert len(templates) == 1
|
|
|
|
async def test_template_toggle_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
async with Client(mcp) as client:
|
|
templates = await client.list_resource_templates()
|
|
assert len(templates) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://test"))
|
|
|
|
async def test_get_template_and_disable(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
template = await mcp.get_resource_template("resource://{param}")
|
|
assert template is not None
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.list_resource_templates()
|
|
assert len(result) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
await client.read_resource(AnyUrl("resource://test"))
|
|
|
|
async def test_cant_read_disabled_template(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}")
|
|
def sample_template(param: str) -> str:
|
|
return f"Template: {param}"
|
|
|
|
mcp.disable(keys=["template:resource://{param}"])
|
|
|
|
with pytest.raises(McpError, match="Unknown resource"):
|
|
async with Client(mcp) as client:
|
|
await client.read_resource(AnyUrl("resource://test"))
|
|
|
|
|
|
class TestPrompts:
|
|
"""Test prompt functionality in FastMCP server."""
|
|
|
|
async def test_prompt_decorator(self):
|
|
"""Test that the prompt decorator registers prompts correctly."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def fn() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = next(p for p in prompts if p.name == "fn")
|
|
assert prompt.name == "fn"
|
|
# Don't compare functions directly since validate_call wraps them
|
|
content = await prompt.render()
|
|
if not isinstance(content, PromptResult):
|
|
content = PromptResult.from_value(content)
|
|
assert isinstance(content.messages[0].content, TextContent)
|
|
assert content.messages[0].content.text == "Hello, world!"
|
|
|
|
async def test_prompt_decorator_with_name(self):
|
|
"""Test prompt decorator with custom name."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt(name="custom_name")
|
|
def fn() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = next(p for p in prompts if p.name == "custom_name")
|
|
assert prompt.name == "custom_name"
|
|
content = await prompt.render()
|
|
if not isinstance(content, PromptResult):
|
|
content = PromptResult.from_value(content)
|
|
assert isinstance(content.messages[0].content, TextContent)
|
|
assert content.messages[0].content.text == "Hello, world!"
|
|
|
|
async def test_prompt_decorator_with_description(self):
|
|
"""Test prompt decorator with custom description."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt(description="A custom description")
|
|
def fn() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = next(p for p in prompts if p.name == "fn")
|
|
assert prompt.description == "A custom description"
|
|
content = await prompt.render()
|
|
if not isinstance(content, PromptResult):
|
|
content = PromptResult.from_value(content)
|
|
assert isinstance(content.messages[0].content, TextContent)
|
|
assert content.messages[0].content.text == "Hello, world!"
|
|
|
|
async def test_prompt_decorator_with_parens(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def fn() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = next(p for p in prompts if p.name == "fn")
|
|
assert prompt.name == "fn"
|
|
|
|
async def test_list_prompts(self):
|
|
"""Test listing prompts through MCP protocol."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def fn(name: str, optional: str = "default") -> str:
|
|
return f"Hello, {name}! {optional}"
|
|
|
|
prompts_dict = await mcp.get_prompts()
|
|
assert len(prompts_dict) == 1
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert len(prompts) == 1
|
|
assert prompts[0].name == "fn"
|
|
assert prompts[0].description is None
|
|
assert prompts[0].arguments is not None
|
|
assert len(prompts[0].arguments) == 2
|
|
assert prompts[0].arguments[0].name == "name"
|
|
assert prompts[0].arguments[0].required is True
|
|
assert prompts[0].arguments[1].name == "optional"
|
|
assert prompts[0].arguments[1].required is False
|
|
|
|
async def test_list_prompts_with_enhanced_descriptions(self):
|
|
"""Test that enhanced descriptions with JSON schema are visible via MCP protocol."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def analyze_data(
|
|
name: str, numbers: list[int], metadata: dict[str, str], threshold: float
|
|
) -> str:
|
|
"""Analyze some data."""
|
|
return f"Analyzed {name}"
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = prompts[0]
|
|
assert prompt.name == "analyze_data"
|
|
assert prompt.description == "Analyze some data."
|
|
|
|
# Find each argument and verify schema enhancements
|
|
assert prompt.arguments is not None
|
|
args_by_name = {arg.name: arg for arg in prompt.arguments}
|
|
|
|
# String parameter should not have schema enhancement
|
|
name_arg = args_by_name["name"]
|
|
assert name_arg.description is None
|
|
|
|
# Non-string parameters should have schema enhancements
|
|
numbers_arg = args_by_name["numbers"]
|
|
assert numbers_arg.description is not None
|
|
assert (
|
|
"Provide as a JSON string matching the following schema:"
|
|
in numbers_arg.description
|
|
)
|
|
assert (
|
|
'{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description
|
|
)
|
|
|
|
metadata_arg = args_by_name["metadata"]
|
|
assert metadata_arg.description is not None
|
|
assert (
|
|
"Provide as a JSON string matching the following schema:"
|
|
in metadata_arg.description
|
|
)
|
|
assert (
|
|
'{"additionalProperties":{"type":"string"},"type":"object"}'
|
|
in metadata_arg.description
|
|
)
|
|
|
|
threshold_arg = args_by_name["threshold"]
|
|
assert threshold_arg.description is not None
|
|
assert (
|
|
"Provide as a JSON string matching the following schema:"
|
|
in threshold_arg.description
|
|
)
|
|
assert '{"type":"number"}' in threshold_arg.description
|
|
|
|
async def test_get_prompt(self):
|
|
"""Test getting a prompt through MCP protocol."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def fn(name: str) -> str:
|
|
return f"Hello, {name}!"
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.get_prompt("fn", {"name": "World"})
|
|
assert len(result.messages) == 1
|
|
message = result.messages[0]
|
|
assert message.role == "user"
|
|
content = message.content
|
|
assert isinstance(content, TextContent)
|
|
assert content.text == "Hello, World!"
|
|
|
|
async def test_get_prompt_with_resource(self):
|
|
"""Test getting a prompt that returns resource content."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def fn() -> PromptMessage:
|
|
return PromptMessage(
|
|
role="user",
|
|
content=EmbeddedResource(
|
|
type="resource",
|
|
resource=TextResourceContents(
|
|
uri=AnyUrl("file://file.txt"),
|
|
text="File contents",
|
|
mimeType="text/plain",
|
|
),
|
|
),
|
|
)
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.get_prompt("fn")
|
|
assert result.messages[0].role == "user"
|
|
content = result.messages[0].content
|
|
assert isinstance(content, EmbeddedResource)
|
|
assert isinstance(content.resource, TextResourceContents)
|
|
assert content.resource.text == "File contents"
|
|
assert content.resource.mimeType == "text/plain"
|
|
|
|
async def test_get_unknown_prompt(self):
|
|
"""Test error when getting unknown prompt."""
|
|
mcp = FastMCP()
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
async with Client(mcp) as client:
|
|
await client.get_prompt("unknown")
|
|
|
|
async def test_get_prompt_missing_args(self):
|
|
"""Test error when required arguments are missing."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def prompt_fn(name: str) -> str:
|
|
return f"Hello, {name}!"
|
|
|
|
with pytest.raises(McpError, match="Missing required arguments"):
|
|
async with Client(mcp) as client:
|
|
await client.get_prompt("prompt_fn")
|
|
|
|
async def test_resource_decorator_with_tags(self):
|
|
"""Test that the resource decorator supports tags."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://data", tags={"example", "test-tag"})
|
|
def get_data() -> str:
|
|
return "Hello, world!"
|
|
|
|
resources = await mcp.get_resources()
|
|
assert len(resources) == 1
|
|
assert resources[0].tags == {"example", "test-tag"}
|
|
|
|
async def test_template_decorator_with_tags(self):
|
|
"""Test that the template decorator properly sets tags."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource("resource://{param}", tags={"template", "test-tag"})
|
|
def template_resource(param: str) -> str:
|
|
return f"Template resource: {param}"
|
|
|
|
templates = await mcp.get_resource_templates()
|
|
template = next(t for t in templates if t.uri_template == "resource://{param}")
|
|
assert template.tags == {"template", "test-tag"}
|
|
|
|
async def test_prompt_decorator_with_tags(self):
|
|
"""Test that the prompt decorator properly sets tags."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt(tags={"example", "test-tag"})
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert len(prompts) == 1
|
|
prompt = next(p for p in prompts if p.name == "sample_prompt")
|
|
assert prompt.tags == {"example", "test-tag"}
|
|
|
|
|
|
class TestPromptEnabled:
|
|
async def test_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert any(p.name == "sample_prompt" for p in prompts)
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert not any(p.name == "sample_prompt" for p in prompts)
|
|
|
|
mcp.enable(keys=["prompt:sample_prompt"])
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert any(p.name == "sample_prompt" for p in prompts)
|
|
|
|
async def test_prompt_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert len(prompts) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
await client.get_prompt("sample_prompt")
|
|
|
|
async def test_prompt_toggle_enabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
prompts = await mcp.get_prompts()
|
|
assert not any(p.name == "sample_prompt" for p in prompts)
|
|
|
|
mcp.enable(keys=["prompt:sample_prompt"])
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert len(prompts) == 1
|
|
|
|
async def test_prompt_toggle_disabled(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert len(prompts) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
await client.get_prompt("sample_prompt")
|
|
|
|
async def test_get_prompt_and_disable(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
prompt = await mcp.get_prompt("sample_prompt")
|
|
assert prompt is not None
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.list_prompts()
|
|
assert len(result) == 0
|
|
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
await client.get_prompt("sample_prompt")
|
|
|
|
async def test_cant_get_disabled_prompt(self):
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
mcp.disable(keys=["prompt:sample_prompt"])
|
|
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
async with Client(mcp) as client:
|
|
await client.get_prompt("sample_prompt")
|
|
|
|
|
|
class TestPromptTags:
|
|
def create_server(self, include_tags=None, exclude_tags=None):
|
|
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
|
|
|
@mcp.prompt(tags={"a", "b"})
|
|
def prompt_1() -> str:
|
|
return "1"
|
|
|
|
@mcp.prompt(tags={"b", "c"})
|
|
def prompt_2() -> str:
|
|
return "2"
|
|
|
|
return mcp
|
|
|
|
async def test_include_tags_all_prompts(self):
|
|
mcp = self.create_server(include_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert {p.name for p in prompts} == {"prompt_1", "prompt_2"}
|
|
|
|
async def test_include_tags_some_prompts(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert {p.name for p in prompts} == {"prompt_1"}
|
|
|
|
async def test_exclude_tags_all_prompts(self):
|
|
mcp = self.create_server(exclude_tags={"a", "b"})
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert {p.name for p in prompts} == set()
|
|
|
|
async def test_exclude_tags_some_prompts(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert {p.name for p in prompts} == {"prompt_2"}
|
|
|
|
async def test_exclude_takes_precedence_over_include(self):
|
|
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
assert {p.name for p in prompts} == {"prompt_2"}
|
|
|
|
async def test_read_prompt_includes_tags(self):
|
|
mcp = self.create_server(include_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
result = await client.get_prompt("prompt_1")
|
|
assert isinstance(result.messages[0].content, TextContent)
|
|
assert result.messages[0].content.text == "1"
|
|
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
await client.get_prompt("prompt_2")
|
|
|
|
async def test_read_prompt_excludes_tags(self):
|
|
mcp = self.create_server(exclude_tags={"a"})
|
|
|
|
async with Client(mcp) as client:
|
|
with pytest.raises(McpError, match="Unknown prompt"):
|
|
await client.get_prompt("prompt_1")
|
|
|
|
result = await client.get_prompt("prompt_2")
|
|
assert isinstance(result.messages[0].content, TextContent)
|
|
assert result.messages[0].content.text == "2"
|
|
|
|
|
|
class TestMeta:
|
|
"""Test that include_fastmcp_meta controls whether _fastmcp key is present in meta."""
|
|
|
|
async def test_tool_tags_in_meta_with_default_setting(self):
|
|
"""Test that tool tags appear in meta under _fastmcp key with default setting."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool(tags={"tool-example", "test-tool-tag"})
|
|
def sample_tool(x: int) -> int:
|
|
"""A sample tool."""
|
|
return x * 2
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
tool = next(t for t in tools if t.name == "sample_tool")
|
|
assert tool.meta is not None
|
|
assert set(tool.meta["_fastmcp"]["tags"]) == {
|
|
"tool-example",
|
|
"test-tool-tag",
|
|
}
|
|
|
|
async def test_resource_tags_in_meta_with_default_setting(self):
|
|
"""Test that resource tags appear in meta under _fastmcp key with default setting."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource(
|
|
uri="test://resource", tags={"resource-example", "test-resource-tag"}
|
|
)
|
|
def sample_resource() -> str:
|
|
"""A sample resource."""
|
|
return "resource content"
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
resource = next(r for r in resources if str(r.uri) == "test://resource")
|
|
assert resource.meta is not None
|
|
assert set(resource.meta["_fastmcp"]["tags"]) == {
|
|
"resource-example",
|
|
"test-resource-tag",
|
|
}
|
|
|
|
async def test_resource_template_tags_in_meta_with_default_setting(self):
|
|
"""Test that resource template tags appear in meta under _fastmcp key with default setting."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.resource(
|
|
"test://template/{id}", tags={"template-example", "test-template-tag"}
|
|
)
|
|
def sample_template(id: str) -> str:
|
|
"""A sample resource template."""
|
|
return f"template content for {id}"
|
|
|
|
async with Client(mcp) as client:
|
|
templates = await client.list_resource_templates()
|
|
template = next(
|
|
t for t in templates if t.uriTemplate == "test://template/{id}"
|
|
)
|
|
assert template.meta is not None
|
|
assert set(template.meta["_fastmcp"]["tags"]) == {
|
|
"template-example",
|
|
"test-template-tag",
|
|
}
|
|
|
|
async def test_prompt_tags_in_meta_with_default_setting(self):
|
|
"""Test that prompt tags appear in meta under _fastmcp key with default setting."""
|
|
mcp = FastMCP()
|
|
|
|
@mcp.prompt(tags={"example", "test-tag"})
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
prompt = next(p for p in prompts if p.name == "sample_prompt")
|
|
assert prompt.meta is not None
|
|
assert set(prompt.meta["_fastmcp"]["tags"]) == {"example", "test-tag"}
|
|
|
|
async def test_tool_meta_with_include_fastmcp_meta_false(self):
|
|
mcp = FastMCP(include_fastmcp_meta=False)
|
|
|
|
@mcp.tool(tags={"tool-example", "test-tool-tag"})
|
|
def sample_tool(x: int) -> int:
|
|
"""A sample tool."""
|
|
return x * 2
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
tool = next(t for t in tools if t.name == "sample_tool")
|
|
# Meta should be None when include_fastmcp_meta is False and no explicit meta is set
|
|
assert tool.meta is None
|
|
|
|
async def test_resource_meta_with_include_fastmcp_meta_false(self):
|
|
mcp = FastMCP(include_fastmcp_meta=False)
|
|
|
|
@mcp.resource(
|
|
uri="test://resource", tags={"resource-example", "test-resource-tag"}
|
|
)
|
|
def sample_resource() -> str:
|
|
"""A sample resource."""
|
|
return "resource content"
|
|
|
|
async with Client(mcp) as client:
|
|
resources = await client.list_resources()
|
|
resource = next(r for r in resources if str(r.uri) == "test://resource")
|
|
# Meta should be None when include_fastmcp_meta is False and no explicit meta is set
|
|
assert resource.meta is None
|
|
|
|
async def test_resource_template_meta_with_include_fastmcp_meta_false(self):
|
|
mcp = FastMCP(include_fastmcp_meta=False)
|
|
|
|
@mcp.resource(
|
|
"test://template/{id}", tags={"template-example", "test-template-tag"}
|
|
)
|
|
def sample_template(id: str) -> str:
|
|
"""A sample resource template."""
|
|
return f"template content for {id}"
|
|
|
|
async with Client(mcp) as client:
|
|
templates = await client.list_resource_templates()
|
|
template = next(
|
|
t for t in templates if t.uriTemplate == "test://template/{id}"
|
|
)
|
|
# Meta should be None when include_fastmcp_meta is False and no explicit meta is set
|
|
assert template.meta is None
|
|
|
|
async def test_prompt_meta_with_include_fastmcp_meta_false(self):
|
|
mcp = FastMCP(include_fastmcp_meta=False)
|
|
|
|
@mcp.prompt(tags={"example", "test-tag"})
|
|
def sample_prompt() -> str:
|
|
return "Hello, world!"
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
prompt = next(p for p in prompts if p.name == "sample_prompt")
|
|
# Meta should be None when include_fastmcp_meta is False and no explicit meta is set
|
|
assert prompt.meta is None
|
|
|
|
async def test_global_settings_inheritance(self):
|
|
"""Test that servers inherit the global include_fastmcp_meta setting."""
|
|
with temporary_settings(include_fastmcp_meta=False):
|
|
# Server should inherit global setting
|
|
mcp = FastMCP()
|
|
|
|
@mcp.tool(tags={"test-tag"})
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
tool = next(t for t in tools if t.name == "sample_tool")
|
|
# Meta should be None because global setting is False
|
|
assert tool.meta is None
|
|
|
|
# Verify that default behavior is restored
|
|
mcp2 = FastMCP()
|
|
|
|
@mcp2.tool(tags={"test-tag"})
|
|
def another_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
async with Client(mcp2) as client:
|
|
tools = await client.list_tools()
|
|
tool = next(t for t in tools if t.name == "another_tool")
|
|
# Meta should have _fastmcp key because global setting is back to default (True)
|
|
assert tool.meta is not None
|
|
assert "_fastmcp" in tool.meta
|
|
assert tool.meta["_fastmcp"]["tags"] == ["test-tag"]
|
|
|
|
async def test_explicit_override_of_global_setting(self):
|
|
"""Test that explicit include_fastmcp_meta parameter overrides global setting."""
|
|
with temporary_settings(include_fastmcp_meta=False):
|
|
# Explicitly override global setting to True
|
|
mcp = FastMCP(include_fastmcp_meta=True)
|
|
|
|
@mcp.tool(tags={"test-tag"})
|
|
def sample_tool(x: int) -> int:
|
|
return x * 2
|
|
|
|
async with Client(mcp) as client:
|
|
tools = await client.list_tools()
|
|
tool = next(t for t in tools if t.name == "sample_tool")
|
|
# Meta should have _fastmcp key because explicit setting overrides global
|
|
assert tool.meta is not None
|
|
assert "_fastmcp" in tool.meta
|
|
assert tool.meta["_fastmcp"]["tags"] == ["test-tag"]
|