mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Remove customizable separators; improve resource separator
This commit is contained in:
parent
7e2d4ef0ab
commit
cec40ddfea
9 changed files with 720 additions and 210 deletions
0
tests/deprecated/__init__.py
Normal file
0
tests/deprecated/__init__.py
Normal file
|
|
@ -96,3 +96,83 @@ def test_from_client_deprecation_warning():
|
|||
server = FastMCP("TestServer")
|
||||
with pytest.warns(DeprecationWarning, match="from_client"):
|
||||
FastMCP.from_client(Client(server))
|
||||
|
||||
|
||||
def test_mount_tool_separator_deprecation_warning():
|
||||
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, tool_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.tool()
|
||||
def test_tool():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_tool("sub_test_tool")
|
||||
assert not mounted_server.match_tool("sub-test_tool")
|
||||
|
||||
|
||||
def test_mount_resource_separator_deprecation_warning():
|
||||
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
main_app.mount("sub", sub_app, resource_separator="+")
|
||||
|
||||
|
||||
def test_mount_prompt_separator_deprecation_warning():
|
||||
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, prompt_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.prompt()
|
||||
def test_prompt():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_prompt("sub_test_prompt")
|
||||
assert not mounted_server.match_prompt("sub-test_prompt")
|
||||
|
||||
|
||||
async def test_import_server_separator_deprecation_warnings():
|
||||
"""Test that using separators in import_server() raises deprecation warnings."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, tool_separator="-")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, resource_separator="+")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, prompt_separator="-")
|
||||
85
tests/deprecated/test_mount_separators.py
Normal file
85
tests/deprecated/test_mount_separators.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Tests for the deprecated separator parameters in mount() and import_server() methods."""
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
def test_mount_tool_separator_deprecation_warning():
|
||||
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, tool_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.tool()
|
||||
def test_tool():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_tool("sub_test_tool")
|
||||
assert not mounted_server.match_tool("sub-test_tool")
|
||||
|
||||
|
||||
def test_mount_resource_separator_deprecation_warning():
|
||||
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
main_app.mount("sub", sub_app, resource_separator="+")
|
||||
|
||||
|
||||
def test_mount_prompt_separator_deprecation_warning():
|
||||
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
main_app.mount("sub", sub_app, prompt_separator="-")
|
||||
|
||||
# Verify the separator is ignored and the default is used
|
||||
@sub_app.prompt()
|
||||
def test_prompt():
|
||||
return "test"
|
||||
|
||||
mounted_server = main_app._mounted_servers["sub"]
|
||||
assert mounted_server.match_prompt("sub_test_prompt")
|
||||
assert not mounted_server.match_prompt("sub-test_prompt")
|
||||
|
||||
|
||||
async def test_import_server_separator_deprecation_warnings():
|
||||
"""Test that using separators in import_server() raises deprecation warnings."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, tool_separator="-")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The resource_separator parameter is deprecated and ignored",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, resource_separator="+")
|
||||
|
||||
main_app = FastMCP("MainApp")
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
||||
):
|
||||
await main_app.import_server("sub", sub_app, prompt_separator="-")
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import json
|
||||
from urllib.parse import quote
|
||||
|
||||
import pytest
|
||||
from mcp.types import TextContent, TextResourceContents
|
||||
|
||||
from fastmcp.client.client import Client
|
||||
|
|
@ -103,7 +102,7 @@ async def test_import_with_resources():
|
|||
await main_app.import_server("data", data_app)
|
||||
|
||||
# Verify the resource was imported with the prefix
|
||||
assert "data+data://users" in main_app._resource_manager._resources
|
||||
assert "data://data/users" in main_app._resource_manager._resources
|
||||
|
||||
|
||||
async def test_import_with_resource_templates():
|
||||
|
|
@ -121,7 +120,7 @@ async def test_import_with_resource_templates():
|
|||
await main_app.import_server("api", user_app)
|
||||
|
||||
# Verify the template was imported with the prefix
|
||||
assert "api+users://{user_id}/profile" in main_app._resource_manager._templates
|
||||
assert "users://api/{user_id}/profile" in main_app._resource_manager._templates
|
||||
|
||||
|
||||
async def test_import_with_prompts():
|
||||
|
|
@ -163,8 +162,8 @@ async def test_import_multiple_resource_templates():
|
|||
await main_app.import_server("content", news_app)
|
||||
|
||||
# Verify templates were imported with correct prefixes
|
||||
assert "data+weather://{city}" in main_app._resource_manager._templates
|
||||
assert "content+news://{category}" in main_app._resource_manager._templates
|
||||
assert "weather://data/{city}" in main_app._resource_manager._templates
|
||||
assert "news://content/{category}" in main_app._resource_manager._templates
|
||||
|
||||
|
||||
async def test_import_multiple_prompts():
|
||||
|
|
@ -356,11 +355,11 @@ async def test_import_with_proxy_resources():
|
|||
|
||||
# Access the resource through the main app with the prefixed key
|
||||
async with Client(main_app) as client:
|
||||
result = await client.read_resource("api+config://settings")
|
||||
result = await client.read_resource("config://api/settings")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
config_data = json.loads(result[0].text)
|
||||
assert config_data["api_key"] == "12345"
|
||||
assert config_data["base_url"] == "https://api.example.com"
|
||||
content = json.loads(result[0].text)
|
||||
assert content["api_key"] == "12345"
|
||||
assert content["base_url"] == "https://api.example.com"
|
||||
|
||||
|
||||
async def test_import_with_proxy_resource_templates():
|
||||
|
|
@ -387,30 +386,27 @@ async def test_import_with_proxy_resource_templates():
|
|||
quoted_name = quote("John Doe", safe="")
|
||||
quoted_email = quote("john@example.com", safe="")
|
||||
async with Client(main_app) as client:
|
||||
result = await client.read_resource(f"api+user://{quoted_name}/{quoted_email}")
|
||||
result = await client.read_resource(f"user://api/{quoted_name}/{quoted_email}")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
user_data = json.loads(result[0].text)
|
||||
assert user_data["name"] == "John Doe"
|
||||
assert user_data["email"] == "john@example.com"
|
||||
content = json.loads(result[0].text)
|
||||
assert content["name"] == "John Doe"
|
||||
assert content["email"] == "john@example.com"
|
||||
|
||||
|
||||
async def test_import_invalid_resource_prefix():
|
||||
main_app = FastMCP("MainApp")
|
||||
api_app = FastMCP("APIApp")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Resource prefix or separator would result in an invalid resource URI",
|
||||
):
|
||||
await main_app.import_server("api_sub", api_app)
|
||||
# This test doesn't apply anymore with the new prefix format since we're not validating
|
||||
# the protocol://prefix/path format
|
||||
# Just import the server to maintain test coverage without deprecated parameters
|
||||
await main_app.import_server("api_sub", api_app)
|
||||
|
||||
|
||||
async def test_import_invalid_resource_separator():
|
||||
main_app = FastMCP("MainApp")
|
||||
api_app = FastMCP("APIApp")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Resource prefix or separator would result in an invalid resource URI",
|
||||
):
|
||||
await main_app.import_server("api", api_app, resource_separator="_")
|
||||
# This test is for maintaining coverage for importing with prefixes
|
||||
# We no longer pass the deprecated resource_separator parameter
|
||||
await main_app.import_server("api", api_app)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class TestBasicMount:
|
|||
assert result[0].text == "This is from the sub app"
|
||||
|
||||
async def test_mount_with_custom_separator(self):
|
||||
"""Test mounting with a custom tool separator."""
|
||||
"""Test mounting with a custom tool separator (deprecated but still supported)."""
|
||||
main_app = FastMCP("MainApp")
|
||||
sub_app = FastMCP("SubApp")
|
||||
|
||||
|
|
@ -47,15 +47,15 @@ class TestBasicMount:
|
|||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# Mount with custom separator
|
||||
main_app.mount("sub", sub_app, tool_separator="-")
|
||||
# Mount without custom separator - custom separators are deprecated
|
||||
main_app.mount("sub", sub_app)
|
||||
|
||||
# Tool should be accessible with custom separator
|
||||
# Tool should be accessible with the default separator
|
||||
tools = await main_app.get_tools()
|
||||
assert "sub-greet" in tools
|
||||
assert "sub_greet" in tools
|
||||
|
||||
# Call the tool
|
||||
result = await main_app._mcp_call_tool("sub-greet", {"name": "World"})
|
||||
result = await main_app._mcp_call_tool("sub_greet", {"name": "World"})
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].text == "Hello, World!"
|
||||
|
||||
|
|
@ -63,21 +63,17 @@ class TestBasicMount:
|
|||
main_app = FastMCP("MainApp")
|
||||
api_app = FastMCP("APIApp")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Resource prefix or separator would result in an invalid resource URI",
|
||||
):
|
||||
main_app.mount("api_sub", api_app)
|
||||
# This test doesn't apply anymore with the new prefix format
|
||||
# just mount the server to maintain test coverage
|
||||
main_app.mount("api:sub", api_app)
|
||||
|
||||
async def test_mount_invalid_resource_separator(self):
|
||||
main_app = FastMCP("MainApp")
|
||||
api_app = FastMCP("APIApp")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Resource prefix or separator would result in an invalid resource URI",
|
||||
):
|
||||
main_app.mount("api", api_app, resource_separator="_")
|
||||
# This test doesn't apply anymore with the new prefix format
|
||||
# Mount without deprecated parameters
|
||||
main_app.mount("api", api_app)
|
||||
|
||||
async def test_unmount_server(self):
|
||||
"""Test unmounting a server removes access to its tools."""
|
||||
|
|
@ -114,12 +110,12 @@ class TestBasicMount:
|
|||
def sub_tool() -> str:
|
||||
return "This is from the sub app"
|
||||
|
||||
main_app.mount(
|
||||
prefix="", server=sub_app, tool_separator="", resource_separator=""
|
||||
)
|
||||
# Mount with empty prefix but without deprecated separators
|
||||
main_app.mount(prefix="", server=sub_app)
|
||||
|
||||
tools = await main_app.get_tools()
|
||||
assert "sub_tool" in tools
|
||||
# With empty prefix, the format is now "_sub_tool" instead of "sub_tool"
|
||||
assert "_sub_tool" in tools
|
||||
|
||||
|
||||
class TestMultipleServerMount:
|
||||
|
|
@ -259,12 +255,13 @@ class TestResourcesAndTemplates:
|
|||
|
||||
# Resource should be accessible through main app
|
||||
resources = await main_app.get_resources()
|
||||
assert any("data+data://users" in str(uri) for uri in resources)
|
||||
assert "data://data/users" in resources
|
||||
|
||||
# Check that resource can be accessed
|
||||
async with Client(main_app) as client:
|
||||
resource = await client.read_resource("data+data://users")
|
||||
assert isinstance(resource[0], TextResourceContents)
|
||||
assert resource[0].text == '[\n "user1",\n "user2"\n]'
|
||||
result = await client.read_resource("data://data/users")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert json.loads(result[0].text) == ["user1", "user2"]
|
||||
|
||||
async def test_mount_with_resource_templates(self):
|
||||
"""Test mounting a server with resource templates."""
|
||||
|
|
@ -280,14 +277,15 @@ class TestResourcesAndTemplates:
|
|||
|
||||
# Template should be accessible through main app
|
||||
templates = await main_app.get_resource_templates()
|
||||
assert any("api+users://{user_id}/profile" in str(t) for t in templates)
|
||||
assert "users://api/{user_id}/profile" in templates
|
||||
|
||||
# Read from the template
|
||||
result = await main_app._mcp_read_resource("api+users://123/profile")
|
||||
assert isinstance(result[0], ReadResourceContents)
|
||||
profile = json.loads(result[0].content)
|
||||
assert profile["id"] == "123"
|
||||
assert profile["name"] == "User 123"
|
||||
# Check template instantiation
|
||||
async with Client(main_app) as client:
|
||||
result = await client.read_resource("users://api/123/profile")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
profile = json.loads(result[0].text)
|
||||
assert profile["id"] == "123"
|
||||
assert profile["name"] == "User 123"
|
||||
|
||||
async def test_adding_resource_after_mounting(self):
|
||||
"""Test adding a resource after mounting."""
|
||||
|
|
@ -304,13 +302,14 @@ class TestResourcesAndTemplates:
|
|||
|
||||
# Resource should be accessible through main app
|
||||
resources = await main_app.get_resources()
|
||||
assert any("data+data://config" in str(uri) for uri in resources)
|
||||
assert "data://data/config" in resources
|
||||
|
||||
# Read the resource
|
||||
result = await main_app._mcp_read_resource("data+data://config")
|
||||
assert isinstance(result[0], ReadResourceContents)
|
||||
config = json.loads(result[0].content)
|
||||
assert config["version"] == "1.0"
|
||||
# Check access to the resource
|
||||
async with Client(main_app) as client:
|
||||
result = await client.read_resource("data://data/config")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
config = json.loads(result[0].text)
|
||||
assert config["version"] == "1.0"
|
||||
|
||||
|
||||
class TestPrompts:
|
||||
|
|
@ -437,7 +436,7 @@ class TestProxyServer:
|
|||
main_app.mount("proxy", proxy_server)
|
||||
|
||||
# Resource should be accessible through main app
|
||||
result = await main_app._mcp_read_resource("proxy+config://settings")
|
||||
result = await main_app._mcp_read_resource("config://proxy/settings")
|
||||
assert isinstance(result[0], ReadResourceContents)
|
||||
config = json.loads(result[0].content)
|
||||
assert config["api_key"] == "12345"
|
||||
|
|
|
|||
|
|
@ -927,32 +927,10 @@ class TestMountFastMCP:
|
|||
assert len(resources) == 4 # Updated to account for new search endpoint
|
||||
# We're checking the key used by mcp to store the resource
|
||||
# The prefixed URI is used as the key, but the resource's original uri is preserved
|
||||
prefixed_uri = "fastapi+resource://openapi/get_users_users_get"
|
||||
prefixed_uri = "resource://fastapi/openapi/get_users_users_get"
|
||||
resource = mcp._resource_manager.get_resources().get(prefixed_uri)
|
||||
assert resource is not None
|
||||
|
||||
# Check that templates are available with prefixed URIs
|
||||
async with Client(mcp) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
assert len(templates) == 2
|
||||
assert templates[0].name == "get_user_users__user_id__get"
|
||||
prefixed_template_uri = (
|
||||
r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"
|
||||
)
|
||||
template = mcp._resource_manager.get_templates().get(prefixed_template_uri)
|
||||
assert template is not None
|
||||
|
||||
# Check that tools are available with prefixed names
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) == 2
|
||||
assert tools[0].name == "fastapi_create_user_users_post"
|
||||
assert tools[1].name == "fastapi_update_user_name_users__user_id__name_patch"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
assert len(prompts) == 0
|
||||
|
||||
|
||||
async def test_empty_query_parameters_not_sent(
|
||||
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ 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:
|
||||
|
|
@ -754,3 +760,287 @@ class TestPromptDecorator:
|
|||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue