Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
09cabfa
1
Parent(s): 462d3af
Fix import prefix behavior
Browse files- src/fastmcp/prompts/prompt_manager.py +9 -11
- src/fastmcp/resources/resource_manager.py +25 -44
- src/fastmcp/server/proxy.py +4 -10
- src/fastmcp/server/server.py +18 -29
- src/fastmcp/tools/tool_manager.py +9 -10
- tests/client/test_client.py +6 -1
- tests/deprecated/test_resource_prefixes.py +1 -1
- tests/server/openapi/test_openapi.py +12 -12
- tests/server/test_import_server.py +4 -4
- tests/server/test_mount.py +15 -12
- tests/server/test_server.py +1 -1
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.
|
| 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
|
| 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
|
| 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 =
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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
|
| 269 |
"""Add a resource to the manager.
|
| 270 |
|
| 271 |
Args:
|
| 272 |
-
resource: A Resource instance to add
|
| 273 |
-
|
|
|
|
| 274 |
"""
|
| 275 |
-
|
| 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: {
|
| 289 |
-
self._resources[
|
| 290 |
elif self.duplicate_behavior == "replace":
|
| 291 |
-
self._resources[
|
| 292 |
elif self.duplicate_behavior == "error":
|
| 293 |
-
raise ValueError(f"Resource already exists: {
|
| 294 |
elif self.duplicate_behavior == "ignore":
|
| 295 |
return existing
|
| 296 |
-
self._resources[
|
| 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 |
-
|
|
|
|
| 334 |
|
| 335 |
Returns:
|
| 336 |
The added template. If a template with the same URI already exists,
|
| 337 |
returns the existing template.
|
| 338 |
"""
|
| 339 |
-
|
| 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: {
|
| 354 |
-
self._templates[
|
| 355 |
elif self.duplicate_behavior == "replace":
|
| 356 |
-
self._templates[
|
| 357 |
elif self.duplicate_behavior == "error":
|
| 358 |
-
raise ValueError(f"Template already exists: {
|
| 359 |
elif self.duplicate_behavior == "ignore":
|
| 360 |
return existing
|
| 361 |
-
self._templates[
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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
|
| 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
|
| 913 |
self._cache.clear()
|
| 914 |
|
| 915 |
-
def add_template(self, template: ResourceTemplate
|
| 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
|
| 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 |
-
|
| 1674 |
-
|
| 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 |
-
|
| 1685 |
-
|
| 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 |
-
|
| 1694 |
-
|
| 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 |
-
|
| 1701 |
-
|
| 1702 |
-
prompt_key = key
|
| 1703 |
-
self._prompt_manager.add_prompt(prompt, key=prompt_key)
|
| 1704 |
|
| 1705 |
if prefix:
|
| 1706 |
-
logger.
|
| 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.
|
| 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.
|
| 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
|
| 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
|
| 143 |
"""Register a tool with the server."""
|
| 144 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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,
|
| 954 |
|
| 955 |
assert lifespan_check == []
|
| 956 |
|
| 957 |
async with Client(mcp) as client:
|
| 958 |
-
await client.call_tool("
|
| 959 |
|
| 960 |
-
assert lifespan_check
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
|