Jeremiah Lowin commited on
Commit
09cabfa
·
1 Parent(s): 462d3af

Fix import prefix behavior

Browse files
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -63,7 +63,7 @@ class PromptManager:
63
  child_results = await mounted.server._list_prompts()
64
  else:
65
  # Use the manager-to-manager unfiltered path
66
- child_results = await mounted.server._prompt_manager._list_prompts()
67
 
68
  # The combination logic is the same for both paths
69
  child_dict = {p.key: p for p in child_results}
@@ -104,7 +104,7 @@ class PromptManager:
104
  """
105
  return await self._load_prompts(via_server=False)
106
 
107
- async def _list_prompts(self) -> list[Prompt]:
108
  """
109
  Lists all prompts, applying protocol filtering.
110
  """
@@ -131,24 +131,22 @@ class PromptManager:
131
  )
132
  return self.add_prompt(prompt) # type: ignore
133
 
134
- def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
135
  """Add a prompt to the manager."""
136
- key = key or prompt.name
137
-
138
  # Check for duplicates
139
- existing = self._prompts.get(key)
140
  if existing:
141
  if self.duplicate_behavior == "warn":
142
- logger.warning(f"Prompt already exists: {key}")
143
- self._prompts[key] = prompt
144
  elif self.duplicate_behavior == "replace":
145
- self._prompts[key] = prompt
146
  elif self.duplicate_behavior == "error":
147
- raise ValueError(f"Prompt already exists: {key}")
148
  elif self.duplicate_behavior == "ignore":
149
  return existing
150
  else:
151
- self._prompts[key] = prompt
152
  return prompt
153
 
154
  async def render_prompt(
 
63
  child_results = await mounted.server._list_prompts()
64
  else:
65
  # Use the manager-to-manager unfiltered path
66
+ child_results = await mounted.server._prompt_manager.list_prompts()
67
 
68
  # The combination logic is the same for both paths
69
  child_dict = {p.key: p for p in child_results}
 
104
  """
105
  return await self._load_prompts(via_server=False)
106
 
107
+ async def list_prompts(self) -> list[Prompt]:
108
  """
109
  Lists all prompts, applying protocol filtering.
110
  """
 
131
  )
132
  return self.add_prompt(prompt) # type: ignore
133
 
134
+ def add_prompt(self, prompt: Prompt) -> Prompt:
135
  """Add a prompt to the manager."""
 
 
136
  # Check for duplicates
137
+ existing = self._prompts.get(prompt.key)
138
  if existing:
139
  if self.duplicate_behavior == "warn":
140
+ logger.warning(f"Prompt already exists: {prompt.key}")
141
+ self._prompts[prompt.key] = prompt
142
  elif self.duplicate_behavior == "replace":
143
+ self._prompts[prompt.key] = prompt
144
  elif self.duplicate_behavior == "error":
145
+ raise ValueError(f"Prompt already exists: {prompt.key}")
146
  elif self.duplicate_behavior == "ignore":
147
  return existing
148
  else:
149
+ self._prompts[prompt.key] = prompt
150
  return prompt
151
 
152
  async def render_prompt(
src/fastmcp/resources/resource_manager.py CHANGED
@@ -136,7 +136,9 @@ class ResourceManager:
136
  child_templates = await mounted.server._list_resource_templates()
137
  else:
138
  # Use the manager-to-manager unfiltered path
139
- child_templates = await mounted.server._resource_manager._list_resource_templates()
 
 
140
  child_dict = {template.key: template for template in child_templates}
141
 
142
  # Apply prefix if needed
@@ -163,14 +165,14 @@ class ResourceManager:
163
  all_templates.update(self._templates)
164
  return all_templates
165
 
166
- async def _list_resources(self) -> list[Resource]:
167
  """
168
  Lists all resources, applying protocol filtering.
169
  """
170
  resources_dict = await self._load_resources(via_server=True)
171
  return list(resources_dict.values())
172
 
173
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
174
  """
175
  Lists all templates, applying protocol filtering.
176
  """
@@ -265,35 +267,26 @@ class ResourceManager:
265
  )
266
  return self.add_resource(resource)
267
 
268
- def add_resource(self, resource: Resource, key: str | None = None) -> Resource:
269
  """Add a resource to the manager.
270
 
271
  Args:
272
- resource: A Resource instance to add
273
- key: Optional URI to use as the storage key (if different from resource.uri)
 
274
  """
275
- storage_key = key or str(resource.uri)
276
- logger.debug(
277
- "Adding resource",
278
- extra={
279
- "uri": resource.uri,
280
- "storage_key": storage_key,
281
- "type": type(resource).__name__,
282
- "resource_name": resource.name,
283
- },
284
- )
285
- existing = self._resources.get(storage_key)
286
  if existing:
287
  if self.duplicate_behavior == "warn":
288
- logger.warning(f"Resource already exists: {storage_key}")
289
- self._resources[storage_key] = resource
290
  elif self.duplicate_behavior == "replace":
291
- self._resources[storage_key] = resource
292
  elif self.duplicate_behavior == "error":
293
- raise ValueError(f"Resource already exists: {storage_key}")
294
  elif self.duplicate_behavior == "ignore":
295
  return existing
296
- self._resources[storage_key] = resource
297
  return resource
298
 
299
  def add_template_from_fn(
@@ -323,42 +316,30 @@ class ResourceManager:
323
  )
324
  return self.add_template(template)
325
 
326
- def add_template(
327
- self, template: ResourceTemplate, key: str | None = None
328
- ) -> ResourceTemplate:
329
  """Add a template to the manager.
330
 
331
  Args:
332
- template: A ResourceTemplate instance to add
333
- key: Optional URI template to use as the storage key (if different from template.uri_template)
 
334
 
335
  Returns:
336
  The added template. If a template with the same URI already exists,
337
  returns the existing template.
338
  """
339
- uri_template_str = str(template.uri_template)
340
- storage_key = key or uri_template_str
341
- logger.debug(
342
- "Adding template",
343
- extra={
344
- "uri_template": uri_template_str,
345
- "storage_key": storage_key,
346
- "type": type(template).__name__,
347
- "template_name": template.name,
348
- },
349
- )
350
- existing = self._templates.get(storage_key)
351
  if existing:
352
  if self.duplicate_behavior == "warn":
353
- logger.warning(f"Template already exists: {storage_key}")
354
- self._templates[storage_key] = template
355
  elif self.duplicate_behavior == "replace":
356
- self._templates[storage_key] = template
357
  elif self.duplicate_behavior == "error":
358
- raise ValueError(f"Template already exists: {storage_key}")
359
  elif self.duplicate_behavior == "ignore":
360
  return existing
361
- self._templates[storage_key] = template
362
  return template
363
 
364
  async def has_resource(self, uri: AnyUrl | str) -> bool:
 
136
  child_templates = await mounted.server._list_resource_templates()
137
  else:
138
  # Use the manager-to-manager unfiltered path
139
+ child_templates = (
140
+ await mounted.server._resource_manager.list_resource_templates()
141
+ )
142
  child_dict = {template.key: template for template in child_templates}
143
 
144
  # Apply prefix if needed
 
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(via_server=True)
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
  """
 
267
  )
268
  return self.add_resource(resource)
269
 
270
+ def add_resource(self, resource: Resource) -> Resource:
271
  """Add a resource to the manager.
272
 
273
  Args:
274
+ resource: A Resource instance to add. The resource's .key attribute
275
+ will be used as the storage key. To overwrite it, call
276
+ Resource.with_key() before calling this method.
277
  """
278
+ existing = self._resources.get(resource.key)
 
 
 
 
 
 
 
 
 
 
279
  if existing:
280
  if self.duplicate_behavior == "warn":
281
+ logger.warning(f"Resource already exists: {resource.key}")
282
+ self._resources[resource.key] = resource
283
  elif self.duplicate_behavior == "replace":
284
+ self._resources[resource.key] = resource
285
  elif self.duplicate_behavior == "error":
286
+ raise ValueError(f"Resource already exists: {resource.key}")
287
  elif self.duplicate_behavior == "ignore":
288
  return existing
289
+ self._resources[resource.key] = resource
290
  return resource
291
 
292
  def add_template_from_fn(
 
316
  )
317
  return self.add_template(template)
318
 
319
+ def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
 
 
320
  """Add a template to the manager.
321
 
322
  Args:
323
+ template: A ResourceTemplate instance to add. The template's .key attribute
324
+ will be used as the storage key. To overwrite it, call
325
+ ResourceTemplate.with_key() before calling this method.
326
 
327
  Returns:
328
  The added template. If a template with the same URI already exists,
329
  returns the existing template.
330
  """
331
+ existing = self._templates.get(template.key)
 
 
 
 
 
 
 
 
 
 
 
332
  if existing:
333
  if self.duplicate_behavior == "warn":
334
+ logger.warning(f"Template already exists: {template.key}")
335
+ self._templates[template.key] = template
336
  elif self.duplicate_behavior == "replace":
337
+ self._templates[template.key] = template
338
  elif self.duplicate_behavior == "error":
339
+ raise ValueError(f"Template already exists: {template.key}")
340
  elif self.duplicate_behavior == "ignore":
341
  return existing
342
+ self._templates[template.key] = template
343
  return template
344
 
345
  async def has_resource(self, uri: AnyUrl | str) -> bool:
src/fastmcp/server/proxy.py CHANGED
@@ -62,7 +62,7 @@ class ProxyToolManager(ToolManager):
62
 
63
  return all_tools
64
 
65
- async def _list_tools(self) -> list[Tool]:
66
  """Gets the filtered list of tools including local, mounted, and proxy tools."""
67
  tools_dict = await self.get_tools()
68
  return list(tools_dict.values())
@@ -129,12 +129,12 @@ class ProxyResourceManager(ResourceManager):
129
 
130
  return all_templates
131
 
132
- async def _list_resources(self) -> list[Resource]:
133
  """Gets the filtered list of resources including local, mounted, and proxy resources."""
134
  resources_dict = await self.get_resources()
135
  return list(resources_dict.values())
136
 
137
- async def _list_resource_templates(self) -> list[ResourceTemplate]:
138
  """Gets the filtered list of templates including local, mounted, and proxy templates."""
139
  templates_dict = await self.get_resource_templates()
140
  return list(templates_dict.values())
@@ -185,7 +185,7 @@ class ProxyPromptManager(PromptManager):
185
 
186
  return all_prompts
187
 
188
- async def _list_prompts(self) -> list[Prompt]:
189
  """Gets the filtered list of prompts including local, mounted, and proxy prompts."""
190
  prompts_dict = await self.get_prompts()
191
  return list(prompts_dict.values())
@@ -399,9 +399,3 @@ class FastMCPProxy(FastMCP):
399
  self._tool_manager = ProxyToolManager(client=self.client)
400
  self._resource_manager = ProxyResourceManager(client=self.client)
401
  self._prompt_manager = ProxyPromptManager(client=self.client)
402
-
403
- #
404
- # NO MORE METHOD OVERRIDES ARE NEEDED!
405
- # The inherited _list_tools, _call_tool, etc., from FastMCP will now
406
- # correctly delegate to the proxy managers, which in turn call the client.
407
- #
 
62
 
63
  return all_tools
64
 
65
+ async def list_tools(self) -> list[Tool]:
66
  """Gets the filtered list of tools including local, mounted, and proxy tools."""
67
  tools_dict = await self.get_tools()
68
  return list(tools_dict.values())
 
129
 
130
  return all_templates
131
 
132
+ async def list_resources(self) -> list[Resource]:
133
  """Gets the filtered list of resources including local, mounted, and proxy resources."""
134
  resources_dict = await self.get_resources()
135
  return list(resources_dict.values())
136
 
137
+ async def list_resource_templates(self) -> list[ResourceTemplate]:
138
  """Gets the filtered list of templates including local, mounted, and proxy templates."""
139
  templates_dict = await self.get_resource_templates()
140
  return list(templates_dict.values())
 
185
 
186
  return all_prompts
187
 
188
+ async def list_prompts(self) -> list[Prompt]:
189
  """Gets the filtered list of prompts including local, mounted, and proxy prompts."""
190
  prompts_dict = await self.get_prompts()
191
  return list(prompts_dict.values())
 
399
  self._tool_manager = ProxyToolManager(client=self.client)
400
  self._resource_manager = ProxyResourceManager(client=self.client)
401
  self._prompt_manager = ProxyPromptManager(client=self.client)
 
 
 
 
 
 
src/fastmcp/server/server.py CHANGED
@@ -159,7 +159,6 @@ class FastMCP(Generic[LifespanResultT]):
159
  self._cache = TimedCache(
160
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
161
  )
162
- self._mounted_servers: list[MountedServer] = []
163
  self._additional_http_routes: list[BaseRoute] = []
164
  self._tool_manager = ToolManager(
165
  duplicate_behavior=on_duplicate_tools,
@@ -442,7 +441,7 @@ class FastMCP(Generic[LifespanResultT]):
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:
@@ -483,7 +482,7 @@ class FastMCP(Generic[LifespanResultT]):
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:
@@ -525,7 +524,7 @@ class FastMCP(Generic[LifespanResultT]):
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:
@@ -564,7 +563,7 @@ class FastMCP(Generic[LifespanResultT]):
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:
@@ -902,23 +901,23 @@ class FastMCP(Generic[LifespanResultT]):
902
  enabled=enabled,
903
  )
904
 
905
- def add_resource(self, resource: Resource, key: str | None = None) -> None:
906
  """Add a resource to the server.
907
 
908
  Args:
909
  resource: A Resource instance to add
910
  """
911
 
912
- self._resource_manager.add_resource(resource, key=key)
913
  self._cache.clear()
914
 
915
- def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
916
  """Add a resource template to the server.
917
 
918
  Args:
919
  template: A ResourceTemplate instance to add
920
  """
921
- self._resource_manager.add_template(template, key=key)
922
 
923
  def add_resource_fn(
924
  self,
@@ -1670,10 +1669,8 @@ class FastMCP(Generic[LifespanResultT]):
1670
  # Import tools from the server
1671
  for key, tool in (await server.get_tools()).items():
1672
  if prefix:
1673
- tool_key = f"{prefix}_{key}"
1674
- else:
1675
- tool_key = key
1676
- self._tool_manager.add_tool(tool, key=tool_key)
1677
 
1678
  # Import resources and templates from the server
1679
  for key, resource in (await server.get_resources()).items():
@@ -1681,35 +1678,27 @@ class FastMCP(Generic[LifespanResultT]):
1681
  resource_key = add_resource_prefix(
1682
  key, prefix, self.resource_prefix_format
1683
  )
1684
- else:
1685
- resource_key = key
1686
- self._resource_manager.add_resource(resource, key=resource_key)
1687
 
1688
  for key, template in (await server.get_resource_templates()).items():
1689
  if prefix:
1690
  template_key = add_resource_prefix(
1691
  key, prefix, self.resource_prefix_format
1692
  )
1693
- else:
1694
- template_key = key
1695
- self._resource_manager.add_template(template, key=template_key)
1696
 
1697
  # Import prompts from the server
1698
  for key, prompt in (await server.get_prompts()).items():
1699
  if prefix:
1700
- prompt_key = f"{prefix}_{key}"
1701
- else:
1702
- prompt_key = key
1703
- self._prompt_manager.add_prompt(prompt, key=prompt_key)
1704
 
1705
  if prefix:
1706
- logger.info(f"Imported server {server.name} with prefix '{prefix}'")
1707
- logger.debug(f"Imported tools with prefix '{prefix}_'")
1708
- logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
1709
- logger.debug(f"Imported prompts with prefix '{prefix}_'")
1710
  else:
1711
- logger.info(f"Imported server {server.name}")
1712
- logger.debug("Imported tools, resources, templates, and prompts")
1713
 
1714
  self._cache.clear()
1715
 
 
159
  self._cache = TimedCache(
160
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
161
  )
 
162
  self._additional_http_routes: list[BaseRoute] = []
163
  self._tool_manager = ToolManager(
164
  duplicate_behavior=on_duplicate_tools,
 
441
  async def _handler(
442
  context: MiddlewareContext[mcp.types.ListToolsRequest],
443
  ) -> list[Tool]:
444
+ tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
445
 
446
  mcp_tools: list[Tool] = []
447
  for tool in tools:
 
482
  async def _handler(
483
  context: MiddlewareContext[dict[str, Any]],
484
  ) -> list[Resource]:
485
+ resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
486
 
487
  mcp_resources: list[Resource] = []
488
  for resource in resources:
 
524
  async def _handler(
525
  context: MiddlewareContext[dict[str, Any]],
526
  ) -> list[ResourceTemplate]:
527
+ templates = await self._resource_manager.list_resource_templates()
528
 
529
  mcp_templates: list[ResourceTemplate] = []
530
  for template in templates:
 
563
  async def _handler(
564
  context: MiddlewareContext[mcp.types.ListPromptsRequest],
565
  ) -> list[Prompt]:
566
+ prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
567
 
568
  mcp_prompts: list[Prompt] = []
569
  for prompt in prompts:
 
901
  enabled=enabled,
902
  )
903
 
904
+ def add_resource(self, resource: Resource) -> None:
905
  """Add a resource to the server.
906
 
907
  Args:
908
  resource: A Resource instance to add
909
  """
910
 
911
+ self._resource_manager.add_resource(resource)
912
  self._cache.clear()
913
 
914
+ def add_template(self, template: ResourceTemplate) -> None:
915
  """Add a resource template to the server.
916
 
917
  Args:
918
  template: A ResourceTemplate instance to add
919
  """
920
+ self._resource_manager.add_template(template)
921
 
922
  def add_resource_fn(
923
  self,
 
1669
  # Import tools from the server
1670
  for key, tool in (await server.get_tools()).items():
1671
  if prefix:
1672
+ tool = tool.with_key(f"{prefix}_{key}")
1673
+ self._tool_manager.add_tool(tool)
 
 
1674
 
1675
  # Import resources and templates from the server
1676
  for key, resource in (await server.get_resources()).items():
 
1678
  resource_key = add_resource_prefix(
1679
  key, prefix, self.resource_prefix_format
1680
  )
1681
+ resource = resource.with_key(resource_key)
1682
+ self._resource_manager.add_resource(resource)
 
1683
 
1684
  for key, template in (await server.get_resource_templates()).items():
1685
  if prefix:
1686
  template_key = add_resource_prefix(
1687
  key, prefix, self.resource_prefix_format
1688
  )
1689
+ template = template.with_key(template_key)
1690
+ self._resource_manager.add_template(template)
 
1691
 
1692
  # Import prompts from the server
1693
  for key, prompt in (await server.get_prompts()).items():
1694
  if prefix:
1695
+ prompt = prompt.with_key(f"{prefix}_{key}")
1696
+ self._prompt_manager.add_prompt(prompt)
 
 
1697
 
1698
  if prefix:
1699
+ logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
 
 
 
1700
  else:
1701
+ logger.debug(f"Imported server {server.name}")
 
1702
 
1703
  self._cache.clear()
1704
 
src/fastmcp/tools/tool_manager.py CHANGED
@@ -64,7 +64,7 @@ class ToolManager:
64
  child_results = await mounted.server._list_tools()
65
  else:
66
  # Use the manager-to-manager unfiltered path
67
- child_results = await mounted.server._tool_manager._list_tools()
68
 
69
  # The combination logic is the same for both paths
70
  child_dict = {t.key: t for t in child_results}
@@ -103,7 +103,7 @@ class ToolManager:
103
  """
104
  return await self._load_tools(via_server=False)
105
 
106
- async def _list_tools(self) -> list[Tool]:
107
  """
108
  Lists all tools, applying protocol filtering.
109
  """
@@ -139,22 +139,21 @@ class ToolManager:
139
  )
140
  return self.add_tool(tool)
141
 
142
- def add_tool(self, tool: Tool, key: str | None = None) -> Tool:
143
  """Register a tool with the server."""
144
- key = key or tool.name
145
- existing = self._tools.get(key)
146
  if existing:
147
  if self.duplicate_behavior == "warn":
148
- logger.warning(f"Tool already exists: {key}")
149
- self._tools[key] = tool
150
  elif self.duplicate_behavior == "replace":
151
- self._tools[key] = tool
152
  elif self.duplicate_behavior == "error":
153
- raise ValueError(f"Tool already exists: {key}")
154
  elif self.duplicate_behavior == "ignore":
155
  return existing
156
  else:
157
- self._tools[key] = tool
158
  return tool
159
 
160
  def remove_tool(self, key: str) -> None:
 
64
  child_results = await mounted.server._list_tools()
65
  else:
66
  # Use the manager-to-manager unfiltered path
67
+ child_results = await mounted.server._tool_manager.list_tools()
68
 
69
  # The combination logic is the same for both paths
70
  child_dict = {t.key: t for t in child_results}
 
103
  """
104
  return await self._load_tools(via_server=False)
105
 
106
+ async def list_tools(self) -> list[Tool]:
107
  """
108
  Lists all tools, applying protocol filtering.
109
  """
 
139
  )
140
  return self.add_tool(tool)
141
 
142
+ def add_tool(self, tool: Tool) -> Tool:
143
  """Register a tool with the server."""
144
+ existing = self._tools.get(tool.key)
 
145
  if existing:
146
  if self.duplicate_behavior == "warn":
147
+ logger.warning(f"Tool already exists: {tool.key}")
148
+ self._tools[tool.key] = tool
149
  elif self.duplicate_behavior == "replace":
150
+ self._tools[tool.key] = tool
151
  elif self.duplicate_behavior == "error":
152
+ raise ValueError(f"Tool already exists: {tool.key}")
153
  elif self.duplicate_behavior == "ignore":
154
  return existing
155
  else:
156
+ self._tools[tool.key] = tool
157
  return tool
158
 
159
  def remove_tool(self, key: str) -> None:
tests/client/test_client.py CHANGED
@@ -833,7 +833,12 @@ class TestInferTransport:
833
  transport = infer_transport(config)
834
  assert isinstance(transport, MCPConfigTransport)
835
  assert isinstance(transport.transport, FastMCPTransport)
836
- assert len(cast(FastMCP, transport.transport.server)._mounted_servers) == 2
 
 
 
 
 
837
 
838
  def test_infer_fastmcp_server(self, fastmcp_server):
839
  """FastMCP server instances should infer to FastMCPTransport."""
 
833
  transport = infer_transport(config)
834
  assert isinstance(transport, MCPConfigTransport)
835
  assert isinstance(transport.transport, FastMCPTransport)
836
+ assert (
837
+ len(
838
+ cast(FastMCP, transport.transport.server)._tool_manager._mounted_servers
839
+ )
840
+ == 2
841
+ )
842
 
843
  def test_infer_fastmcp_server(self, fastmcp_server):
844
  """FastMCP server instances should infer to FastMCPTransport."""
tests/deprecated/test_resource_prefixes.py CHANGED
@@ -99,7 +99,7 @@ async def test_import_server_with_legacy_prefixes():
99
  await main_server.import_server("sub", sub_server) # type: ignore[arg-type]
100
 
101
  # Check that the resource is prefixed using the legacy format
102
- resources = main_server._resource_manager.get_resources()
103
 
104
  # In legacy format, the key would be "sub+resource://test"
105
  assert "sub+resource://test" in resources
 
99
  await main_server.import_server("sub", sub_server) # type: ignore[arg-type]
100
 
101
  # Check that the resource is prefixed using the legacy format
102
+ resources = await main_server.get_resources()
103
 
104
  # In legacy format, the key would be "sub+resource://test"
105
  assert "sub+resource://test" in resources
tests/server/openapi/test_openapi.py CHANGED
@@ -472,7 +472,7 @@ class TestTagTransfer:
472
  ):
473
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
474
  # Get internal tools directly (not the public API which returns MCP.Content)
475
- tools = await fastmcp_openapi_server._tool_manager._list_tools()
476
 
477
  # Find the create_user and update_user_name tools
478
  create_user_tool = next(
@@ -1546,7 +1546,7 @@ class TestFastAPIDescriptionPropagation:
1546
  print(f" Template: {name}, Name attribute: {template.name}")
1547
 
1548
  print("\nDEBUG - Tools created:")
1549
- tools = await server._tool_manager._list_tools()
1550
  for tool in tools:
1551
  print(f" Tool: {tool.name}")
1552
 
@@ -1779,7 +1779,7 @@ class TestReprMethods:
1779
 
1780
  async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
1781
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1782
- tools = await fastmcp_openapi_server._tool_manager._list_tools()
1783
  tool = next(iter(tools))
1784
 
1785
  # Verify repr doesn't cause recursion and contains expected elements
@@ -1854,7 +1854,7 @@ class TestEnumHandling:
1854
  )
1855
 
1856
  # Get the tools from the server
1857
- tools = await server._tool_manager._list_tools()
1858
 
1859
  # Find the read_item tool
1860
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
@@ -1947,7 +1947,7 @@ class TestRouteMapWildcard:
1947
  )
1948
 
1949
  # All operations should be mapped to tools
1950
- tools = await mcp._tool_manager._list_tools()
1951
  tool_names = {tool.name for tool in tools}
1952
 
1953
  # Check that all 4 operations became tools
@@ -2264,7 +2264,7 @@ class TestMCPNames:
2264
  )
2265
 
2266
  # Check tools use custom names
2267
- tools = await server._tool_manager._list_tools()
2268
  tool_names = {tool.name for tool in tools}
2269
  assert "admin_create_user" in tool_names
2270
 
@@ -2294,7 +2294,7 @@ class TestMCPNames:
2294
  route_maps=GET_ROUTE_MAPS,
2295
  )
2296
 
2297
- tools = await server._tool_manager._list_tools()
2298
  tool_names = {tool.name for tool in tools}
2299
 
2300
  templates_dict = await server._resource_manager.get_resource_templates()
@@ -2350,7 +2350,7 @@ class TestMCPNames:
2350
  # Check all component types
2351
  all_names = []
2352
 
2353
- tools = await server._tool_manager._list_tools()
2354
  all_names.extend(tool.name for tool in tools)
2355
 
2356
  resources_dict = await server._resource_manager.get_resources()
@@ -2383,7 +2383,7 @@ class TestMCPNames:
2383
  mcp_names=mcp_names,
2384
  )
2385
 
2386
- tools = await server._tool_manager._list_tools()
2387
  tool_names = {tool.name for tool in tools}
2388
  assert "openapi_user_list" in tool_names
2389
 
@@ -2415,7 +2415,7 @@ class TestMCPNames:
2415
  mcp_names=mcp_names,
2416
  )
2417
 
2418
- tools = await server._tool_manager._list_tools()
2419
  tool_names = {tool.name for tool in tools}
2420
 
2421
  assert "fastapi_create_user" in tool_names
@@ -2518,7 +2518,7 @@ class TestRouteMapMCPTags:
2518
  )
2519
 
2520
  # Get the POST tool
2521
- tools = await server._tool_manager._list_tools()
2522
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2523
 
2524
  assert create_user_tool is not None, "create_user tool not found"
@@ -2634,7 +2634,7 @@ class TestRouteMapMCPTags:
2634
  )
2635
 
2636
  # Check tool tags
2637
- tools = await server._tool_manager._list_tools()
2638
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2639
  assert create_tool is not None
2640
  assert "write-operation" in create_tool.tags
 
472
  ):
473
  """Test that tags from OpenAPI routes are correctly transferred to Tools."""
474
  # Get internal tools directly (not the public API which returns MCP.Content)
475
+ tools = await fastmcp_openapi_server._tool_manager.list_tools()
476
 
477
  # Find the create_user and update_user_name tools
478
  create_user_tool = next(
 
1546
  print(f" Template: {name}, Name attribute: {template.name}")
1547
 
1548
  print("\nDEBUG - Tools created:")
1549
+ tools = await server._tool_manager.list_tools()
1550
  for tool in tools:
1551
  print(f" Tool: {tool.name}")
1552
 
 
1779
 
1780
  async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
1781
  """Test that OpenAPITool's __repr__ method works without recursion errors."""
1782
+ tools = await fastmcp_openapi_server._tool_manager.list_tools()
1783
  tool = next(iter(tools))
1784
 
1785
  # Verify repr doesn't cause recursion and contains expected elements
 
1854
  )
1855
 
1856
  # Get the tools from the server
1857
+ tools = await server._tool_manager.list_tools()
1858
 
1859
  # Find the read_item tool
1860
  read_item_tool = next((t for t in tools if t.name == "read_item_items"), None)
 
1947
  )
1948
 
1949
  # All operations should be mapped to tools
1950
+ tools = await mcp._tool_manager.list_tools()
1951
  tool_names = {tool.name for tool in tools}
1952
 
1953
  # Check that all 4 operations became tools
 
2264
  )
2265
 
2266
  # Check tools use custom names
2267
+ tools = await server._tool_manager.list_tools()
2268
  tool_names = {tool.name for tool in tools}
2269
  assert "admin_create_user" in tool_names
2270
 
 
2294
  route_maps=GET_ROUTE_MAPS,
2295
  )
2296
 
2297
+ tools = await server._tool_manager.list_tools()
2298
  tool_names = {tool.name for tool in tools}
2299
 
2300
  templates_dict = await server._resource_manager.get_resource_templates()
 
2350
  # Check all component types
2351
  all_names = []
2352
 
2353
+ tools = await server._tool_manager.list_tools()
2354
  all_names.extend(tool.name for tool in tools)
2355
 
2356
  resources_dict = await server._resource_manager.get_resources()
 
2383
  mcp_names=mcp_names,
2384
  )
2385
 
2386
+ tools = await server._tool_manager.list_tools()
2387
  tool_names = {tool.name for tool in tools}
2388
  assert "openapi_user_list" in tool_names
2389
 
 
2415
  mcp_names=mcp_names,
2416
  )
2417
 
2418
+ tools = await server._tool_manager.list_tools()
2419
  tool_names = {tool.name for tool in tools}
2420
 
2421
  assert "fastapi_create_user" in tool_names
 
2518
  )
2519
 
2520
  # Get the POST tool
2521
+ tools = await server._tool_manager.list_tools()
2522
  create_user_tool = next((t for t in tools if "create_user" in t.name), None)
2523
 
2524
  assert create_user_tool is not None, "create_user tool not found"
 
2634
  )
2635
 
2636
  # Check tool tags
2637
+ tools = await server._tool_manager.list_tools()
2638
  create_tool = next((t for t in tools if "create_user" in t.name), None)
2639
  assert create_tool is not None
2640
  assert "write-operation" in create_tool.tags
tests/server/test_import_server.py CHANGED
@@ -25,7 +25,7 @@ async def test_import_basic_functionality():
25
  assert "sub_tool" in sub_app._tool_manager._tools
26
 
27
  # Verify the original tool still exists in the sub-app
28
- tool = main_app._tool_manager.get_tool("sub_sub_tool")
29
  assert tool is not None
30
  assert tool.name == "sub_tool"
31
  assert isinstance(tool, FunctionTool)
@@ -203,7 +203,7 @@ async def test_tool_custom_name_preserved_when_imported():
203
  await main_app.import_server(api_app, "api")
204
 
205
  # Check that the tool is accessible by its prefixed name
206
- tool = main_app._tool_manager.get_tool("api_get_data")
207
  assert tool is not None
208
 
209
  # Check that the function name is preserved
@@ -239,7 +239,7 @@ async def test_first_level_importing_with_custom_name():
239
  await service_app.import_server(provider_app, "provider")
240
 
241
  # Tool is accessible in the service app with the first prefix
242
- tool = service_app._tool_manager.get_tool("provider_compute")
243
  assert tool is not None
244
  assert isinstance(tool, FunctionTool)
245
  assert tool.fn.__name__ == "calculate_value"
@@ -259,7 +259,7 @@ async def test_nested_importing_preserves_prefixes():
259
  await main_app.import_server(service_app, "service")
260
 
261
  # Tool is accessible in the main app with both prefixes
262
- tool = main_app._tool_manager.get_tool("service_provider_compute")
263
  assert tool is not None
264
 
265
 
 
25
  assert "sub_tool" in sub_app._tool_manager._tools
26
 
27
  # Verify the original tool still exists in the sub-app
28
+ tool = await main_app._tool_manager.get_tool("sub_sub_tool")
29
  assert tool is not None
30
  assert tool.name == "sub_tool"
31
  assert isinstance(tool, FunctionTool)
 
203
  await main_app.import_server(api_app, "api")
204
 
205
  # Check that the tool is accessible by its prefixed name
206
+ tool = await main_app._tool_manager.get_tool("api_get_data")
207
  assert tool is not None
208
 
209
  # Check that the function name is preserved
 
239
  await service_app.import_server(provider_app, "provider")
240
 
241
  # Tool is accessible in the service app with the first prefix
242
+ tool = await service_app._tool_manager.get_tool("provider_compute")
243
  assert tool is not None
244
  assert isinstance(tool, FunctionTool)
245
  assert tool.fn.__name__ == "calculate_value"
 
259
  await main_app.import_server(service_app, "service")
260
 
261
  # Tool is accessible in the main app with both prefixes
262
+ tool = await main_app._tool_manager.get_tool("service_provider_compute")
263
  assert tool is not None
264
 
265
 
tests/server/test_mount.py CHANGED
@@ -862,7 +862,7 @@ class TestAsProxyKwarg:
862
  sub = FastMCP("Sub")
863
 
864
  mcp.mount(sub, "sub")
865
- assert mcp._mounted_servers[0].server is sub
866
 
867
  async def test_as_proxy_false(self):
868
  mcp = FastMCP("Main")
@@ -870,7 +870,7 @@ class TestAsProxyKwarg:
870
 
871
  mcp.mount(sub, "sub", as_proxy=False)
872
 
873
- assert mcp._mounted_servers[0].server is sub
874
 
875
  async def test_as_proxy_true(self):
876
  mcp = FastMCP("Main")
@@ -878,8 +878,8 @@ class TestAsProxyKwarg:
878
 
879
  mcp.mount(sub, "sub", as_proxy=True)
880
 
881
- assert mcp._mounted_servers[0].server is not sub
882
- assert isinstance(mcp._mounted_servers[0].server, FastMCPProxy)
883
 
884
  async def test_as_proxy_defaults_true_if_lifespan(self):
885
  @asynccontextmanager
@@ -891,8 +891,8 @@ class TestAsProxyKwarg:
891
 
892
  mcp.mount(sub, "sub")
893
 
894
- assert mcp._mounted_servers[0].server is not sub
895
- assert isinstance(mcp._mounted_servers[0].server, FastMCPProxy)
896
 
897
  async def test_as_proxy_ignored_for_proxy_mounts_default(self):
898
  mcp = FastMCP("Main")
@@ -901,7 +901,7 @@ class TestAsProxyKwarg:
901
 
902
  mcp.mount(sub_proxy, "sub")
903
 
904
- assert mcp._mounted_servers[0].server is sub_proxy
905
 
906
  async def test_as_proxy_ignored_for_proxy_mounts_false(self):
907
  mcp = FastMCP("Main")
@@ -910,7 +910,7 @@ class TestAsProxyKwarg:
910
 
911
  mcp.mount(sub_proxy, "sub", as_proxy=False)
912
 
913
- assert mcp._mounted_servers[0].server is sub_proxy
914
 
915
  async def test_as_proxy_ignored_for_proxy_mounts_true(self):
916
  mcp = FastMCP("Main")
@@ -919,7 +919,7 @@ class TestAsProxyKwarg:
919
 
920
  mcp.mount(sub_proxy, "sub", as_proxy=True)
921
 
922
- assert mcp._mounted_servers[0].server is sub_proxy
923
 
924
  async def test_as_proxy_mounts_still_have_live_link(self):
925
  mcp = FastMCP("Main")
@@ -950,11 +950,14 @@ class TestAsProxyKwarg:
950
  def hello():
951
  return "hi"
952
 
953
- mcp.mount(sub, "sub", as_proxy=True)
954
 
955
  assert lifespan_check == []
956
 
957
  async with Client(mcp) as client:
958
- await client.call_tool("sub_hello", {})
959
 
960
- assert lifespan_check == ["start"]
 
 
 
 
862
  sub = FastMCP("Sub")
863
 
864
  mcp.mount(sub, "sub")
865
+ assert mcp._tool_manager._mounted_servers[0].server is sub
866
 
867
  async def test_as_proxy_false(self):
868
  mcp = FastMCP("Main")
 
870
 
871
  mcp.mount(sub, "sub", as_proxy=False)
872
 
873
+ assert mcp._tool_manager._mounted_servers[0].server is sub
874
 
875
  async def test_as_proxy_true(self):
876
  mcp = FastMCP("Main")
 
878
 
879
  mcp.mount(sub, "sub", as_proxy=True)
880
 
881
+ assert mcp._tool_manager._mounted_servers[0].server is not sub
882
+ assert isinstance(mcp._tool_manager._mounted_servers[0].server, FastMCPProxy)
883
 
884
  async def test_as_proxy_defaults_true_if_lifespan(self):
885
  @asynccontextmanager
 
891
 
892
  mcp.mount(sub, "sub")
893
 
894
+ assert mcp._tool_manager._mounted_servers[0].server is not sub
895
+ assert isinstance(mcp._tool_manager._mounted_servers[0].server, FastMCPProxy)
896
 
897
  async def test_as_proxy_ignored_for_proxy_mounts_default(self):
898
  mcp = FastMCP("Main")
 
901
 
902
  mcp.mount(sub_proxy, "sub")
903
 
904
+ assert mcp._tool_manager._mounted_servers[0].server is sub_proxy
905
 
906
  async def test_as_proxy_ignored_for_proxy_mounts_false(self):
907
  mcp = FastMCP("Main")
 
910
 
911
  mcp.mount(sub_proxy, "sub", as_proxy=False)
912
 
913
+ assert mcp._tool_manager._mounted_servers[0].server is sub_proxy
914
 
915
  async def test_as_proxy_ignored_for_proxy_mounts_true(self):
916
  mcp = FastMCP("Main")
 
919
 
920
  mcp.mount(sub_proxy, "sub", as_proxy=True)
921
 
922
+ assert mcp._tool_manager._mounted_servers[0].server is sub_proxy
923
 
924
  async def test_as_proxy_mounts_still_have_live_link(self):
925
  mcp = FastMCP("Main")
 
950
  def hello():
951
  return "hi"
952
 
953
+ mcp.mount(sub, as_proxy=True)
954
 
955
  assert lifespan_check == []
956
 
957
  async with Client(mcp) as client:
958
+ await client.call_tool("hello", {})
959
 
960
+ assert len(lifespan_check) > 0
961
+ # in the present implementation the sub server will be invoked 3 times
962
+ # to call its tool
963
+ assert lifespan_check == ["start", "start", "start"]
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 = await 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