Jeremiah Lowin commited on
Commit
428a20f
·
1 Parent(s): e814600

Prevent invalid resource URIs

Browse files
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.networks import AnyUrl
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")