fastmcp/tests/server/test_server.py

1046 lines
36 KiB
Python

from typing import Annotated
import pytest
from mcp import McpError
from mcp.types import (
TextContent,
TextResourceContents,
)
from pydantic import Field
from fastmcp import Client, FastMCP
from fastmcp.exceptions import NotFoundError
from fastmcp.server.server import (
MountedServer,
add_resource_prefix,
has_resource_prefix,
remove_resource_prefix,
)
class TestCreateServer:
async def test_create_server(self):
mcp = FastMCP(instructions="Server instructions")
assert mcp.name == "FastMCP"
assert mcp.instructions == "Server instructions"
async def test_non_ascii_description(self):
"""Test that FastMCP handles non-ASCII characters in descriptions correctly"""
mcp = FastMCP()
@mcp.tool(
description=(
"🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉"
)
)
def hello_world(name: str = "世界") -> str:
return f"¡Hola, {name}! 👋"
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.description is not None
assert "🌟" in tool.description
assert "漢字" in tool.description
assert "🎉" in tool.description
result = await client.call_tool("hello_world", {})
assert len(result) == 1
content = result[0]
assert isinstance(content, TextContent)
assert "¡Hola, 世界! 👋" == content.text
class TestTools:
async def test_mcp_tool_name(self):
"""Test MCPTool name for add_tool (key != tool.name)."""
mcp = FastMCP()
@mcp.tool()
def fn(x: int) -> int:
return x + 1
mcp_tools = await mcp._mcp_list_tools()
assert len(mcp_tools) == 1
assert mcp_tools[0].name == "fn"
async def test_mcp_tool_custom_name(self):
"""Test MCPTool name for add_tool (key != tool.name)."""
mcp = FastMCP()
@mcp.tool(name="custom_name")
def fn(x: int) -> int:
return x + 1
mcp_tools = await mcp._mcp_list_tools()
assert len(mcp_tools) == 1
assert mcp_tools[0].name == "custom_name"
async def test_remove_tool_successfully(self):
"""Test that FastMCP.remove_tool removes the tool from the registry."""
mcp = FastMCP()
@mcp.tool(name="adder")
def add(a: int, b: int) -> int:
return a + b
mcp_tools = await mcp.get_tools()
assert "adder" in mcp_tools
mcp.remove_tool("adder")
mcp_tools = await mcp.get_tools()
assert "adder" not in mcp_tools
with pytest.raises(NotFoundError, match="Unknown tool: adder"):
await mcp._mcp_call_tool("adder", {"a": 1, "b": 2})
class TestToolDecorator:
async def test_no_tools_before_decorator(self):
mcp = FastMCP()
with pytest.raises(NotFoundError, match="Unknown tool: add"):
await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
async def test_tool_decorator(self):
mcp = FastMCP()
@mcp.tool()
def add(x: int, y: int) -> int:
return x + y
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "3"
async def test_tool_decorator_incorrect_usage(self):
mcp = FastMCP()
with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"):
@mcp.tool # Missing parentheses #type: ignore
def add(x: int, y: int) -> int:
return x + y
async def test_tool_decorator_with_name(self):
mcp = FastMCP()
@mcp.tool(name="custom-add")
def add(x: int, y: int) -> int:
return x + y
result = await mcp._mcp_call_tool("custom-add", {"x": 1, "y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "3"
async def test_tool_decorator_with_description(self):
mcp = FastMCP()
@mcp.tool(description="Add two numbers")
def add(x: int, y: int) -> int:
return x + y
tools = await mcp._mcp_list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.description == "Add two numbers"
async def test_tool_decorator_instance_method(self):
mcp = FastMCP()
class MyClass:
def __init__(self, x: int):
self.x = x
@mcp.tool()
def add(self, y: int) -> int:
return self.x + y
obj = MyClass(10)
mcp.add_tool(obj.add)
result = await mcp._mcp_call_tool("add", {"y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "12"
async def test_tool_decorator_classmethod(self):
mcp = FastMCP()
class MyClass:
x: int = 10
@classmethod
def add(cls, y: int) -> int:
return cls.x + y
mcp.add_tool(MyClass.add)
result = await mcp._mcp_call_tool("add", {"y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "12"
async def test_tool_decorator_staticmethod(self):
mcp = FastMCP()
class MyClass:
@staticmethod
@mcp.tool()
def add(x: int, y: int) -> int:
return x + y
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "3"
async def test_tool_decorator_async_function(self):
mcp = FastMCP()
@mcp.tool()
async def add(x: int, y: int) -> int:
return x + y
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "3"
async def test_tool_decorator_classmethod_async_function(self):
mcp = FastMCP()
class MyClass:
x = 10
@classmethod
async def add(cls, y: int) -> int:
return cls.x + y
mcp.add_tool(MyClass.add)
result = await mcp._mcp_call_tool("add", {"y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "12"
async def test_tool_decorator_staticmethod_async_function(self):
mcp = FastMCP()
class MyClass:
@staticmethod
async def add(x: int, y: int) -> int:
return x + y
mcp.add_tool(MyClass.add)
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
assert isinstance(result[0], TextContent)
assert result[0].text == "3"
async def test_tool_decorator_with_tags(self):
"""Test that the tool decorator properly sets tags."""
mcp = FastMCP()
@mcp.tool(tags={"example", "test-tag"})
def sample_tool(x: int) -> int:
return x * 2
# Verify the tags were set correctly
tools = mcp._tool_manager.list_tools()
assert len(tools) == 1
assert tools[0].tags == {"example", "test-tag"}
async def test_add_tool_with_custom_name(self):
"""Test adding a tool with a custom name using server.add_tool()."""
mcp = FastMCP()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
mcp.add_tool(multiply, name="custom_multiply")
# Check that the tool is registered with the custom name
tools = await mcp.get_tools()
assert "custom_multiply" in tools
# Call the tool by its custom name
result = await mcp._mcp_call_tool("custom_multiply", {"a": 5, "b": 3})
assert isinstance(result[0], TextContent)
assert result[0].text == "15"
# Original name should not be registered
assert "multiply" not in tools
async def test_tool_with_annotated_arguments(self):
"""Test that tools with annotated arguments work correctly."""
mcp = FastMCP()
@mcp.tool()
def add(
x: Annotated[int, Field(description="x is an int")],
y: Annotated[str, Field(description="y is not an int")],
) -> None:
pass
tool = (await mcp.get_tools())["add"]
assert tool.parameters["properties"]["x"]["description"] == "x is an int"
assert tool.parameters["properties"]["y"]["description"] == "y is not an int"
async def test_tool_with_field_defaults(self):
"""Test that tools with annotated arguments work correctly."""
mcp = FastMCP()
@mcp.tool()
def add(
x: int = Field(description="x is an int"),
y: str = Field(description="y is not an int"),
) -> None:
pass
tool = (await mcp.get_tools())["add"]
assert tool.parameters["properties"]["x"]["description"] == "x is an int"
assert tool.parameters["properties"]["y"]["description"] == "y is not an int"
class TestResourceDecorator:
async def test_no_resources_before_decorator(self):
mcp = FastMCP()
with pytest.raises(McpError, match="Unknown resource"):
async with Client(mcp) as client:
await client.read_resource("resource://data")
async def test_resource_decorator(self):
mcp = FastMCP()
@mcp.resource("resource://data")
def get_data() -> str:
return "Hello, world!"
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Hello, world!"
async def test_resource_decorator_incorrect_usage(self):
mcp = FastMCP()
with pytest.raises(
TypeError, match="The @resource decorator was used incorrectly"
):
@mcp.resource # Missing parentheses #type: ignore
def get_data() -> str:
return "Hello, world!"
async def test_resource_decorator_with_name(self):
mcp = FastMCP()
@mcp.resource("resource://data", name="custom-data")
def get_data() -> str:
return "Hello, world!"
resources_dict = await mcp.get_resources()
resources = list(resources_dict.values())
assert len(resources) == 1
assert resources[0].name == "custom-data"
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Hello, world!"
async def test_resource_decorator_with_description(self):
mcp = FastMCP()
@mcp.resource("resource://data", description="Data resource")
def get_data() -> str:
return "Hello, world!"
resources_dict = await mcp.get_resources()
resources = list(resources_dict.values())
assert len(resources) == 1
assert resources[0].description == "Data resource"
async def test_resource_decorator_with_tags(self):
"""Test that the resource decorator properly sets tags."""
mcp = FastMCP()
@mcp.resource("resource://data", tags={"example", "test-tag"})
def get_data() -> str:
return "Hello, world!"
resources_dict = await mcp.get_resources()
resources = list(resources_dict.values())
assert len(resources) == 1
assert resources[0].tags == {"example", "test-tag"}
async def test_resource_decorator_instance_method(self):
mcp = FastMCP()
class MyClass:
def __init__(self, prefix: str):
self.prefix = prefix
def get_data(self) -> str:
return f"{self.prefix} Hello, world!"
obj = MyClass("My prefix:")
mcp.add_resource_fn(
obj.get_data, uri="resource://data", name="instance-resource"
)
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "My prefix: Hello, world!"
async def test_resource_decorator_classmethod(self):
mcp = FastMCP()
class MyClass:
prefix = "Class prefix:"
@classmethod
def get_data(cls) -> str:
return f"{cls.prefix} Hello, world!"
mcp.add_resource_fn(
MyClass.get_data, uri="resource://data", name="class-resource"
)
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Class prefix: Hello, world!"
async def test_resource_decorator_staticmethod(self):
mcp = FastMCP()
class MyClass:
@staticmethod
@mcp.resource("resource://data")
def get_data() -> str:
return "Static Hello, world!"
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Static Hello, world!"
async def test_resource_decorator_async_function(self):
mcp = FastMCP()
@mcp.resource("resource://data")
async def get_data() -> str:
return "Async Hello, world!"
async with Client(mcp) as client:
result = await client.read_resource("resource://data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Async Hello, world!"
class TestTemplateDecorator:
async def test_template_decorator(self):
mcp = FastMCP()
@mcp.resource("resource://{name}/data")
def get_data(name: str) -> str:
return f"Data for {name}"
templates_dict = await mcp.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].name == "get_data"
assert templates[0].uri_template == "resource://{name}/data"
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Data for test"
async def test_template_decorator_incorrect_usage(self):
mcp = FastMCP()
with pytest.raises(
TypeError, match="The @resource decorator was used incorrectly"
):
@mcp.resource # Missing parentheses #type: ignore
def get_data(name: str) -> str:
return f"Data for {name}"
async def test_template_decorator_with_name(self):
mcp = FastMCP()
@mcp.resource("resource://{name}/data", name="custom-template")
def get_data(name: str) -> str:
return f"Data for {name}"
templates_dict = await mcp.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].name == "custom-template"
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Data for test"
async def test_template_decorator_with_description(self):
mcp = FastMCP()
@mcp.resource("resource://{name}/data", description="Template description")
def get_data(name: str) -> str:
return f"Data for {name}"
templates_dict = await mcp.get_resource_templates()
templates = list(templates_dict.values())
assert len(templates) == 1
assert templates[0].description == "Template description"
async def test_template_decorator_instance_method(self):
mcp = FastMCP()
class MyClass:
def __init__(self, prefix: str):
self.prefix = prefix
def get_data(self, name: str) -> str:
return f"{self.prefix} Data for {name}"
obj = MyClass("My prefix:")
mcp.add_resource_fn(
obj.get_data, uri="resource://{name}/data", name="instance-template"
)
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "My prefix: Data for test"
async def test_template_decorator_classmethod(self):
mcp = FastMCP()
class MyClass:
prefix = "Class prefix:"
@classmethod
def get_data(cls, name: str) -> str:
return f"{cls.prefix} Data for {name}"
mcp.add_resource_fn(
MyClass.get_data,
uri="resource://{name}/data",
name="class-template",
)
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Class prefix: Data for test"
async def test_template_decorator_staticmethod(self):
mcp = FastMCP()
class MyClass:
@staticmethod
@mcp.resource("resource://{name}/data")
def get_data(name: str) -> str:
return f"Static Data for {name}"
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Static Data for test"
async def test_template_decorator_async_function(self):
mcp = FastMCP()
@mcp.resource("resource://{name}/data")
async def get_data(name: str) -> str:
return f"Async Data for {name}"
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Async Data for test"
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_dict = await mcp.get_resource_templates()
template = templates_dict["resource://{param}"]
assert template.tags == {"template", "test-tag"}
async def test_template_decorator_wildcard_param(self):
mcp = FastMCP()
@mcp.resource("resource://{param*}")
def template_resource(param: str) -> str:
return f"Template resource: {param}"
templates_dict = await mcp.get_resource_templates()
template = templates_dict["resource://{param*}"]
assert template.uri_template == "resource://{param*}"
assert template.name == "template_resource"
class TestPromptDecorator:
async def test_prompt_decorator(self):
mcp = FastMCP()
@mcp.prompt()
def fn() -> str:
return "Hello, world!"
prompts_dict = await mcp.get_prompts()
assert len(prompts_dict) == 1
prompt = prompts_dict["fn"]
assert prompt.name == "fn"
# Don't compare functions directly since validate_call wraps them
content = await prompt.render()
assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!"
async def test_prompt_decorator_incorrect_usage(self):
mcp = FastMCP()
with pytest.raises(
TypeError, match="The @prompt decorator was used incorrectly"
):
@mcp.prompt # Missing parentheses #type: ignore
def fn() -> str:
return "Hello, world!"
async def test_prompt_decorator_with_name(self):
mcp = FastMCP()
@mcp.prompt(name="custom_name")
def fn() -> str:
return "Hello, world!"
prompts_dict = await mcp.get_prompts()
assert len(prompts_dict) == 1
prompt = prompts_dict["custom_name"]
assert prompt.name == "custom_name"
content = await prompt.render()
assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!"
async def test_prompt_decorator_with_description(self):
mcp = FastMCP()
@mcp.prompt(description="A custom description")
def fn() -> str:
return "Hello, world!"
prompts_dict = await mcp.get_prompts()
assert len(prompts_dict) == 1
prompt = prompts_dict["fn"]
assert prompt.description == "A custom description"
content = await prompt.render()
assert isinstance(content[0].content, TextContent)
assert content[0].content.text == "Hello, world!"
async def test_prompt_decorator_with_parameters(self):
mcp = FastMCP()
@mcp.prompt()
def test_prompt(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
prompts_dict = await mcp.get_prompts()
assert len(prompts_dict) == 1
prompt = prompts_dict["test_prompt"]
assert prompt.arguments is not None
assert len(prompt.arguments) == 2
assert prompt.arguments[0].name == "name"
assert prompt.arguments[0].required is True
assert prompt.arguments[1].name == "greeting"
assert prompt.arguments[1].required is False
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt", {"name": "World"})
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Hello, World!"
result = await client.get_prompt(
"test_prompt", {"name": "World", "greeting": "Hi"}
)
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Hi, World!"
async def test_prompt_decorator_instance_method(self):
mcp = FastMCP()
class MyClass:
def __init__(self, prefix: str):
self.prefix = prefix
def test_prompt(self) -> str:
return f"{self.prefix} Hello, world!"
obj = MyClass("My prefix:")
mcp.add_prompt(obj.test_prompt, name="test_prompt")
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "My prefix: Hello, world!"
async def test_prompt_decorator_classmethod(self):
mcp = FastMCP()
class MyClass:
prefix = "Class prefix:"
@classmethod
def test_prompt(cls) -> str:
return f"{cls.prefix} Hello, world!"
mcp.add_prompt(MyClass.test_prompt, name="test_prompt")
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Class prefix: Hello, world!"
async def test_prompt_decorator_staticmethod(self):
mcp = FastMCP()
class MyClass:
@staticmethod
@mcp.prompt()
def test_prompt() -> str:
return "Static Hello, world!"
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Static Hello, world!"
async def test_prompt_decorator_async_function(self):
mcp = FastMCP()
@mcp.prompt()
async def test_prompt() -> str:
return "Async Hello, world!"
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
assert len(result.messages) == 1
message = result.messages[0]
assert isinstance(message.content, TextContent)
assert message.content.text == "Async Hello, world!"
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_dict = await mcp.get_prompts()
assert len(prompts_dict) == 1
prompt = prompts_dict["sample_prompt"]
assert prompt.tags == {"example", "test-tag"}
class TestResourcePrefixHelpers:
@pytest.mark.parametrize(
"uri,prefix,expected",
[
# Normal paths
(
"resource://path/to/resource",
"prefix",
"resource://prefix/path/to/resource",
),
# Absolute paths (with triple slash)
("resource:///absolute/path", "prefix", "resource://prefix//absolute/path"),
# Empty prefix should return the original URI
("resource://path/to/resource", "", "resource://path/to/resource"),
# Different protocols
("file://path/to/file", "prefix", "file://prefix/path/to/file"),
("http://example.com/path", "prefix", "http://prefix/example.com/path"),
# Prefixes with special characters
(
"resource://path/to/resource",
"pre.fix",
"resource://pre.fix/path/to/resource",
),
(
"resource://path/to/resource",
"pre/fix",
"resource://pre/fix/path/to/resource",
),
# Empty paths
("resource://", "prefix", "resource://prefix/"),
],
)
def test_add_resource_prefix(self, uri, prefix, expected):
"""Test that add_resource_prefix correctly adds prefixes to URIs."""
result = add_resource_prefix(uri, prefix)
assert result == expected
@pytest.mark.parametrize(
"invalid_uri",
[
"not-a-uri",
"resource:no-slashes",
"missing-protocol",
"http:/missing-slash",
],
)
def test_add_resource_prefix_invalid_uri(self, invalid_uri):
"""Test that add_resource_prefix raises ValueError for invalid URIs."""
with pytest.raises(ValueError, match="Invalid URI format"):
add_resource_prefix(invalid_uri, "prefix")
@pytest.mark.parametrize(
"uri,prefix,expected",
[
# Normal paths
(
"resource://prefix/path/to/resource",
"prefix",
"resource://path/to/resource",
),
# Absolute paths (with triple slash)
("resource://prefix//absolute/path", "prefix", "resource:///absolute/path"),
# URI without the expected prefix should return the original URI
(
"resource://other/path/to/resource",
"prefix",
"resource://other/path/to/resource",
),
# Empty prefix should return the original URI
("resource://path/to/resource", "", "resource://path/to/resource"),
# Different protocols
("file://prefix/path/to/file", "prefix", "file://path/to/file"),
# Prefixes with special characters (that need escaping in regex)
(
"resource://pre.fix/path/to/resource",
"pre.fix",
"resource://path/to/resource",
),
(
"resource://pre/fix/path/to/resource",
"pre/fix",
"resource://path/to/resource",
),
# Empty paths
("resource://prefix/", "prefix", "resource://"),
],
)
def test_remove_resource_prefix(self, uri, prefix, expected):
"""Test that remove_resource_prefix correctly removes prefixes from URIs."""
result = remove_resource_prefix(uri, prefix)
assert result == expected
@pytest.mark.parametrize(
"invalid_uri",
[
"not-a-uri",
"resource:no-slashes",
"missing-protocol",
"http:/missing-slash",
],
)
def test_remove_resource_prefix_invalid_uri(self, invalid_uri):
"""Test that remove_resource_prefix raises ValueError for invalid URIs."""
with pytest.raises(ValueError, match="Invalid URI format"):
remove_resource_prefix(invalid_uri, "prefix")
@pytest.mark.parametrize(
"uri,prefix,expected",
[
# URI with prefix
("resource://prefix/path/to/resource", "prefix", True),
# URI with another prefix
("resource://other/path/to/resource", "prefix", False),
# URI with prefix as a substring but not at path start
("resource://path/prefix/resource", "prefix", False),
# Empty prefix
("resource://path/to/resource", "", False),
# Different protocols
("file://prefix/path/to/file", "prefix", True),
# Prefix with special characters
("resource://pre.fix/path/to/resource", "pre.fix", True),
# Empty paths
("resource://prefix/", "prefix", True),
],
)
def test_has_resource_prefix(self, uri, prefix, expected):
"""Test that has_resource_prefix correctly identifies prefixes in URIs."""
result = has_resource_prefix(uri, prefix)
assert result == expected
@pytest.mark.parametrize(
"invalid_uri",
[
"not-a-uri",
"resource:no-slashes",
"missing-protocol",
"http:/missing-slash",
],
)
def test_has_resource_prefix_invalid_uri(self, invalid_uri):
"""Test that has_resource_prefix raises ValueError for invalid URIs."""
with pytest.raises(ValueError, match="Invalid URI format"):
has_resource_prefix(invalid_uri, "prefix")
class TestResourcePrefixMounting:
"""Test resource prefixing in mounted servers."""
async def test_mounted_server_resource_prefixing(self):
"""Test that resources in mounted servers use the correct prefix format."""
# Create a server with resources
server = FastMCP(name="ResourceServer")
@server.resource("resource://test-resource")
def get_resource():
return "Resource content"
@server.resource("resource:///absolute/path")
def get_absolute_resource():
return "Absolute resource content"
@server.resource("resource://{param}/template")
def get_template_resource(param: str):
return f"Template resource with {param}"
# Create a main server and mount the resource server
main_server = FastMCP(name="MainServer")
main_server.mount("prefix", server)
# Check that the resources are mounted with the correct prefixes
resources = await main_server.get_resources()
templates = await main_server.get_resource_templates()
assert "resource://prefix/test-resource" in resources
assert "resource://prefix//absolute/path" in resources
assert "resource://prefix/{param}/template" in templates
# Test that prefixed resources can be accessed
async with Client(main_server) as client:
# Regular resource
result = await client.read_resource("resource://prefix/test-resource")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Resource content"
# Absolute path resource
result = await client.read_resource("resource://prefix//absolute/path")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Absolute resource content"
# Template resource
result = await client.read_resource(
"resource://prefix/param-value/template"
)
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Template resource with param-value"
@pytest.mark.parametrize(
"uri,prefix,expected_match,expected_strip",
[
# Regular resource
(
"resource://prefix/path/to/resource",
"prefix",
True,
"resource://path/to/resource",
),
# Absolute path
(
"resource://prefix//absolute/path",
"prefix",
True,
"resource:///absolute/path",
),
# Non-matching prefix
(
"resource://other/path/to/resource",
"prefix",
False,
"resource://other/path/to/resource",
),
# Different protocol
("http://prefix/example.com", "prefix", True, "http://example.com"),
],
)
async def test_mounted_server_matching_and_stripping(
self, uri, prefix, expected_match, expected_strip
):
"""Test that MountedServer correctly matches and strips resource prefixes."""
# Create a basic server to mount
server = FastMCP()
mounted = MountedServer(prefix=prefix, server=server)
# Test matching
assert mounted.match_resource(uri) == expected_match
# Test stripping
assert mounted.strip_resource_prefix(uri) == expected_strip
async def test_import_server_with_new_prefix_format(self):
"""Test that import_server correctly uses the new prefix format."""
# Create a server with resources
source_server = FastMCP(name="SourceServer")
@source_server.resource("resource://test-resource")
def get_resource():
return "Resource content"
@source_server.resource("resource:///absolute/path")
def get_absolute_resource():
return "Absolute resource content"
@source_server.resource("resource://{param}/template")
def get_template_resource(param: str):
return f"Template resource with {param}"
# Create target server and import the source server
target_server = FastMCP(name="TargetServer")
await target_server.import_server("imported", source_server)
# Check that the resources were imported with the correct prefixes
resources = await target_server.get_resources()
templates = await target_server.get_resource_templates()
assert "resource://imported/test-resource" in resources
assert "resource://imported//absolute/path" in resources
assert "resource://imported/{param}/template" in templates
# Verify we can access the resources
async with Client(target_server) as client:
result = await client.read_resource("resource://imported/test-resource")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Resource content"
result = await client.read_resource("resource://imported//absolute/path")
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Absolute resource content"
result = await client.read_resource(
"resource://imported/param-value/template"
)
assert isinstance(result[0], TextResourceContents)
assert result[0].text == "Template resource with param-value"