diff --git a/examples/mount_example.py b/examples/mount_example.py index 73661f0d2..1d24af3fd 100644 --- a/examples/mount_example.py +++ b/examples/mount_example.py @@ -64,10 +64,11 @@ def check_app_status() -> dict[str, str]: # Mount sub-applications app.mount("weather", weather_app) + app.mount("news", news_app) -async def start_server(): +async def get_server_details(): """Print information about mounted resources.""" # Print available tools tools = app._tool_manager.list_tools() @@ -105,7 +106,7 @@ async def start_server(): if __name__ == "__main__": # First run our async function to display info - asyncio.run(start_server()) + asyncio.run(get_server_details()) # Then start the server (uncomment to run the server) # app.run() diff --git a/src/fastmcp/prompts/prompt_manager.py b/src/fastmcp/prompts/prompt_manager.py index c1653a50d..6e1bb5ab5 100644 --- a/src/fastmcp/prompts/prompt_manager.py +++ b/src/fastmcp/prompts/prompt_manager.py @@ -85,8 +85,6 @@ class PromptManager: new_prompt = prompt.copy(updates=dict(name=prefixed_name)) - # Log the import - logger.debug(f"Importing prompt with name {name} as {prefixed_name}") - # Store the prompt with the prefixed name self.add_prompt(new_prompt) + logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"') diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py index afb26aecf..55a2f9636 100644 --- a/src/fastmcp/resources/resource_manager.py +++ b/src/fastmcp/resources/resource_manager.py @@ -156,11 +156,9 @@ class ResourceManager: new_resource = resource.copy(updates=dict(uri=prefixed_uri)) - # Log the import - logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}") - # Store directly in resources dictionary self.add_resource(new_resource) + logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"') def import_templates( self, manager: "ResourceManager", prefix: str | None = None @@ -188,10 +186,8 @@ class ResourceManager: updates=dict(uri_template=prefixed_uri_template) ) - # Log the import - logger.debug( - f"Importing resource template with URI {uri_template} as {prefixed_uri_template}" - ) - # Store directly in templates dictionary self.add_template(new_template) + logger.debug( + f'Imported template "{uri_template}" as "{prefixed_uri_template}"' + ) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 4a372b468..3c3014518 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -153,6 +153,7 @@ class FastMCP(Generic[LifespanResultT]): async def list_tools(self) -> list[MCPTool]: """List all available tools.""" + tools = self._tool_manager.list_tools() return [ MCPTool( @@ -534,37 +535,51 @@ class FastMCP(Generic[LifespanResultT]): logger.error(f"Error getting prompt {name}: {e}") raise ValueError(str(e)) - def mount(self, prefix: str, app: "FastMCP") -> None: + def mount( + self, + prefix: str, + app: "FastMCP", + tool_separator: str | None = None, + resource_separator: str | None = None, + prompt_separator: str | None = None, + ) -> None: """Mount another FastMCP application with a given prefix. When an application is mounted: - - The tools are imported with prefixed names - Example: If app has a tool named "get_weather", it will be available as "weather/get_weather" - - The resources are imported with prefixed URIs + - The tools are imported with prefixed names using the tool_separator + Example: If app has a tool named "get_weather", it will be available as "weatherget_weather" + - The resources are imported with prefixed URIs using the resource_separator Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast" - - The templates are imported with prefixed URI templates + - The templates are imported with prefixed URI templates using the resource_separator Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}" - - The prompts are imported with prefixed names - Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt" + - The prompts are imported with prefixed names using the prompt_separator + Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt" Args: prefix: The prefix to use for the mounted application app: The FastMCP application to mount """ + if tool_separator is None: + tool_separator = "_" + if resource_separator is None: + resource_separator = "+" + if prompt_separator is None: + prompt_separator = "_" + # Mount the app in the list of mounted apps self._mounted_apps[prefix] = app - # Import tools from the mounted app with / delimiter - tool_prefix = f"{prefix}/" + # Import tools from the mounted app + tool_prefix = f"{prefix}{tool_separator}" self._tool_manager.import_tools(app._tool_manager, tool_prefix) - # Import resources and templates from the mounted app with + delimiter - resource_prefix = f"{prefix}+" + # Import resources and templates from the mounted app + resource_prefix = f"{prefix}{resource_separator}" self._resource_manager.import_resources(app._resource_manager, resource_prefix) self._resource_manager.import_templates(app._resource_manager, resource_prefix) - # Import prompts with / delimiter - prompt_prefix = f"{prefix}/" + # Import prompts from the mounted app + prompt_prefix = f"{prefix}{prompt_separator}" self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix) logger.info(f"Mounted app with prefix '{prefix}'") diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 00279ed89..9bfb95ad0 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -93,4 +93,4 @@ class ToolManager: new_tool = tool.copy(updates=dict(name=prefixed_name)) # Store the copied tool self.add_tool(new_tool) - logger.debug(f"Imported tool: {name} as {prefixed_name}") + logger.debug(f'Imported tool "{name}" as "{prefixed_name}"') diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index c5023015a..74a94f3dd 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -16,12 +16,12 @@ async def test_mount_basic_functionality(): main_app.mount("sub", sub_app) # Verify the tool was imported with the prefix - assert "sub/sub_tool" in main_app._tool_manager._tools + assert "sub_sub_tool" in main_app._tool_manager._tools assert "sub_tool" in sub_app._tool_manager._tools # Verify the original tool still exists in the sub-app - tool = main_app._tool_manager._tools["sub/sub_tool"] - assert tool.name == "sub/sub_tool" + tool = main_app._tool_manager._tools["sub_sub_tool"] + assert tool.name == "sub_sub_tool" assert callable(tool.fn) @@ -46,8 +46,8 @@ async def test_mount_multiple_apps(): main_app.mount("news", news_app) # Verify tools were imported with the correct prefixes - assert "weather/get_forecast" in main_app._tool_manager._tools - assert "news/get_headlines" in main_app._tool_manager._tools + assert "weather_get_forecast" in main_app._tool_manager._tools + assert "news_get_headlines" in main_app._tool_manager._tools async def test_mount_combines_tools(): @@ -68,16 +68,16 @@ async def test_mount_combines_tools(): # Mount first app main_app.mount("api", first_app) - assert "api/first_tool" in main_app._tool_manager._tools + assert "api_first_tool" in main_app._tool_manager._tools # Mount second app to same prefix main_app.mount("api", second_app) # Verify second tool is there - assert "api/second_tool" in main_app._tool_manager._tools + assert "api_second_tool" in main_app._tool_manager._tools # Tools from both mounts are combined - assert "api/first_tool" in main_app._tool_manager._tools + assert "api_first_tool" in main_app._tool_manager._tools async def test_mount_with_resources(): @@ -131,7 +131,7 @@ async def test_mount_with_prompts(): main_app.mount("assistant", assistant_app) # Verify the prompt was imported with the prefix - assert "assistant/greeting" in main_app._prompt_manager._prompts + assert "assistant_greeting" in main_app._prompt_manager._prompts async def test_mount_multiple_resource_templates(): @@ -180,5 +180,5 @@ async def test_mount_multiple_prompts(): main_app.mount("sql", sql_app) # Verify prompts were imported with correct prefixes - assert "python/review_python" in main_app._prompt_manager._prompts - assert "sql/explain_sql" in main_app._prompt_manager._prompts + assert "python_review_python" in main_app._prompt_manager._prompts + assert "sql_explain_sql" in main_app._prompt_manager._prompts