Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
428a20f
1
Parent(s): e814600
Prevent invalid resource URIs
Browse files- src/fastmcp/server/server.py +15 -1
- tests/server/test_import_server.py +23 -0
- tests/server/test_mount.py +20 -0
src/fastmcp/server/server.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
| 14 |
|
| 15 |
import anyio
|
| 16 |
import httpx
|
|
|
|
| 17 |
import uvicorn
|
| 18 |
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
| 19 |
from mcp.server.auth.middleware.bearer_auth import (
|
|
@@ -39,7 +40,7 @@ from mcp.types import Prompt as MCPPrompt
|
|
| 39 |
from mcp.types import Resource as MCPResource
|
| 40 |
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
| 41 |
from mcp.types import Tool as MCPTool
|
| 42 |
-
from pydantic
|
| 43 |
from starlette.applications import Starlette
|
| 44 |
from starlette.middleware import Middleware
|
| 45 |
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
@@ -88,6 +89,8 @@ class MountedServer:
|
|
| 88 |
if prompt_separator is None:
|
| 89 |
prompt_separator = "_"
|
| 90 |
|
|
|
|
|
|
|
| 91 |
self.server = server
|
| 92 |
self.prefix = prefix
|
| 93 |
self.tool_separator = tool_separator
|
|
@@ -1074,6 +1077,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1074 |
|
| 1075 |
# Import resources and templates from the mounted server
|
| 1076 |
resource_prefix = f"{prefix}{resource_separator}"
|
|
|
|
| 1077 |
for key, resource in (await server.get_resources()).items():
|
| 1078 |
self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
|
| 1079 |
for key, template in (await server.get_resource_templates()).items():
|
|
@@ -1131,3 +1135,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1131 |
from fastmcp.server.proxy import FastMCPProxy
|
| 1132 |
|
| 1133 |
return FastMCPProxy(client=client, **settings)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
import anyio
|
| 16 |
import httpx
|
| 17 |
+
import pydantic
|
| 18 |
import uvicorn
|
| 19 |
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
|
| 20 |
from mcp.server.auth.middleware.bearer_auth import (
|
|
|
|
| 40 |
from mcp.types import Resource as MCPResource
|
| 41 |
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
| 42 |
from mcp.types import Tool as MCPTool
|
| 43 |
+
from pydantic import AnyUrl
|
| 44 |
from starlette.applications import Starlette
|
| 45 |
from starlette.middleware import Middleware
|
| 46 |
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
|
| 89 |
if prompt_separator is None:
|
| 90 |
prompt_separator = "_"
|
| 91 |
|
| 92 |
+
_validate_resource_prefix(f"{prefix}{resource_separator}")
|
| 93 |
+
|
| 94 |
self.server = server
|
| 95 |
self.prefix = prefix
|
| 96 |
self.tool_separator = tool_separator
|
|
|
|
| 1077 |
|
| 1078 |
# Import resources and templates from the mounted server
|
| 1079 |
resource_prefix = f"{prefix}{resource_separator}"
|
| 1080 |
+
_validate_resource_prefix(resource_prefix)
|
| 1081 |
for key, resource in (await server.get_resources()).items():
|
| 1082 |
self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
|
| 1083 |
for key, template in (await server.get_resource_templates()).items():
|
|
|
|
| 1135 |
from fastmcp.server.proxy import FastMCPProxy
|
| 1136 |
|
| 1137 |
return FastMCPProxy(client=client, **settings)
|
| 1138 |
+
|
| 1139 |
+
|
| 1140 |
+
def _validate_resource_prefix(prefix: str) -> None:
|
| 1141 |
+
valid_resource = "resource://path/to/resource"
|
| 1142 |
+
try:
|
| 1143 |
+
AnyUrl(f"{prefix}{valid_resource}")
|
| 1144 |
+
except pydantic.ValidationError as e:
|
| 1145 |
+
raise ValueError(
|
| 1146 |
+
f"Resource prefix or separator would result in an invalid resource URI: {e}"
|
| 1147 |
+
)
|
tests/server/test_import_server.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import json
|
| 2 |
from urllib.parse import quote
|
| 3 |
|
|
|
|
| 4 |
from mcp.types import TextContent, TextResourceContents
|
| 5 |
|
| 6 |
from fastmcp.client.client import Client
|
|
@@ -391,3 +392,25 @@ async def test_import_with_proxy_resource_templates():
|
|
| 391 |
user_data = json.loads(result[0].text)
|
| 392 |
assert user_data["name"] == "John Doe"
|
| 393 |
assert user_data["email"] == "john@example.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
from urllib.parse import quote
|
| 3 |
|
| 4 |
+
import pytest
|
| 5 |
from mcp.types import TextContent, TextResourceContents
|
| 6 |
|
| 7 |
from fastmcp.client.client import Client
|
|
|
|
| 392 |
user_data = json.loads(result[0].text)
|
| 393 |
assert user_data["name"] == "John Doe"
|
| 394 |
assert user_data["email"] == "john@example.com"
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
async def test_import_invalid_resource_prefix():
|
| 398 |
+
main_app = FastMCP("MainApp")
|
| 399 |
+
api_app = FastMCP("APIApp")
|
| 400 |
+
|
| 401 |
+
with pytest.raises(
|
| 402 |
+
ValueError,
|
| 403 |
+
match="Resource prefix or separator would result in an invalid resource URI",
|
| 404 |
+
):
|
| 405 |
+
await main_app.import_server("api_sub", api_app)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
async def test_import_invalid_resource_separator():
|
| 409 |
+
main_app = FastMCP("MainApp")
|
| 410 |
+
api_app = FastMCP("APIApp")
|
| 411 |
+
|
| 412 |
+
with pytest.raises(
|
| 413 |
+
ValueError,
|
| 414 |
+
match="Resource prefix or separator would result in an invalid resource URI",
|
| 415 |
+
):
|
| 416 |
+
await main_app.import_server("api", api_app, resource_separator="_")
|
tests/server/test_mount.py
CHANGED
|
@@ -59,6 +59,26 @@ class TestBasicMount:
|
|
| 59 |
assert isinstance(result[0], TextContent)
|
| 60 |
assert result[0].text == "Hello, World!"
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
async def test_unmount_server(self):
|
| 63 |
"""Test unmounting a server removes access to its tools."""
|
| 64 |
main_app = FastMCP("MainApp")
|
|
|
|
| 59 |
assert isinstance(result[0], TextContent)
|
| 60 |
assert result[0].text == "Hello, World!"
|
| 61 |
|
| 62 |
+
async def test_mount_invalid_resource_prefix(self):
|
| 63 |
+
main_app = FastMCP("MainApp")
|
| 64 |
+
api_app = FastMCP("APIApp")
|
| 65 |
+
|
| 66 |
+
with pytest.raises(
|
| 67 |
+
ValueError,
|
| 68 |
+
match="Resource prefix or separator would result in an invalid resource URI",
|
| 69 |
+
):
|
| 70 |
+
main_app.mount("api_sub", api_app)
|
| 71 |
+
|
| 72 |
+
async def test_mount_invalid_resource_separator(self):
|
| 73 |
+
main_app = FastMCP("MainApp")
|
| 74 |
+
api_app = FastMCP("APIApp")
|
| 75 |
+
|
| 76 |
+
with pytest.raises(
|
| 77 |
+
ValueError,
|
| 78 |
+
match="Resource prefix or separator would result in an invalid resource URI",
|
| 79 |
+
):
|
| 80 |
+
main_app.mount("api", api_app, resource_separator="_")
|
| 81 |
+
|
| 82 |
async def test_unmount_server(self):
|
| 83 |
"""Test unmounting a server removes access to its tools."""
|
| 84 |
main_app = FastMCP("MainApp")
|