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
- uri_str = str(resource.uri)
119
  logger.debug(
120
  "Adding resource",
121
  extra={
122
- "uri": uri_str,
 
123
  "type": type(resource).__name__,
124
  "resource_name": resource.name,
125
  },
126
  )
127
- existing = self._resources.get(uri_str)
128
  if existing:
129
  if self.duplicate_behavior == "warn":
130
- logger.warning(f"Resource already exists: {uri_str}")
131
- self._resources[uri_str] = resource
132
  elif self.duplicate_behavior == "replace":
133
- self._resources[uri_str] = resource
134
  elif self.duplicate_behavior == "error":
135
- raise ValueError(f"Resource already exists: {uri_str}")
136
  elif self.duplicate_behavior == "ignore":
137
  return existing
138
- else:
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(self, template: ResourceTemplate) -> ResourceTemplate:
 
 
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 resource",
176
  extra={
177
- "uri": uri_template_str,
 
178
  "type": type(template).__name__,
179
- "resource_name": template.name,
180
  },
181
  )
182
- existing = self._templates.get(uri_template_str)
183
  if existing:
184
  if self.duplicate_behavior == "warn":
185
- logger.warning(f"Resource already exists: {uri_template_str}")
186
- self._templates[uri_template_str] = template
187
  elif self.duplicate_behavior == "replace":
188
- self._templates[uri_template_str] = template
189
  elif self.duplicate_behavior == "error":
190
- raise ValueError(f"Resource already exists: {uri_template_str}")
191
  elif self.duplicate_behavior == "ignore":
192
  return existing
193
- else:
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 copy the resource with the new URI
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 copy the template with the new URI template
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 "Resource already exists" in caplog.text
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="Resource already exists"):
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
- assert resources[0].uri == AnyUrl(
702
- "fastapi+resource://openapi/get_users_users_get"
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
- assert (
709
- templates[0].uriTemplate
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"