Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
5a29d74
1
Parent(s): 0631483
Add `key` for storing resources
Browse files
src/fastmcp/resources/resource_manager.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
"""Resource manager functionality."""
|
| 2 |
|
| 3 |
-
import copy
|
| 4 |
import inspect
|
| 5 |
from collections.abc import Callable
|
| 6 |
from typing import Any
|
|
@@ -109,34 +108,35 @@ class ResourceManager:
|
|
| 109 |
)
|
| 110 |
return self.add_resource(resource)
|
| 111 |
|
| 112 |
-
def add_resource(self, resource: Resource) -> Resource:
|
| 113 |
"""Add a resource to the manager.
|
| 114 |
|
| 115 |
Args:
|
| 116 |
resource: A Resource instance to add
|
|
|
|
| 117 |
"""
|
| 118 |
-
|
| 119 |
logger.debug(
|
| 120 |
"Adding resource",
|
| 121 |
extra={
|
| 122 |
-
"uri":
|
|
|
|
| 123 |
"type": type(resource).__name__,
|
| 124 |
"resource_name": resource.name,
|
| 125 |
},
|
| 126 |
)
|
| 127 |
-
existing = self._resources.get(
|
| 128 |
if existing:
|
| 129 |
if self.duplicate_behavior == "warn":
|
| 130 |
-
logger.warning(f"Resource already exists: {
|
| 131 |
-
self._resources[
|
| 132 |
elif self.duplicate_behavior == "replace":
|
| 133 |
-
self._resources[
|
| 134 |
elif self.duplicate_behavior == "error":
|
| 135 |
-
raise ValueError(f"Resource already exists: {
|
| 136 |
elif self.duplicate_behavior == "ignore":
|
| 137 |
return existing
|
| 138 |
-
|
| 139 |
-
self._resources[uri_str] = resource
|
| 140 |
return resource
|
| 141 |
|
| 142 |
def add_template_from_fn(
|
|
@@ -160,38 +160,42 @@ class ResourceManager:
|
|
| 160 |
)
|
| 161 |
return self.add_template(template)
|
| 162 |
|
| 163 |
-
def add_template(
|
|
|
|
|
|
|
| 164 |
"""Add a template to the manager.
|
| 165 |
|
| 166 |
Args:
|
| 167 |
template: A ResourceTemplate instance to add
|
|
|
|
| 168 |
|
| 169 |
Returns:
|
| 170 |
The added template. If a template with the same URI already exists,
|
| 171 |
returns the existing template.
|
| 172 |
"""
|
| 173 |
uri_template_str = str(template.uri_template)
|
|
|
|
| 174 |
logger.debug(
|
| 175 |
-
"Adding
|
| 176 |
extra={
|
| 177 |
-
"
|
|
|
|
| 178 |
"type": type(template).__name__,
|
| 179 |
-
"
|
| 180 |
},
|
| 181 |
)
|
| 182 |
-
existing = self._templates.get(
|
| 183 |
if existing:
|
| 184 |
if self.duplicate_behavior == "warn":
|
| 185 |
-
logger.warning(f"
|
| 186 |
-
self._templates[
|
| 187 |
elif self.duplicate_behavior == "replace":
|
| 188 |
-
self._templates[
|
| 189 |
elif self.duplicate_behavior == "error":
|
| 190 |
-
raise ValueError(f"
|
| 191 |
elif self.duplicate_behavior == "ignore":
|
| 192 |
return existing
|
| 193 |
-
|
| 194 |
-
self._templates[uri_template_str] = template
|
| 195 |
return template
|
| 196 |
|
| 197 |
async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
|
|
@@ -213,11 +217,19 @@ class ResourceManager:
|
|
| 213 |
|
| 214 |
raise ResourceError(f"Unknown resource: {uri}")
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
def list_resources(self) -> list[Resource]:
|
| 217 |
"""List all registered resources."""
|
| 218 |
logger.debug("Listing resources", extra={"count": len(self._resources)})
|
| 219 |
return list(self._resources.values())
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
def list_templates(self) -> list[ResourceTemplate]:
|
| 222 |
"""List all registered templates."""
|
| 223 |
logger.debug("Listing templates", extra={"count": len(self._templates)})
|
|
@@ -240,14 +252,9 @@ class ResourceManager:
|
|
| 240 |
If None, the original URI is used.
|
| 241 |
"""
|
| 242 |
for uri, resource in manager._resources.items():
|
| 243 |
-
# Create prefixed URI and
|
| 244 |
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
| 245 |
-
|
| 246 |
-
new_resource = copy.copy(resource)
|
| 247 |
-
new_resource.uri = AnyUrl(prefixed_uri)
|
| 248 |
-
|
| 249 |
-
# Store directly in resources dictionary
|
| 250 |
-
self.add_resource(new_resource)
|
| 251 |
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
| 252 |
|
| 253 |
def import_templates(
|
|
@@ -267,16 +274,11 @@ class ResourceManager:
|
|
| 267 |
If None, the original URI template is used.
|
| 268 |
"""
|
| 269 |
for uri_template, template in manager._templates.items():
|
| 270 |
-
# Create prefixed URI template and
|
| 271 |
prefixed_uri_template = (
|
| 272 |
f"{prefix}{uri_template}" if prefix else uri_template
|
| 273 |
)
|
| 274 |
-
|
| 275 |
-
new_template = copy.copy(template)
|
| 276 |
-
new_template.uri_template = prefixed_uri_template
|
| 277 |
-
|
| 278 |
-
# Store directly in templates dictionary
|
| 279 |
-
self.add_template(new_template)
|
| 280 |
logger.debug(
|
| 281 |
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
| 282 |
)
|
|
|
|
| 1 |
"""Resource manager functionality."""
|
| 2 |
|
|
|
|
| 3 |
import inspect
|
| 4 |
from collections.abc import Callable
|
| 5 |
from typing import Any
|
|
|
|
| 108 |
)
|
| 109 |
return self.add_resource(resource)
|
| 110 |
|
| 111 |
+
def add_resource(self, resource: Resource, key: str | None = None) -> Resource:
|
| 112 |
"""Add a resource to the manager.
|
| 113 |
|
| 114 |
Args:
|
| 115 |
resource: A Resource instance to add
|
| 116 |
+
key: Optional URI to use as the storage key (if different from resource.uri)
|
| 117 |
"""
|
| 118 |
+
storage_key = key or str(resource.uri)
|
| 119 |
logger.debug(
|
| 120 |
"Adding resource",
|
| 121 |
extra={
|
| 122 |
+
"uri": resource.uri,
|
| 123 |
+
"storage_key": storage_key,
|
| 124 |
"type": type(resource).__name__,
|
| 125 |
"resource_name": resource.name,
|
| 126 |
},
|
| 127 |
)
|
| 128 |
+
existing = self._resources.get(storage_key)
|
| 129 |
if existing:
|
| 130 |
if self.duplicate_behavior == "warn":
|
| 131 |
+
logger.warning(f"Resource already exists: {storage_key}")
|
| 132 |
+
self._resources[storage_key] = resource
|
| 133 |
elif self.duplicate_behavior == "replace":
|
| 134 |
+
self._resources[storage_key] = resource
|
| 135 |
elif self.duplicate_behavior == "error":
|
| 136 |
+
raise ValueError(f"Resource already exists: {storage_key}")
|
| 137 |
elif self.duplicate_behavior == "ignore":
|
| 138 |
return existing
|
| 139 |
+
self._resources[storage_key] = resource
|
|
|
|
| 140 |
return resource
|
| 141 |
|
| 142 |
def add_template_from_fn(
|
|
|
|
| 160 |
)
|
| 161 |
return self.add_template(template)
|
| 162 |
|
| 163 |
+
def add_template(
|
| 164 |
+
self, template: ResourceTemplate, key: str | None = None
|
| 165 |
+
) -> ResourceTemplate:
|
| 166 |
"""Add a template to the manager.
|
| 167 |
|
| 168 |
Args:
|
| 169 |
template: A ResourceTemplate instance to add
|
| 170 |
+
key: Optional URI template to use as the storage key (if different from template.uri_template)
|
| 171 |
|
| 172 |
Returns:
|
| 173 |
The added template. If a template with the same URI already exists,
|
| 174 |
returns the existing template.
|
| 175 |
"""
|
| 176 |
uri_template_str = str(template.uri_template)
|
| 177 |
+
storage_key = key or uri_template_str
|
| 178 |
logger.debug(
|
| 179 |
+
"Adding template",
|
| 180 |
extra={
|
| 181 |
+
"uri_template": uri_template_str,
|
| 182 |
+
"storage_key": storage_key,
|
| 183 |
"type": type(template).__name__,
|
| 184 |
+
"template_name": template.name,
|
| 185 |
},
|
| 186 |
)
|
| 187 |
+
existing = self._templates.get(storage_key)
|
| 188 |
if existing:
|
| 189 |
if self.duplicate_behavior == "warn":
|
| 190 |
+
logger.warning(f"Template already exists: {storage_key}")
|
| 191 |
+
self._templates[storage_key] = template
|
| 192 |
elif self.duplicate_behavior == "replace":
|
| 193 |
+
self._templates[storage_key] = template
|
| 194 |
elif self.duplicate_behavior == "error":
|
| 195 |
+
raise ValueError(f"Template already exists: {storage_key}")
|
| 196 |
elif self.duplicate_behavior == "ignore":
|
| 197 |
return existing
|
| 198 |
+
self._templates[storage_key] = template
|
|
|
|
| 199 |
return template
|
| 200 |
|
| 201 |
async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
|
|
|
|
| 217 |
|
| 218 |
raise ResourceError(f"Unknown resource: {uri}")
|
| 219 |
|
| 220 |
+
def get_resources(self) -> dict[str, Resource]:
|
| 221 |
+
"""Get all registered resources, keyed by URI."""
|
| 222 |
+
return self._resources
|
| 223 |
+
|
| 224 |
def list_resources(self) -> list[Resource]:
|
| 225 |
"""List all registered resources."""
|
| 226 |
logger.debug("Listing resources", extra={"count": len(self._resources)})
|
| 227 |
return list(self._resources.values())
|
| 228 |
|
| 229 |
+
def get_templates(self) -> dict[str, ResourceTemplate]:
|
| 230 |
+
"""Get all registered templates, keyed by URI template."""
|
| 231 |
+
return self._templates
|
| 232 |
+
|
| 233 |
def list_templates(self) -> list[ResourceTemplate]:
|
| 234 |
"""List all registered templates."""
|
| 235 |
logger.debug("Listing templates", extra={"count": len(self._templates)})
|
|
|
|
| 252 |
If None, the original URI is used.
|
| 253 |
"""
|
| 254 |
for uri, resource in manager._resources.items():
|
| 255 |
+
# Create prefixed URI and import the resource with the new URI as the storage key
|
| 256 |
prefixed_uri = f"{prefix}{uri}" if prefix else uri
|
| 257 |
+
self.add_resource(resource, key=prefixed_uri)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
| 259 |
|
| 260 |
def import_templates(
|
|
|
|
| 274 |
If None, the original URI template is used.
|
| 275 |
"""
|
| 276 |
for uri_template, template in manager._templates.items():
|
| 277 |
+
# Create prefixed URI template and import the template with the new URI as the storage key
|
| 278 |
prefixed_uri_template = (
|
| 279 |
f"{prefix}{uri_template}" if prefix else uri_template
|
| 280 |
)
|
| 281 |
+
self.add_template(template, key=prefixed_uri_template)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
logger.debug(
|
| 283 |
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
| 284 |
)
|
src/fastmcp/server/server.py
CHANGED
|
@@ -362,14 +362,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 362 |
|
| 363 |
return decorator
|
| 364 |
|
| 365 |
-
def add_resource(self, resource: Resource) -> None:
|
| 366 |
"""Add a resource to the server.
|
| 367 |
|
| 368 |
Args:
|
| 369 |
resource: A Resource instance to add
|
| 370 |
"""
|
| 371 |
|
| 372 |
-
self._resource_manager.add_resource(resource)
|
| 373 |
|
| 374 |
def add_resource_fn(
|
| 375 |
self,
|
|
|
|
| 362 |
|
| 363 |
return decorator
|
| 364 |
|
| 365 |
+
def add_resource(self, resource: Resource, key: str | None = None) -> None:
|
| 366 |
"""Add a resource to the server.
|
| 367 |
|
| 368 |
Args:
|
| 369 |
resource: A Resource instance to add
|
| 370 |
"""
|
| 371 |
|
| 372 |
+
self._resource_manager.add_resource(resource, key=key)
|
| 373 |
|
| 374 |
def add_resource_fn(
|
| 375 |
self,
|
tests/resources/test_resource_manager.py
CHANGED
|
@@ -168,7 +168,7 @@ class TestResourceManager:
|
|
| 168 |
manager.add_template(template)
|
| 169 |
manager.add_template(template)
|
| 170 |
|
| 171 |
-
assert "
|
| 172 |
# Should have the template
|
| 173 |
assert len(manager.list_templates()) == 1
|
| 174 |
|
|
@@ -187,7 +187,7 @@ class TestResourceManager:
|
|
| 187 |
|
| 188 |
manager.add_template(template)
|
| 189 |
|
| 190 |
-
with pytest.raises(ValueError, match="
|
| 191 |
manager.add_template(template)
|
| 192 |
|
| 193 |
def test_replace_duplicate_templates(self):
|
|
|
|
| 168 |
manager.add_template(template)
|
| 169 |
manager.add_template(template)
|
| 170 |
|
| 171 |
+
assert "Template already exists" in caplog.text
|
| 172 |
# Should have the template
|
| 173 |
assert len(manager.list_templates()) == 1
|
| 174 |
|
|
|
|
| 187 |
|
| 188 |
manager.add_template(template)
|
| 189 |
|
| 190 |
+
with pytest.raises(ValueError, match="Template already exists"):
|
| 191 |
manager.add_template(template)
|
| 192 |
|
| 193 |
def test_replace_duplicate_templates(self):
|
tests/server/test_openapi.py
CHANGED
|
@@ -696,20 +696,26 @@ class TestMountFastMCP:
|
|
| 696 |
|
| 697 |
mcp.mount("fastapi", fastmcp_openapi_server)
|
| 698 |
|
|
|
|
| 699 |
resources = await mcp._mcp_list_resources()
|
| 700 |
assert len(resources) == 1
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
|
|
|
|
|
|
| 704 |
|
|
|
|
| 705 |
templates = await mcp._mcp_list_resource_templates()
|
| 706 |
assert len(templates) == 1
|
| 707 |
assert templates[0].name == "get_user_users__user_id__get"
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
== r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"
|
| 711 |
)
|
|
|
|
|
|
|
| 712 |
|
|
|
|
| 713 |
tools = await mcp._mcp_list_tools()
|
| 714 |
assert len(tools) == 2
|
| 715 |
assert tools[0].name == "fastapi_create_user_users_post"
|
|
|
|
| 696 |
|
| 697 |
mcp.mount("fastapi", fastmcp_openapi_server)
|
| 698 |
|
| 699 |
+
# Check that resources are available with prefixed URIs
|
| 700 |
resources = await mcp._mcp_list_resources()
|
| 701 |
assert len(resources) == 1
|
| 702 |
+
# We're checking the key used by mcp to store the resource
|
| 703 |
+
# The prefixed URI is used as the key, but the resource's original uri is preserved
|
| 704 |
+
prefixed_uri = "fastapi+resource://openapi/get_users_users_get"
|
| 705 |
+
resource = mcp._resource_manager.get_resources().get(prefixed_uri)
|
| 706 |
+
assert resource is not None
|
| 707 |
|
| 708 |
+
# Check that templates are available with prefixed URIs
|
| 709 |
templates = await mcp._mcp_list_resource_templates()
|
| 710 |
assert len(templates) == 1
|
| 711 |
assert templates[0].name == "get_user_users__user_id__get"
|
| 712 |
+
prefixed_template_uri = (
|
| 713 |
+
r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"
|
|
|
|
| 714 |
)
|
| 715 |
+
template = mcp._resource_manager.get_templates().get(prefixed_template_uri)
|
| 716 |
+
assert template is not None
|
| 717 |
|
| 718 |
+
# Check that tools are available with prefixed names
|
| 719 |
tools = await mcp._mcp_list_tools()
|
| 720 |
assert len(tools) == 2
|
| 721 |
assert tools[0].name == "fastapi_create_user_users_post"
|