Jeremiah Lowin commited on
Commit
d37c5e4
·
1 Parent(s): 80723cc

Update resource manager

Browse files
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -65,14 +65,10 @@ class PromptManager:
65
  child_results = await mounted.server._list_prompts()
66
  else: # mode == "inventory"
67
  # PATH 1: Use the manager-to-manager unfiltered path
68
- child_results = await mounted.server._prompt_manager.get_prompts()
69
 
70
  # The combination logic is the same for both paths
71
- child_dict = (
72
- {p.key: p for p in child_results}
73
- if isinstance(child_results, list)
74
- else child_results
75
- )
76
  if mounted.prefix:
77
  for prompt in child_dict.values():
78
  prefixed_prompt = prompt.with_key(
@@ -110,7 +106,7 @@ class PromptManager:
110
  """
111
  return await self._load_prompts(mode="inventory")
112
 
113
- async def list_prompts(self) -> list[Prompt]:
114
  """
115
  Lists all prompts, applying protocol filtering.
116
  """
 
65
  child_results = await mounted.server._list_prompts()
66
  else: # mode == "inventory"
67
  # PATH 1: Use the manager-to-manager unfiltered path
68
+ child_results = await mounted.server._prompt_manager._list_prompts()
69
 
70
  # The combination logic is the same for both paths
71
+ child_dict = {p.key: p for p in child_results}
 
 
 
 
72
  if mounted.prefix:
73
  for prompt in child_dict.values():
74
  prefixed_prompt = prompt.with_key(
 
106
  """
107
  return await self._load_prompts(mode="inventory")
108
 
109
+ async def _list_prompts(self) -> list[Prompt]:
110
  """
111
  Lists all prompts, applying protocol filtering.
112
  """
src/fastmcp/resources/resource_manager.py CHANGED
@@ -1,9 +1,11 @@
1
  """Resource manager functionality."""
2
 
 
 
3
  import inspect
4
  import warnings
5
  from collections.abc import Callable
6
- from typing import Any
7
 
8
  from pydantic import AnyUrl
9
 
@@ -17,6 +19,9 @@ from fastmcp.resources.template import (
17
  from fastmcp.settings import DuplicateBehavior
18
  from fastmcp.utilities.logging import get_logger
19
 
 
 
 
20
  logger = get_logger(__name__)
21
 
22
 
@@ -38,6 +43,7 @@ class ResourceManager:
38
  """
39
  self._resources: dict[str, Resource] = {}
40
  self._templates: dict[str, ResourceTemplate] = {}
 
41
  self.mask_error_details = mask_error_details or settings.mask_error_details
42
 
43
  # Default to "warn" if None is provided
@@ -51,6 +57,128 @@ class ResourceManager:
51
  )
52
  self.duplicate_behavior = duplicate_behavior
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def add_resource_or_template_from_fn(
55
  self,
56
  fn: Callable[..., Any],
@@ -235,14 +363,21 @@ class ResourceManager:
235
  self._templates[storage_key] = template
236
  return template
237
 
238
- def has_resource(self, uri: AnyUrl | str) -> bool:
239
  """Check if a resource exists."""
240
  uri_str = str(uri)
241
- if uri_str in self._resources:
 
 
 
242
  return True
243
- for template_key in self._templates.keys():
 
 
 
244
  if match_uri_template(uri_str, template_key):
245
  return True
 
246
  return False
247
 
248
  async def get_resource(self, uri: AnyUrl | str) -> Resource:
@@ -257,12 +392,14 @@ class ResourceManager:
257
  uri_str = str(uri)
258
  logger.debug("Getting resource", extra={"uri": uri_str})
259
 
260
- # First check concrete resources
261
- if resource := self._resources.get(uri_str):
 
262
  return resource
263
 
264
- # Then check templates - use the utility function to match against storage keys
265
- for storage_key, template in self._templates.items():
 
266
  # Try to match against the storage key (which might be a custom key)
267
  if params := match_uri_template(uri_str, storage_key):
268
  try:
@@ -289,31 +426,89 @@ class ResourceManager:
289
  raise NotFoundError(f"Unknown resource: {uri_str}")
290
 
291
  async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
292
- """Read a resource contents."""
293
- resource = await self.get_resource(uri)
294
-
295
- try:
296
- return await resource.read()
297
-
298
- # raise ResourceErrors as-is
299
- except ResourceError as e:
300
- logger.error(f"Error reading resource {uri!r}: {e}")
301
- raise e
302
-
303
- # Handle other exceptions
304
- except Exception as e:
305
- logger.error(f"Error reading resource {uri!r}: {e}")
306
- if self.mask_error_details:
307
- # Mask internal details
308
- raise ResourceError(f"Error reading resource {uri!r}") from e
309
- else:
310
- # Include original error details
311
- raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
312
-
313
- def get_resources(self) -> dict[str, Resource]:
314
- """Get all registered resources, keyed by URI."""
315
- return self._resources
316
 
317
- def get_templates(self) -> dict[str, ResourceTemplate]:
318
- """Get all registered templates, keyed by URI template."""
319
- return self._templates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Resource manager functionality."""
2
 
3
+ from __future__ import annotations
4
+
5
  import inspect
6
  import warnings
7
  from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Any, Literal
9
 
10
  from pydantic import AnyUrl
11
 
 
19
  from fastmcp.settings import DuplicateBehavior
20
  from fastmcp.utilities.logging import get_logger
21
 
22
+ if TYPE_CHECKING:
23
+ from fastmcp.server.server import MountedServer
24
+
25
  logger = get_logger(__name__)
26
 
27
 
 
43
  """
44
  self._resources: dict[str, Resource] = {}
45
  self._templates: dict[str, ResourceTemplate] = {}
46
+ self._mounted_sources: list[MountedServer] = []
47
  self.mask_error_details = mask_error_details or settings.mask_error_details
48
 
49
  # Default to "warn" if None is provided
 
57
  )
58
  self.duplicate_behavior = duplicate_behavior
59
 
60
+ def mount(self, server: MountedServer) -> None:
61
+ """Adds a mounted server as a source for resources and templates."""
62
+ self._mounted_sources.append(server)
63
+
64
+ async def get_resources(self) -> dict[str, Resource]:
65
+ """Get all registered resources, keyed by URI."""
66
+ return await self._load_resources(mode="inventory")
67
+
68
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
69
+ """Get all registered templates, keyed by URI template."""
70
+ return await self._load_resource_templates(mode="inventory")
71
+
72
+ async def _load_resources(
73
+ self, *, mode: Literal["inventory", "protocol"]
74
+ ) -> dict[str, Resource]:
75
+ """
76
+ The single, consolidated recursive method for fetching resources. The 'mode'
77
+ parameter determines the communication path.
78
+
79
+ - mode="inventory": Manager-to-manager path for complete, unfiltered inventory
80
+ - mode="protocol": Server-to-server path for filtered MCP requests
81
+ """
82
+ all_resources: dict[str, Resource] = {}
83
+
84
+ for mounted in self._mounted_sources:
85
+ try:
86
+ if mode == "protocol":
87
+ # PATH 2: Use the server-to-server filtered path
88
+ child_resources_list = await mounted.server._list_resources()
89
+ child_resources = {
90
+ resource.key: resource for resource in child_resources_list
91
+ }
92
+ else: # mode == "inventory"
93
+ # PATH 1: Use the manager-to-manager unfiltered path
94
+ child_resources = (
95
+ await mounted.server._resource_manager.get_resources()
96
+ )
97
+
98
+ # Apply prefix if needed
99
+ if mounted.prefix:
100
+ from fastmcp.server.server import add_resource_prefix
101
+
102
+ for uri, resource in child_resources.items():
103
+ prefixed_uri = add_resource_prefix(
104
+ uri, mounted.prefix, mounted.resource_prefix_format
105
+ )
106
+ # Create a copy of the resource with the prefixed key
107
+ prefixed_resource = resource.with_key(prefixed_uri)
108
+ all_resources[prefixed_uri] = prefixed_resource
109
+ else:
110
+ all_resources.update(child_resources)
111
+ except Exception as e:
112
+ # Skip failed mounts silently, matches existing behavior
113
+ logger.warning(
114
+ f"Failed to get resources from mounted server '{mounted.prefix}': {e}"
115
+ )
116
+ continue
117
+
118
+ # Finally, add local resources, which always take precedence
119
+ all_resources.update(self._resources)
120
+ return all_resources
121
+
122
+ async def _load_resource_templates(
123
+ self, *, mode: Literal["inventory", "protocol"]
124
+ ) -> dict[str, ResourceTemplate]:
125
+ """
126
+ The single, consolidated recursive method for fetching templates. The 'mode'
127
+ parameter determines the communication path.
128
+
129
+ - mode="inventory": Manager-to-manager path for complete, unfiltered inventory
130
+ - mode="protocol": Server-to-server path for filtered MCP requests
131
+ """
132
+ all_templates: dict[str, ResourceTemplate] = {}
133
+
134
+ for mounted in self._mounted_sources:
135
+ try:
136
+ if mode == "protocol":
137
+ # PATH 2: Use the server-to-server filtered path
138
+ child_templates = await mounted.server._list_resource_templates()
139
+ else: # mode == "inventory"
140
+ # PATH 1: Use the manager-to-manager unfiltered path
141
+ child_templates = await mounted.server._resource_manager._list_resource_templates()
142
+ child_dict = {template.key: template for template in child_templates}
143
+
144
+ # Apply prefix if needed
145
+ if mounted.prefix:
146
+ from fastmcp.server.server import add_resource_prefix
147
+
148
+ for uri_template, template in child_dict.items():
149
+ prefixed_uri_template = add_resource_prefix(
150
+ uri_template, mounted.prefix, mounted.resource_prefix_format
151
+ )
152
+ # Create a copy of the template with the prefixed key
153
+ prefixed_template = template.with_key(prefixed_uri_template)
154
+ all_templates[prefixed_uri_template] = prefixed_template
155
+ else:
156
+ all_templates.update(child_dict)
157
+ except Exception as e:
158
+ # Skip failed mounts silently, matches existing behavior
159
+ logger.warning(
160
+ f"Failed to get templates from mounted server '{mounted.prefix}': {e}"
161
+ )
162
+ continue
163
+
164
+ # Finally, add local templates, which always take precedence
165
+ all_templates.update(self._templates)
166
+ return all_templates
167
+
168
+ async def _list_resources(self) -> list[Resource]:
169
+ """
170
+ Lists all resources, applying protocol filtering.
171
+ """
172
+ resources_dict = await self._load_resources(mode="protocol")
173
+ return list(resources_dict.values())
174
+
175
+ async def _list_resource_templates(self) -> list[ResourceTemplate]:
176
+ """
177
+ Lists all templates, applying protocol filtering.
178
+ """
179
+ templates_dict = await self._load_resource_templates(mode="protocol")
180
+ return list(templates_dict.values())
181
+
182
  def add_resource_or_template_from_fn(
183
  self,
184
  fn: Callable[..., Any],
 
363
  self._templates[storage_key] = template
364
  return template
365
 
366
+ async def has_resource(self, uri: AnyUrl | str) -> bool:
367
  """Check if a resource exists."""
368
  uri_str = str(uri)
369
+
370
+ # First check concrete resources (local and mounted)
371
+ resources = await self.get_resources()
372
+ if uri_str in resources:
373
  return True
374
+
375
+ # Then check templates (local and mounted) only if not found in concrete resources
376
+ templates = await self.get_resource_templates()
377
+ for template_key in templates.keys():
378
  if match_uri_template(uri_str, template_key):
379
  return True
380
+
381
  return False
382
 
383
  async def get_resource(self, uri: AnyUrl | str) -> Resource:
 
392
  uri_str = str(uri)
393
  logger.debug("Getting resource", extra={"uri": uri_str})
394
 
395
+ # First check concrete resources (local and mounted)
396
+ resources = await self.get_resources()
397
+ if resource := resources.get(uri_str):
398
  return resource
399
 
400
+ # Then check templates (local and mounted) - use the utility function to match against storage keys
401
+ templates = await self.get_resource_templates()
402
+ for storage_key, template in templates.items():
403
  # Try to match against the storage key (which might be a custom key)
404
  if params := match_uri_template(uri_str, storage_key):
405
  try:
 
426
  raise NotFoundError(f"Unknown resource: {uri_str}")
427
 
428
  async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
429
+ """
430
+ Internal API for servers: Finds and reads a resource, respecting the
431
+ filtered protocol path.
432
+ """
433
+ uri_str = str(uri)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ # 1. Check local resources first. The server will have already applied its filter.
436
+ if uri_str in self._resources:
437
+ resource = await self.get_resource(uri_str)
438
+ if not resource:
439
+ raise NotFoundError(f"Resource {uri_str!r} not found")
440
+
441
+ try:
442
+ return await resource.read()
443
+
444
+ # raise ResourceErrors as-is
445
+ except ResourceError as e:
446
+ logger.exception(f"Error reading resource {uri_str!r}: {e}")
447
+ raise e
448
+
449
+ # Handle other exceptions
450
+ except Exception as e:
451
+ logger.exception(f"Error reading resource {uri_str!r}: {e}")
452
+ if self.mask_error_details:
453
+ # Mask internal details
454
+ raise ResourceError(f"Error reading resource {uri_str!r}") from e
455
+ else:
456
+ # Include original error details
457
+ raise ResourceError(
458
+ f"Error reading resource {uri_str!r}: {e}"
459
+ ) from e
460
+
461
+ # 1b. Check local templates if not found in concrete resources
462
+ for template in self._templates.values():
463
+ if params := match_uri_template(uri_str, template.uri_template):
464
+ try:
465
+ resource = await template.create_resource(uri_str, params=params)
466
+ return await resource.read()
467
+ except ResourceError as e:
468
+ logger.exception(
469
+ f"Error reading resource from template {uri_str!r}: {e}"
470
+ )
471
+ raise e
472
+ except Exception as e:
473
+ logger.exception(
474
+ f"Error reading resource from template {uri_str!r}: {e}"
475
+ )
476
+ if self.mask_error_details:
477
+ raise ResourceError(
478
+ f"Error reading resource from template {uri_str!r}"
479
+ ) from e
480
+ else:
481
+ raise ResourceError(
482
+ f"Error reading resource from template {uri_str!r}: {e}"
483
+ ) from e
484
+
485
+ # 2. Check mounted servers using the filtered protocol path.
486
+ from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
487
+
488
+ for mounted in reversed(self._mounted_sources):
489
+ resource_uri = uri_str
490
+ try:
491
+ if mounted.prefix:
492
+ # If server has a prefix, check if URI matches and strip prefix
493
+ if has_resource_prefix(
494
+ resource_uri,
495
+ mounted.prefix,
496
+ mounted.resource_prefix_format,
497
+ ):
498
+ resource_uri = remove_resource_prefix(
499
+ resource_uri,
500
+ mounted.prefix,
501
+ mounted.resource_prefix_format,
502
+ )
503
+ else:
504
+ continue
505
+
506
+ result = await mounted.server._read_resource(resource_uri)
507
+ # Extract content from the first ReadResourceContents
508
+ if result and len(result) > 0:
509
+ return result[0].content
510
+ raise NotFoundError(f"Resource {uri_str!r} returned empty content")
511
+ except NotFoundError:
512
+ continue
513
+
514
+ raise NotFoundError(f"Resource {uri_str!r} not found.")
src/fastmcp/server/server.py CHANGED
@@ -351,8 +351,7 @@ class FastMCP(Generic[LifespanResultT]):
351
 
352
  async def get_resources(self) -> dict[str, Resource]:
353
  """Get all registered resources, indexed by registered key."""
354
- resources = await self._list_resources(apply_middleware=False)
355
- return {resource.key: resource for resource in resources}
356
 
357
  async def get_resource(self, key: str) -> Resource:
358
  resources = await self.get_resources()
@@ -362,8 +361,7 @@ class FastMCP(Generic[LifespanResultT]):
362
 
363
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
364
  """Get all registered resource templates, indexed by registered key."""
365
- templates = await self._list_resource_templates(apply_middleware=False)
366
- return {template.key: template for template in templates}
367
 
368
  async def get_resource_template(self, key: str) -> ResourceTemplate:
369
  templates = await self.get_resource_templates()
@@ -444,7 +442,7 @@ class FastMCP(Generic[LifespanResultT]):
444
  async def _handler(
445
  context: MiddlewareContext[mcp.types.ListToolsRequest],
446
  ) -> list[Tool]:
447
- tools = await self._tool_manager.list_tools()
448
 
449
  mcp_tools: list[Tool] = []
450
  for tool in tools:
@@ -470,12 +468,12 @@ class FastMCP(Generic[LifespanResultT]):
470
  logger.debug("Handler called: list_resources")
471
 
472
  with fastmcp.server.context.Context(fastmcp=self):
473
- resources = await self._middleware_list_resources()
474
  return [
475
  resource.to_mcp_resource(uri=resource.key) for resource in resources
476
  ]
477
 
478
- async def _middleware_list_resources(self) -> list[Resource]:
479
  """
480
  List all available resources, in the format expected by the low-level MCP
481
  server.
@@ -485,7 +483,7 @@ class FastMCP(Generic[LifespanResultT]):
485
  async def _handler(
486
  context: MiddlewareContext[dict[str, Any]],
487
  ) -> list[Resource]:
488
- resources = await self._list_resources()
489
 
490
  mcp_resources: list[Resource] = []
491
  for resource in resources:
@@ -507,62 +505,17 @@ class FastMCP(Generic[LifespanResultT]):
507
  # Apply the middleware chain.
508
  return await self._apply_middleware(mw_context, _handler)
509
 
510
- async def _list_resources(self, apply_middleware: bool = True) -> list[Resource]:
511
- """
512
- List all available resources.
513
- """
514
-
515
- if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
516
- resources: dict[str, Resource] = {}
517
-
518
- # iterate such that new mounts overwrite older ones
519
- for mounted_server in self._mounted_servers:
520
- try:
521
- if apply_middleware:
522
- server_resources = (
523
- await mounted_server.server._middleware_list_resources()
524
- )
525
- else:
526
- server_resources = await mounted_server.server._list_resources()
527
- # Apply prefix to each resource key if prefix exists
528
- if mounted_server.prefix:
529
- for resource in server_resources:
530
- resource = resource.with_key(
531
- add_resource_prefix(
532
- resource.key,
533
- mounted_server.prefix,
534
- self.resource_prefix_format,
535
- )
536
- )
537
- resources[resource.key] = resource
538
- else:
539
- resources.update(
540
- {resource.key: resource for resource in server_resources}
541
- )
542
- except Exception as e:
543
- logger.warning(
544
- f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
545
- )
546
- continue
547
- (
548
- local_resources,
549
- _,
550
- ) = await self._resource_manager.get_resources_and_templates()
551
- resources.update(local_resources)
552
- self._cache.set("resources", resources)
553
- return list(resources.values())
554
-
555
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
556
  logger.debug("Handler called: list_resource_templates")
557
 
558
  with fastmcp.server.context.Context(fastmcp=self):
559
- templates = await self._middleware_list_resource_templates()
560
  return [
561
  template.to_mcp_template(uriTemplate=template.key)
562
  for template in templates
563
  ]
564
 
565
- async def _middleware_list_resource_templates(self) -> list[ResourceTemplate]:
566
  """
567
  List all available resource templates, in the format expected by the low-level MCP
568
  server.
@@ -572,7 +525,7 @@ class FastMCP(Generic[LifespanResultT]):
572
  async def _handler(
573
  context: MiddlewareContext[dict[str, Any]],
574
  ) -> list[ResourceTemplate]:
575
- templates = await self._list_resource_templates()
576
 
577
  mcp_templates: list[ResourceTemplate] = []
578
  for template in templates:
@@ -594,56 +547,6 @@ class FastMCP(Generic[LifespanResultT]):
594
  # Apply the middleware chain.
595
  return await self._apply_middleware(mw_context, _handler)
596
 
597
- async def _list_resource_templates(
598
- self, apply_middleware: bool = True
599
- ) -> list[ResourceTemplate]:
600
- """
601
- List all available resource templates.
602
- """
603
-
604
- if (
605
- templates := self._cache.get("resource_templates")
606
- ) is self._cache.NOT_FOUND:
607
- templates: dict[str, ResourceTemplate] = {}
608
-
609
- # iterate such that new mounts overwrite older ones
610
- for mounted_server in self._mounted_servers:
611
- try:
612
- if apply_middleware:
613
- server_templates = await mounted_server.server._middleware_list_resource_templates()
614
- else:
615
- server_templates = (
616
- await mounted_server.server._list_resource_templates()
617
- )
618
- # Apply prefix to each template key if prefix exists
619
- if mounted_server.prefix:
620
- for template in server_templates:
621
- template = template.with_key(
622
- add_resource_prefix(
623
- template.key,
624
- mounted_server.prefix,
625
- self.resource_prefix_format,
626
- )
627
- )
628
- templates[template.key] = template
629
- else:
630
- templates.update(
631
- {template.key: template for template in server_templates}
632
- )
633
- except Exception as e:
634
- logger.warning(
635
- "Failed to get resource templates from mounted server "
636
- f"'{mounted_server.prefix}': {e}"
637
- )
638
- continue
639
- (
640
- _,
641
- local_templates,
642
- ) = await self._resource_manager.get_resources_and_templates()
643
- templates.update(local_templates)
644
- self._cache.set("resource_templates", templates)
645
- return list(templates.values())
646
-
647
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
648
  logger.debug("Handler called: list_prompts")
649
 
@@ -661,7 +564,7 @@ class FastMCP(Generic[LifespanResultT]):
661
  async def _handler(
662
  context: MiddlewareContext[mcp.types.ListPromptsRequest],
663
  ) -> list[Prompt]:
664
- prompts = await self._prompt_manager.list_prompts()
665
 
666
  mcp_prompts: list[Prompt] = []
667
  for prompt in prompts:
@@ -743,7 +646,7 @@ class FastMCP(Generic[LifespanResultT]):
743
 
744
  with fastmcp.server.context.Context(fastmcp=self):
745
  try:
746
- return await self._middleware_read_resource(uri)
747
  except DisabledError:
748
  # convert to NotFoundError to avoid leaking resource presence
749
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
@@ -751,25 +654,28 @@ class FastMCP(Generic[LifespanResultT]):
751
  # standardize NotFound message
752
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
753
 
754
- async def _middleware_read_resource(
755
- self,
756
- uri: AnyUrl | str,
757
- ) -> list[ReadResourceContents]:
758
  """
759
- Read a resource with middleware.
760
  """
761
 
762
  async def _handler(
763
  context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
764
  ) -> list[ReadResourceContents]:
765
- return await self._read_resource(
766
- uri=context.message.uri,
767
- )
 
 
 
 
 
 
 
 
768
 
769
  # Convert string URI to AnyUrl if needed
770
  if isinstance(uri, str):
771
- from pydantic import AnyUrl
772
-
773
  uri_param = AnyUrl(uri)
774
  else:
775
  uri_param = uri
@@ -783,25 +689,6 @@ class FastMCP(Generic[LifespanResultT]):
783
  )
784
  return await self._apply_middleware(mw_context, _handler)
785
 
786
- async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
787
- """
788
- Read a resource by URI, in the format expected by the low-level MCP
789
- server.
790
- """
791
- if await self._resource_manager.has_resource(uri):
792
- resource = await self._resource_manager.get_resource(uri)
793
- if not self._should_enable_component(resource):
794
- raise DisabledError(f"Resource {str(uri)!r} is disabled")
795
- content = await self._resource_manager.read_resource(uri)
796
- return [
797
- ReadResourceContents(
798
- content=content,
799
- mime_type=resource.mime_type,
800
- )
801
- ]
802
- else:
803
- raise NotFoundError(f"Unknown resource: {uri}")
804
-
805
  async def _mcp_get_prompt(
806
  self, name: str, arguments: dict[str, Any] | None = None
807
  ) -> GetPromptResult:
@@ -1681,7 +1568,11 @@ class FastMCP(Generic[LifespanResultT]):
1681
  server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1682
 
1683
  # Delegate mounting to all three managers
1684
- mounted_server = MountedServer(prefix=prefix, server=server)
 
 
 
 
1685
  self._tool_manager.mount(mounted_server)
1686
  self._resource_manager.mount(mounted_server)
1687
  self._prompt_manager.mount(mounted_server)
@@ -1979,6 +1870,7 @@ class FastMCP(Generic[LifespanResultT]):
1979
  class MountedServer:
1980
  prefix: str | None
1981
  server: FastMCP[Any]
 
1982
 
1983
 
1984
  def add_resource_prefix(
 
351
 
352
  async def get_resources(self) -> dict[str, Resource]:
353
  """Get all registered resources, indexed by registered key."""
354
+ return await self._resource_manager.get_resources()
 
355
 
356
  async def get_resource(self, key: str) -> Resource:
357
  resources = await self.get_resources()
 
361
 
362
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
363
  """Get all registered resource templates, indexed by registered key."""
364
+ return await self._resource_manager.get_resource_templates()
 
365
 
366
  async def get_resource_template(self, key: str) -> ResourceTemplate:
367
  templates = await self.get_resource_templates()
 
442
  async def _handler(
443
  context: MiddlewareContext[mcp.types.ListToolsRequest],
444
  ) -> list[Tool]:
445
+ tools = await self._tool_manager._list_tools() # type: ignore[reportPrivateUsage]
446
 
447
  mcp_tools: list[Tool] = []
448
  for tool in tools:
 
468
  logger.debug("Handler called: list_resources")
469
 
470
  with fastmcp.server.context.Context(fastmcp=self):
471
+ resources = await self._list_resources()
472
  return [
473
  resource.to_mcp_resource(uri=resource.key) for resource in resources
474
  ]
475
 
476
+ async def _list_resources(self) -> list[Resource]:
477
  """
478
  List all available resources, in the format expected by the low-level MCP
479
  server.
 
483
  async def _handler(
484
  context: MiddlewareContext[dict[str, Any]],
485
  ) -> list[Resource]:
486
+ resources = await self._resource_manager._list_resources() # type: ignore[reportPrivateUsage]
487
 
488
  mcp_resources: list[Resource] = []
489
  for resource in resources:
 
505
  # Apply the middleware chain.
506
  return await self._apply_middleware(mw_context, _handler)
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
509
  logger.debug("Handler called: list_resource_templates")
510
 
511
  with fastmcp.server.context.Context(fastmcp=self):
512
+ templates = await self._list_resource_templates()
513
  return [
514
  template.to_mcp_template(uriTemplate=template.key)
515
  for template in templates
516
  ]
517
 
518
+ async def _list_resource_templates(self) -> list[ResourceTemplate]:
519
  """
520
  List all available resource templates, in the format expected by the low-level MCP
521
  server.
 
525
  async def _handler(
526
  context: MiddlewareContext[dict[str, Any]],
527
  ) -> list[ResourceTemplate]:
528
+ templates = await self._resource_manager._list_resource_templates()
529
 
530
  mcp_templates: list[ResourceTemplate] = []
531
  for template in templates:
 
547
  # Apply the middleware chain.
548
  return await self._apply_middleware(mw_context, _handler)
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
551
  logger.debug("Handler called: list_prompts")
552
 
 
564
  async def _handler(
565
  context: MiddlewareContext[mcp.types.ListPromptsRequest],
566
  ) -> list[Prompt]:
567
+ prompts = await self._prompt_manager._list_prompts() # type: ignore[reportPrivateUsage]
568
 
569
  mcp_prompts: list[Prompt] = []
570
  for prompt in prompts:
 
646
 
647
  with fastmcp.server.context.Context(fastmcp=self):
648
  try:
649
+ return await self._read_resource(uri)
650
  except DisabledError:
651
  # convert to NotFoundError to avoid leaking resource presence
652
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
 
654
  # standardize NotFound message
655
  raise NotFoundError(f"Unknown resource: {str(uri)!r}")
656
 
657
+ async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
 
 
 
658
  """
659
+ Applies this server's middleware and delegates the filtered call to the manager.
660
  """
661
 
662
  async def _handler(
663
  context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
664
  ) -> list[ReadResourceContents]:
665
+ resource = await self._resource_manager.get_resource(context.message.uri)
666
+ if not self._should_enable_component(resource):
667
+ raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
668
+
669
+ content = await self._resource_manager.read_resource(context.message.uri)
670
+ return [
671
+ ReadResourceContents(
672
+ content=content,
673
+ mime_type=resource.mime_type,
674
+ )
675
+ ]
676
 
677
  # Convert string URI to AnyUrl if needed
678
  if isinstance(uri, str):
 
 
679
  uri_param = AnyUrl(uri)
680
  else:
681
  uri_param = uri
 
689
  )
690
  return await self._apply_middleware(mw_context, _handler)
691
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  async def _mcp_get_prompt(
693
  self, name: str, arguments: dict[str, Any] | None = None
694
  ) -> GetPromptResult:
 
1568
  server = FastMCPProxy(Client(transport=FastMCPTransport(server)))
1569
 
1570
  # Delegate mounting to all three managers
1571
+ mounted_server = MountedServer(
1572
+ prefix=prefix,
1573
+ server=server,
1574
+ resource_prefix_format=self.resource_prefix_format,
1575
+ )
1576
  self._tool_manager.mount(mounted_server)
1577
  self._resource_manager.mount(mounted_server)
1578
  self._prompt_manager.mount(mounted_server)
 
1870
  class MountedServer:
1871
  prefix: str | None
1872
  server: FastMCP[Any]
1873
+ resource_prefix_format: Literal["protocol", "path"] | None = None
1874
 
1875
 
1876
  def add_resource_prefix(
src/fastmcp/tools/tool_manager.py CHANGED
@@ -66,14 +66,10 @@ class ToolManager:
66
  child_results = await mounted.server._list_tools()
67
  else: # mode == "inventory"
68
  # PATH 1: Use the manager-to-manager unfiltered path
69
- child_results = await mounted.server._tool_manager.get_tools()
70
 
71
  # The combination logic is the same for both paths
72
- child_dict = (
73
- {t.key: t for t in child_results}
74
- if isinstance(child_results, list)
75
- else child_results
76
- )
77
  if mounted.prefix:
78
  for tool in child_dict.values():
79
  prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
@@ -109,7 +105,7 @@ class ToolManager:
109
  """
110
  return await self._load_tools(mode="inventory")
111
 
112
- async def list_tools(self) -> list[Tool]:
113
  """
114
  Lists all tools, applying protocol filtering.
115
  """
 
66
  child_results = await mounted.server._list_tools()
67
  else: # mode == "inventory"
68
  # PATH 1: Use the manager-to-manager unfiltered path
69
+ child_results = await mounted.server._tool_manager._list_tools()
70
 
71
  # The combination logic is the same for both paths
72
+ child_dict = {t.key: t for t in child_results}
 
 
 
 
73
  if mounted.prefix:
74
  for tool in child_dict.values():
75
  prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
 
105
  """
106
  return await self._load_tools(mode="inventory")
107
 
108
+ async def _list_tools(self) -> list[Tool]:
109
  """
110
  Lists all tools, applying protocol filtering.
111
  """
tests/resources/test_resource_manager.py CHANGED
@@ -180,7 +180,7 @@ class TestResourceManager:
180
 
181
  assert "Template already exists" in caplog.text
182
  # Should have the template
183
- assert manager.get_templates() == {"test://{id}": template}
184
 
185
  def test_error_on_duplicate_templates(self):
186
  """Test error on duplicate templates."""
@@ -226,7 +226,7 @@ class TestResourceManager:
226
  manager.add_template(template2)
227
 
228
  # Should have replaced with the new template
229
- templates = list(manager.get_templates().values())
230
  assert len(templates) == 1
231
  assert templates[0].name == "replacement"
232
 
@@ -256,7 +256,7 @@ class TestResourceManager:
256
  result = manager.add_template(template2)
257
 
258
  # Should keep the original
259
- templates = list(manager.get_templates().values())
260
  assert len(templates) == 1
261
  assert templates[0].name == "original"
262
  # Result should be the original template
@@ -379,7 +379,7 @@ class TestResourceTags:
379
  )
380
 
381
  manager.add_template(template)
382
- templates = list(manager.get_templates().values())
383
  assert len(templates) == 1
384
  assert templates[0].tags == {"users", "template", "data"}
385
 
 
180
 
181
  assert "Template already exists" in caplog.text
182
  # Should have the template
183
+ assert manager.get_resource_templates() == {"test://{id}": template}
184
 
185
  def test_error_on_duplicate_templates(self):
186
  """Test error on duplicate templates."""
 
226
  manager.add_template(template2)
227
 
228
  # Should have replaced with the new template
229
+ templates = list(manager.get_resource_templates().values())
230
  assert len(templates) == 1
231
  assert templates[0].name == "replacement"
232
 
 
256
  result = manager.add_template(template2)
257
 
258
  # Should keep the original
259
+ templates = list(manager.get_resource_templates().values())
260
  assert len(templates) == 1
261
  assert templates[0].name == "original"
262
  # Result should be the original template
 
379
  )
380
 
381
  manager.add_template(template)
382
+ templates = list(manager.get_resource_templates().values())
383
  assert len(templates) == 1
384
  assert templates[0].tags == {"users", "template", "data"}
385
 
tests/server/middleware/test_middleware.py CHANGED
@@ -172,6 +172,18 @@ class TestMiddlewareHooks:
172
  assert recording_middleware.assert_called(hook="on_request", times=1)
173
  assert recording_middleware.assert_called(hook="on_read_resource", times=1)
174
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  async def test_get_prompt(
176
  self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
177
  ):
@@ -367,6 +379,50 @@ class TestNestedMiddlewareHooks:
367
  assert nested_middleware.assert_called(hook="on_request", times=1)
368
  assert nested_middleware.assert_called(hook="on_read_resource", times=1)
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  async def test_get_prompt_on_parent_server(
371
  self,
372
  mcp_server: FastMCP,
 
172
  assert recording_middleware.assert_called(hook="on_request", times=1)
173
  assert recording_middleware.assert_called(hook="on_read_resource", times=1)
174
 
175
+ async def test_read_resource_template(
176
+ self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
177
+ ):
178
+ async with Client(mcp_server) as client:
179
+ await client.read_resource("resource://test-template/1")
180
+
181
+ assert recording_middleware.assert_called(times=3)
182
+ assert recording_middleware.assert_called(method="resources/read", times=3)
183
+ assert recording_middleware.assert_called(hook="on_message", times=1)
184
+ assert recording_middleware.assert_called(hook="on_request", times=1)
185
+ assert recording_middleware.assert_called(hook="on_read_resource", times=1)
186
+
187
  async def test_get_prompt(
188
  self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
189
  ):
 
379
  assert nested_middleware.assert_called(hook="on_request", times=1)
380
  assert nested_middleware.assert_called(hook="on_read_resource", times=1)
381
 
382
+ async def test_read_resource_template_on_parent_server(
383
+ self,
384
+ mcp_server: FastMCP,
385
+ nested_mcp_server: FastMCP,
386
+ recording_middleware: RecordingMiddleware,
387
+ nested_middleware: RecordingMiddleware,
388
+ ):
389
+ mcp_server.mount(nested_mcp_server, prefix="nested")
390
+
391
+ async with Client(mcp_server) as client:
392
+ await client.read_resource("resource://test-template/1")
393
+
394
+ assert recording_middleware.assert_called(times=3)
395
+ assert recording_middleware.assert_called(method="resources/read", times=3)
396
+ assert recording_middleware.assert_called(hook="on_message", times=1)
397
+ assert recording_middleware.assert_called(hook="on_request", times=1)
398
+ assert recording_middleware.assert_called(hook="on_read_resource", times=1)
399
+
400
+ assert nested_middleware.assert_called(times=0)
401
+
402
+ async def test_read_resource_template_on_nested_server(
403
+ self,
404
+ mcp_server: FastMCP,
405
+ nested_mcp_server: FastMCP,
406
+ recording_middleware: RecordingMiddleware,
407
+ nested_middleware: RecordingMiddleware,
408
+ ):
409
+ mcp_server.mount(nested_mcp_server, prefix="nested")
410
+
411
+ async with Client(mcp_server) as client:
412
+ await client.read_resource("resource://nested/test-template/1")
413
+
414
+ assert recording_middleware.assert_called(times=3)
415
+ assert recording_middleware.assert_called(method="resources/read", times=3)
416
+ assert recording_middleware.assert_called(hook="on_message", times=1)
417
+ assert recording_middleware.assert_called(hook="on_request", times=1)
418
+ assert recording_middleware.assert_called(hook="on_read_resource", times=1)
419
+
420
+ assert nested_middleware.assert_called(times=3)
421
+ assert nested_middleware.assert_called(method="resources/read", times=3)
422
+ assert nested_middleware.assert_called(hook="on_message", times=1)
423
+ assert nested_middleware.assert_called(hook="on_request", times=1)
424
+ assert nested_middleware.assert_called(hook="on_read_resource", times=1)
425
+
426
  async def test_get_prompt_on_parent_server(
427
  self,
428
  mcp_server: FastMCP,
tests/server/openapi/test_openapi.py CHANGED
@@ -478,7 +478,7 @@ class TestTagTransfer:
478
  ):
479
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
480
  # Get internal tools directly (not the public API which returns MCP.Content)
481
- tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
482
 
483
  # Find the create_user and update_user_name tools
484
  create_user_tool = next(
@@ -528,7 +528,7 @@ class TestTagTransfer:
528
  """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
529
  # Get internal resource templates directly
530
  templates = list(
531
- fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
532
  )
533
 
534
  # Find the get_user template
@@ -549,7 +549,7 @@ class TestTagTransfer:
549
  """Test that tags are preserved when creating resources from templates."""
550
  # Get internal resource templates directly
551
  templates = list(
552
- fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
553
  )
554
 
555
  # Find the get_user template
@@ -1537,11 +1537,11 @@ class TestFastAPIDescriptionPropagation:
1537
  print(f" Resource: {name}, Name attribute: {resource.name}")
1538
 
1539
  print("\nDEBUG - Templates created:")
1540
- for name, template in server._resource_manager.get_templates().items():
1541
  print(f" Template: {name}, Name attribute: {template.name}")
1542
 
1543
  print("\nDEBUG - Tools created:")
1544
- for tool in server._tool_manager.list_tools():
1545
  print(f" Tool: {tool.name}")
1546
 
1547
  return server
@@ -1759,7 +1759,7 @@ class TestReprMethods:
1759
  self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
1760
  ):
1761
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1762
- tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools()
1763
  tool = next(iter(tools))
1764
 
1765
  # Verify repr doesn't cause recursion and contains expected elements
@@ -1790,7 +1790,7 @@ class TestReprMethods:
1790
  ):
1791
  """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
1792
  templates = list(
1793
- fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values()
1794
  )
1795
  template = next(iter(templates))
1796
 
@@ -1836,7 +1836,7 @@ class TestEnumHandling:
1836
  )
1837
 
1838
  # Get the tools from the server
1839
- tools = server._tool_manager.list_tools()
1840
 
1841
  # Find the read_item tool
1842
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
@@ -1929,7 +1929,7 @@ class TestRouteMapWildcard:
1929
  )
1930
 
1931
  # All operations should be mapped to tools
1932
- tools = mcp._tool_manager.list_tools()
1933
  tool_names = {tool.name for tool in tools}
1934
 
1935
  # Check that all 4 operations became tools
@@ -2246,12 +2246,12 @@ class TestMCPNames:
2246
  )
2247
 
2248
  # Check tools use custom names
2249
- tools = server._tool_manager.list_tools()
2250
  tool_names = {tool.name for tool in tools}
2251
  assert "admin_create_user" in tool_names
2252
 
2253
  # Check resource templates use custom names
2254
- templates = list(server._resource_manager.get_templates().values())
2255
  template_names = {template.name for template in templates}
2256
  assert "user_detail" in template_names
2257
 
@@ -2276,10 +2276,10 @@ class TestMCPNames:
2276
  route_maps=GET_ROUTE_MAPS,
2277
  )
2278
 
2279
- tools = server._tool_manager.list_tools()
2280
  tool_names = {tool.name for tool in tools}
2281
 
2282
- templates = list(server._resource_manager.get_templates().values())
2283
  template_names = {template.name for template in templates}
2284
 
2285
  resources = list(server._resource_manager.get_resources().values())
@@ -2330,13 +2330,13 @@ class TestMCPNames:
2330
  # Check all component types
2331
  all_names = []
2332
 
2333
- tools = server._tool_manager.list_tools()
2334
  all_names.extend(tool.name for tool in tools)
2335
 
2336
  resources = list(server._resource_manager.get_resources().values())
2337
  all_names.extend(resource.name for resource in resources)
2338
 
2339
- templates = list(server._resource_manager.get_templates().values())
2340
  all_names.extend(template.name for template in templates)
2341
 
2342
  # All names should be 56 characters or less
@@ -2363,7 +2363,7 @@ class TestMCPNames:
2363
  mcp_names=mcp_names,
2364
  )
2365
 
2366
- tools = server._tool_manager.list_tools()
2367
  tool_names = {tool.name for tool in tools}
2368
  assert "openapi_user_list" in tool_names
2369
 
@@ -2395,7 +2395,7 @@ class TestMCPNames:
2395
  mcp_names=mcp_names,
2396
  )
2397
 
2398
- tools = server._tool_manager.list_tools()
2399
  tool_names = {tool.name for tool in tools}
2400
 
2401
  assert "fastapi_create_user" in tool_names
@@ -2496,7 +2496,7 @@ class TestRouteMapMCPTags:
2496
  )
2497
 
2498
  # Get the POST tool
2499
- tools = server._tool_manager.list_tools()
2500
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2501
 
2502
  assert create_user_tool is not None, "create_user tool not found"
@@ -2564,7 +2564,7 @@ class TestRouteMapMCPTags:
2564
  )
2565
 
2566
  # Get the resource template
2567
- templates = list(server._resource_manager.get_templates().values())
2568
  get_user_template = next((t for t in templates if "get_user" in t.name), None)
2569
 
2570
  assert get_user_template is not None, "get_user template not found"
@@ -2610,14 +2610,14 @@ class TestRouteMapMCPTags:
2610
  )
2611
 
2612
  # Check tool tags
2613
- tools = server._tool_manager.list_tools()
2614
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2615
  assert create_tool is not None
2616
  assert "write-operation" in create_tool.tags
2617
  assert "mutation" in create_tool.tags
2618
 
2619
  # Check resource template tags
2620
- templates = list(server._resource_manager.get_templates().values())
2621
  detail_template = next((t for t in templates if "get_user" in t.name), None)
2622
  assert detail_template is not None
2623
  assert "detail" in detail_template.tags
 
478
  ):
479
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
480
  # Get internal tools directly (not the public API which returns MCP.Content)
481
+ tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
482
 
483
  # Find the create_user and update_user_name tools
484
  create_user_tool = next(
 
528
  """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates."""
529
  # Get internal resource templates directly
530
  templates = list(
531
+ fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
532
  )
533
 
534
  # Find the get_user template
 
549
  """Test that tags are preserved when creating resources from templates."""
550
  # Get internal resource templates directly
551
  templates = list(
552
+ fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
553
  )
554
 
555
  # Find the get_user template
 
1537
  print(f" Resource: {name}, Name attribute: {resource.name}")
1538
 
1539
  print("\nDEBUG - Templates created:")
1540
+ for name, template in server._resource_manager.get_resource_templates().items():
1541
  print(f" Template: {name}, Name attribute: {template.name}")
1542
 
1543
  print("\nDEBUG - Tools created:")
1544
+ for tool in server._tool_manager._list_tools():
1545
  print(f" Tool: {tool.name}")
1546
 
1547
  return server
 
1759
  self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
1760
  ):
1761
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1762
+ tools = fastmcp_openapi_server_with_all_types._tool_manager._list_tools()
1763
  tool = next(iter(tools))
1764
 
1765
  # Verify repr doesn't cause recursion and contains expected elements
 
1790
  ):
1791
  """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
1792
  templates = list(
1793
+ fastmcp_openapi_server_with_all_types._resource_manager.get_resource_templates().values()
1794
  )
1795
  template = next(iter(templates))
1796
 
 
1836
  )
1837
 
1838
  # Get the tools from the server
1839
+ tools = server._tool_manager._list_tools()
1840
 
1841
  # Find the read_item tool
1842
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
 
1929
  )
1930
 
1931
  # All operations should be mapped to tools
1932
+ tools = mcp._tool_manager._list_tools()
1933
  tool_names = {tool.name for tool in tools}
1934
 
1935
  # Check that all 4 operations became tools
 
2246
  )
2247
 
2248
  # Check tools use custom names
2249
+ tools = server._tool_manager._list_tools()
2250
  tool_names = {tool.name for tool in tools}
2251
  assert "admin_create_user" in tool_names
2252
 
2253
  # Check resource templates use custom names
2254
+ templates = list(server._resource_manager.get_resource_templates().values())
2255
  template_names = {template.name for template in templates}
2256
  assert "user_detail" in template_names
2257
 
 
2276
  route_maps=GET_ROUTE_MAPS,
2277
  )
2278
 
2279
+ tools = server._tool_manager._list_tools()
2280
  tool_names = {tool.name for tool in tools}
2281
 
2282
+ templates = list(server._resource_manager.get_resource_templates().values())
2283
  template_names = {template.name for template in templates}
2284
 
2285
  resources = list(server._resource_manager.get_resources().values())
 
2330
  # Check all component types
2331
  all_names = []
2332
 
2333
+ tools = server._tool_manager._list_tools()
2334
  all_names.extend(tool.name for tool in tools)
2335
 
2336
  resources = list(server._resource_manager.get_resources().values())
2337
  all_names.extend(resource.name for resource in resources)
2338
 
2339
+ templates = list(server._resource_manager.get_resource_templates().values())
2340
  all_names.extend(template.name for template in templates)
2341
 
2342
  # All names should be 56 characters or less
 
2363
  mcp_names=mcp_names,
2364
  )
2365
 
2366
+ tools = server._tool_manager._list_tools()
2367
  tool_names = {tool.name for tool in tools}
2368
  assert "openapi_user_list" in tool_names
2369
 
 
2395
  mcp_names=mcp_names,
2396
  )
2397
 
2398
+ tools = server._tool_manager._list_tools()
2399
  tool_names = {tool.name for tool in tools}
2400
 
2401
  assert "fastapi_create_user" in tool_names
 
2496
  )
2497
 
2498
  # Get the POST tool
2499
+ tools = server._tool_manager._list_tools()
2500
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2501
 
2502
  assert create_user_tool is not None, "create_user tool not found"
 
2564
  )
2565
 
2566
  # Get the resource template
2567
+ templates = list(server._resource_manager.get_resource_templates().values())
2568
  get_user_template = next((t for t in templates if "get_user" in t.name), None)
2569
 
2570
  assert get_user_template is not None, "get_user template not found"
 
2610
  )
2611
 
2612
  # Check tool tags
2613
+ tools = server._tool_manager._list_tools()
2614
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2615
  assert create_tool is not None
2616
  assert "write-operation" in create_tool.tags
2617
  assert "mutation" in create_tool.tags
2618
 
2619
  # Check resource template tags
2620
+ templates = list(server._resource_manager.get_resource_templates().values())
2621
  detail_template = next((t for t in templates if "get_user" in t.name), None)
2622
  assert detail_template is not None
2623
  assert "detail" in detail_template.tags
tests/server/test_server.py CHANGED
@@ -282,7 +282,7 @@ class TestToolDecorator:
282
  return x * 2
283
 
284
  # Verify the tags were set correctly
285
- tools = mcp._tool_manager.list_tools()
286
  assert len(tools) == 1
287
  assert tools[0].tags == {"example", "test-tag"}
288
 
 
282
  return x * 2
283
 
284
  # Verify the tags were set correctly
285
+ tools = await mcp._tool_manager._list_tools()
286
  assert len(tools) == 1
287
  assert tools[0].tags == {"example", "test-tag"}
288
 
tests/server/test_tool_annotations.py CHANGED
@@ -22,7 +22,7 @@ async def test_tool_annotations_in_tool_manager():
22
  return message
23
 
24
  # Check internal tool objects directly
25
- tools = mcp._tool_manager.list_tools()
26
  assert len(tools) == 1
27
  assert tools[0].annotations is not None
28
  assert tools[0].annotations.title == "Echo Tool"
@@ -124,7 +124,7 @@ async def test_direct_tool_annotations_in_tool_manager():
124
  return {"modified": True, **data}
125
 
126
  # Check internal tool objects directly
127
- tools = mcp._tool_manager.list_tools()
128
  assert len(tools) == 1
129
  assert tools[0].annotations is not None
130
  assert tools[0].annotations.title == "Direct Tool"
@@ -183,7 +183,7 @@ async def test_add_tool_method_annotations():
183
  mcp.add_tool(tool)
184
 
185
  # Check internal tool objects directly
186
- tools = mcp._tool_manager.list_tools()
187
  assert len(tools) == 1
188
  assert tools[0].annotations is not None
189
  assert tools[0].annotations.title == "Create Item"
 
22
  return message
23
 
24
  # Check internal tool objects directly
25
+ tools = mcp._tool_manager._list_tools()
26
  assert len(tools) == 1
27
  assert tools[0].annotations is not None
28
  assert tools[0].annotations.title == "Echo Tool"
 
124
  return {"modified": True, **data}
125
 
126
  # Check internal tool objects directly
127
+ tools = mcp._tool_manager._list_tools()
128
  assert len(tools) == 1
129
  assert tools[0].annotations is not None
130
  assert tools[0].annotations.title == "Direct Tool"
 
183
  mcp.add_tool(tool)
184
 
185
  # Check internal tool objects directly
186
+ tools = mcp._tool_manager._list_tools()
187
  assert len(tools) == 1
188
  assert tools[0].annotations is not None
189
  assert tools[0].annotations.title == "Create Item"
tests/server/test_tool_exclude_args.py CHANGED
@@ -19,7 +19,7 @@ async def test_tool_exclude_args_in_tool_manager():
19
  pass
20
  return message
21
 
22
- tools = mcp._tool_manager.list_tools()
23
  assert len(tools) == 1
24
  assert "state" not in echo.parameters["properties"]
25
 
@@ -60,7 +60,7 @@ async def test_add_tool_method_exclude_args():
60
  mcp.add_tool(tool)
61
 
62
  # Check internal tool objects directly
63
- tools = mcp._tool_manager.list_tools()
64
  assert len(tools) == 1
65
  assert "state" not in tools[0].parameters["properties"]
66
 
 
19
  pass
20
  return message
21
 
22
+ tools = mcp._tool_manager._list_tools()
23
  assert len(tools) == 1
24
  assert "state" not in echo.parameters["properties"]
25
 
 
60
  mcp.add_tool(tool)
61
 
62
  # Check internal tool objects directly
63
+ tools = mcp._tool_manager._list_tools()
64
  assert len(tools) == 1
65
  assert "state" not in tools[0].parameters["properties"]
66