Repository: jlowin/fastmcp (python)

Issue:
Support for Customizable Prefix Separator in mount() Method
It would be great to support customizable prefix formats to accommodate integrations that have specific naming.

Currently, when using mcp.mount("weather", weather_mcp), the mounted prefix is automatically joined using "/". However Anthropic Claude API require a specific format for tool names.

For example, Anthropic's Claude API returns the following error when keys contain "/":

```javascript
❌ Claude API error: Error code: 400 - {
  'type': 'error',
  'error': {
    'type': 'invalid_request_error',
    'message': "tools.0.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,64}$'"
  }
}

```

Probably:
`mcp.mount("weather", weather_mcp, separator="_")`


Pull request description:
Change default mounted tool separator from / to _
Closes #126 

A few people reported a bug that Claude does not support `/` in tool names, which was part of the default prefix for mounted sub-MCP's. 

This PR updates the default separator to `_` and exposes configuration for customizing this, as requested in #126. 


Patch:
diff --git a/examples/mount_example.py b/examples/mount_example.py
index 73661f0..1d24af3 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/cli/cli.py b/src/fastmcp/cli/cli.py
index 09c2bb3..22ee73c 100644
--- a/src/fastmcp/cli/cli.py
+++ b/src/fastmcp/cli/cli.py
@@ -65,7 +65,7 @@ def _build_uv_command(
     """Build the uv run command that runs a MCP server through mcp run."""
     cmd = ["uv"]
 
-    cmd.extend(["run", "--with", "mcp"])
+    cmd.extend(["run", "--with", "fastmcp"])
 
     if with_editable:
         cmd.extend(["--with-editable", str(with_editable)])
@@ -76,7 +76,7 @@ def _build_uv_command(
                 cmd.extend(["--with", pkg])
 
     # Add mcp run command
-    cmd.extend(["mcp", "run", file_spec])
+    cmd.extend(["fastmcp", "run", file_spec])
     return cmd
 
 
diff --git a/src/fastmcp/prompts/prompt_manager.py b/src/fastmcp/prompts/prompt_manager.py
index c1653a5..6e1bb5a 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 afb26ae..55a2f96 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 4a372b4..3c30145 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 00279ed..9bfb95a 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}"')


Test patch:
diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py
index c502301..74a94f3 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