Permit empty prefixes

This commit is contained in:
Jeremiah Lowin 2025-04-07 18:52:00 -04:00
commit 728aeec293
7 changed files with 68 additions and 46 deletions

View file

@ -11,20 +11,23 @@ class PromptManager(BasePromptManager):
Adds ability to import prompts from other managers with prefixed names.
"""
def import_prompts(self, manager: "PromptManager", prefix: str) -> None:
def import_prompts(
self, manager: "PromptManager", prefix: str | None = None
) -> None:
"""
Import all prompts from another PromptManager with prefixed names.
Args:
manager: Another PromptManager instance to import prompts from
prefix: Prefix to add to prompt names. The resulting prompt name will
be in the format "{prefix}/{original_name}"
For example, with prefix "weather" and prompt "forecast_prompt",
be in the format "{prefix}{original_name}" if prefix is provided,
otherwise the original name is used.
For example, with prefix "weather/" and prompt "forecast_prompt",
the imported prompt would be available as "weather/forecast_prompt"
"""
for name, prompt in manager._prompts.items():
# Create prefixed name - we keep the original name in the Prompt object
prefixed_name = f"{prefix}/{name}"
prefixed_name = f"{prefix}{name}" if prefix else name
# Log the import
logger.debug(f"Importing prompt with name {name} as {prefixed_name}")

View file

@ -10,20 +10,25 @@ logger = logging.getLogger(__name__)
class ResourceManager(BaseResourceManager):
"""ResourceManager that adds methods to import resources from other managers."""
def import_resources(self, manager: "ResourceManager", prefix: str) -> None:
def import_resources(
self, manager: "ResourceManager", prefix: str | None = None
) -> None:
"""Import resources from another resource manager.
Resources are imported with a prefixed URI. For example, if a resource has
URI "data://users" and you import it with prefix "app", the imported resource
will have URI "app+data://users".
Resources are imported with a prefixed URI if a prefix is provided. For example,
if a resource has URI "data://users" and you import it with prefix "app+", the
imported resource will have URI "app+data://users". If no prefix is provided,
the original URI is used.
Args:
manager: The ResourceManager to import from
prefix: A prefix to apply to the resource URIs
prefix: A prefix to apply to the resource URIs, including the delimiter.
For example, "app+" would result in URIs like "app+data://users".
If None, the original URI is used.
"""
for uri, resource in manager._resources.items():
# Create prefixed URI and copy the resource with the new URI
prefixed_uri = f"{prefix}+{uri}"
prefixed_uri = f"{prefix}{uri}" if prefix else uri
# Log the import
logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
@ -31,20 +36,27 @@ class ResourceManager(BaseResourceManager):
# Store directly in resources dictionary
self._resources[prefixed_uri] = resource
def import_templates(self, manager: "ResourceManager", prefix: str) -> None:
def import_templates(
self, manager: "ResourceManager", prefix: str | None = None
) -> None:
"""Import resource templates from another resource manager.
Templates are imported with a prefixed URI template. For example, if a template has
URI template "data://users/{id}" and you import it with prefix "app", the
imported template will have URI template "app+data://users/{id}".
Templates are imported with a prefixed URI template if a prefix is provided.
For example, if a template has URI template "data://users/{id}" and you import
it with prefix "app+", the imported template will have URI template
"app+data://users/{id}". If no prefix is provided, the original URI template is used.
Args:
manager: The ResourceManager to import templates from
prefix: A prefix to apply to the template URIs
prefix: A prefix to apply to the template URIs, including the delimiter.
For example, "app+" would result in URI templates like "app+data://users/{id}".
If None, the original URI template is used.
"""
for uri_template, template in manager._templates.items():
# Create prefixed URI template and copy the template with the new URI template
prefixed_uri_template = f"{prefix}+{uri_template}"
prefixed_uri_template = (
f"{prefix}{uri_template}" if prefix else uri_template
)
# Log the import
logger.debug(

View file

@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import TYPE_CHECKING, Any, Dict
import mcp.server.fastmcp
import mcp.types
@ -9,6 +9,9 @@ from fastmcp.server.context import Context
from fastmcp.tools.tool_manager import ToolManager
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
pass
logger = get_logger(__name__)
@ -62,20 +65,21 @@ class FastMCP(mcp.server.fastmcp.FastMCP):
# Mount the app in the list of mounted apps
self._mounted_apps[prefix] = app
# Import tools from the mounted app
self._tool_manager.import_tools(app._tool_manager, prefix)
# Import tools from the mounted app with / delimiter
tool_prefix = f"{prefix}/"
self._tool_manager.import_tools(app._tool_manager, tool_prefix)
# Import resources from the mounted app
self._resource_manager.import_resources(app._resource_manager, prefix)
# Import resources and templates from the mounted app with + delimiter
resource_prefix = f"{prefix}+"
self._resource_manager.import_resources(app._resource_manager, resource_prefix)
self._resource_manager.import_templates(app._resource_manager, resource_prefix)
# Import resource templates
self._resource_manager.import_templates(app._resource_manager, prefix)
# Import prompts
self._prompt_manager.import_prompts(app._prompt_manager, prefix)
# Import prompts with / delimiter
prompt_prefix = f"{prefix}/"
self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
logger.info(f"Mounted app with prefix '{prefix}'")
logger.debug(f"Imported tools with prefix '{prefix}/'")
logger.debug(f"Imported resources with prefix '{prefix}+'")
logger.debug(f"Imported templates with prefix '{prefix}+'")
logger.debug(f"Imported prompts with prefix '{prefix}/'")
logger.debug(f"Imported tools with prefix '{tool_prefix}'")
logger.debug(f"Imported resources with prefix '{resource_prefix}'")
logger.debug(f"Imported templates with prefix '{resource_prefix}'")
logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")

View file

@ -12,19 +12,22 @@ class ToolManager(mcp.server.fastmcp.tools.ToolManager):
Adds ability to import tools from other managers with prefixed names.
"""
def import_tools(self, tool_manager: "ToolManager", prefix: str) -> None:
def import_tools(
self, tool_manager: "ToolManager", prefix: str | None = None
) -> None:
"""
Import all tools from another ToolManager with prefixed names.
Args:
tool_manager: Another ToolManager instance to import tools from
prefix: Prefix to add to tool names. The resulting tool name will
be in the format "{prefix}/{original_name}"
For example, with prefix "weather" and tool "forecast",
prefix: Prefix to add to tool names, including the delimiter.
The resulting tool name will be in the format "{prefix}{original_name}"
if prefix is provided, otherwise the original name is used.
For example, with prefix "weather/" and tool "forecast",
the imported tool would be available as "weather/forecast"
"""
for name, tool in tool_manager._tools.items():
prefixed_name = f"{prefix}/{name}"
prefixed_name = f"{prefix}{name}" if prefix else name
# Create a shallow copy of the tool with the prefixed name
copied_tool = Tool.from_function(

View file

@ -44,7 +44,7 @@ def test_import_prompts():
target_manager = PromptManager()
# Import prompts from source to target
prefix = "nlp"
prefix = "nlp/"
target_manager.import_prompts(source_manager, prefix)
# Verify prompts were imported with prefixes
@ -109,7 +109,7 @@ def test_import_prompts_with_duplicates():
target_manager._prompts["common"] = target_prompt
# Import prompts with prefix
prefix = "external"
prefix = "external/"
target_manager.import_prompts(source_manager, prefix)
# Verify both prompts exist in target manager
@ -146,10 +146,10 @@ def test_import_prompts_with_nested_prefixes():
first_manager._prompts["analyze"] = original_prompt
# Import to second manager with prefix
second_manager.import_prompts(first_manager, "text")
second_manager.import_prompts(first_manager, "text/")
# Import from second to third with another prefix
third_manager.import_prompts(second_manager, "ai")
third_manager.import_prompts(second_manager, "ai/")
# Verify the nested prefixing
assert "text/analyze" in second_manager._prompts

View file

@ -39,7 +39,7 @@ def test_import_resources():
target_manager = ResourceManager()
# Import resources from source to target
prefix = "data"
prefix = "data+"
target_manager.import_resources(source_manager, prefix)
# Verify resources were imported with prefixes
@ -126,7 +126,7 @@ def test_import_templates():
target_manager = ResourceManager()
# Import templates from source to target
prefix = "shop"
prefix = "shop+"
target_manager.import_templates(source_manager, prefix)
# Verify templates were imported with prefixes
@ -212,7 +212,7 @@ def test_import_multiple_resource_types():
target_manager = ResourceManager()
# Import both resources and templates
prefix = "test"
prefix = "test+"
target_manager.import_resources(source_manager, prefix)
target_manager.import_templates(source_manager, prefix)

View file

@ -23,7 +23,7 @@ def test_import_tools():
target_manager = ToolManager()
# Import tools from source to target
prefix = "source"
prefix = "source/"
target_manager.import_tools(source_manager, prefix)
# Verify tools were imported with prefixes
@ -65,7 +65,7 @@ def test_tool_duplicate_behavior():
) # Pre-create with the prefixed name
# Import tools from source to target
target_manager.import_tools(source_manager, "source")
target_manager.import_tools(source_manager, "source/")
# The original tool in the target manager is replaced by the imported one
assert target_manager._tools["source/common_tool"].fn.__name__ == source_fn.__name__
@ -89,8 +89,8 @@ def test_import_tools_with_multiple_prefixes():
# Create target manager and import from both sources
main_manager = ToolManager()
main_manager.import_tools(weather_manager, "weather")
main_manager.import_tools(news_manager, "news")
main_manager.import_tools(weather_manager, "weather/")
main_manager.import_tools(news_manager, "news/")
# Verify tools were imported with correct prefixes
assert "weather/forecast" in main_manager._tools