diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index 5e7c7cfa8..444782c6b 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -225,10 +225,7 @@ class Client(Generic[ClientTransportT]):
client_info: mcp.types.Implementation | None = None,
auth: httpx.Auth | Literal["oauth"] | str | None = None,
) -> None:
- # Generate random ID if no name provided
- if name is None:
- name = f"FastMCP-Client-{secrets.token_hex(4)}"
- self.name = name
+ self.name = name or self.generate_name()
self.transport = cast(ClientTransportT, infer_transport(transport))
if auth is not None:
@@ -346,6 +343,8 @@ class Client(Generic[ClientTransportT]):
# Reset session state to fresh state
new_client._session_state = ClientSessionState()
+ new_client.name += f"-{secrets.token_hex(2)}"
+
return new_client
@asynccontextmanager
@@ -545,6 +544,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called list_resources")
+
result = await self.session.list_resources()
return result
@@ -572,6 +573,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called list_resource_templates")
+
result = await self.session.list_resource_templates()
return result
@@ -604,6 +607,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called read_resource: {uri}")
+
if isinstance(uri, str):
uri = AnyUrl(uri) # Ensure AnyUrl
result = await self.session.read_resource(uri)
@@ -658,6 +663,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called list_prompts")
+
result = await self.session.list_prompts()
return result
@@ -690,6 +697,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called get_prompt: {name}")
+
# Serialize arguments for MCP protocol - convert non-string values to JSON
serialized_arguments: dict[str, str] | None = None
if arguments:
@@ -747,6 +756,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called complete: {ref}")
+
result = await self.session.complete(ref=ref, argument=argument)
return result
@@ -782,6 +793,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called list_tools")
+
result = await self.session.list_tools()
return result
@@ -824,6 +837,7 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
+ logger.debug(f"[{self.name}] called call_tool: {name}")
if isinstance(timeout, int | float):
timeout = datetime.timedelta(seconds=float(timeout))
@@ -905,6 +919,14 @@ class Client(Generic[ClientTransportT]):
is_error=result.isError,
)
+ @classmethod
+ def generate_name(cls, name: str | None = None) -> str:
+ class_name = cls.__name__
+ if name is None:
+ return f"{class_name}-{secrets.token_hex(2)}"
+ else:
+ return f"{class_name}-{name}-{secrets.token_hex(2)}"
+
@dataclass
class CallToolResult:
diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py
index f01932bd6..71aa8c855 100644
--- a/src/fastmcp/client/transports.py
+++ b/src/fastmcp/client/transports.py
@@ -3,6 +3,7 @@ import asyncio
import contextlib
import datetime
import os
+import secrets
import shutil
import sys
import warnings
@@ -902,7 +903,8 @@ class MCPConfigTransport(ClientTransport):
# otherwise create a composite client
else:
- self._composite_server = FastMCP[Any]()
+ name = FastMCP.generate_name("MCPRouter")
+ self._composite_server = FastMCP[Any](name=name)
for name, server, transport in mcp_config_to_servers_and_transports(
self.config
diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py
index a2ca5ee66..fde181dc7 100644
--- a/src/fastmcp/mcp_config.py
+++ b/src/fastmcp/mcp_config.py
@@ -91,14 +91,20 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
def _to_server_and_underlying_transport(
self,
+ server_name: str | None = None,
+ client_name: str | None = None,
) -> tuple[FastMCP[Any], ClientTransport]:
"""Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport."""
from fastmcp import FastMCP
+ from fastmcp.client import Client
- transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
+ transport: ClientTransport = self.to_transport()
+
+ client: Client[ClientTransport] = Client(transport=transport, name=client_name)
wrapped_mcp_server = FastMCP.as_proxy(
- transport,
+ name=server_name,
+ backend=client,
tool_transformations=self.tools,
include_tags=self.include_tags,
exclude_tags=self.exclude_tags,
diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py
index 2f52a627b..4fcce6b69 100644
--- a/src/fastmcp/server/proxy.py
+++ b/src/fastmcp/server/proxy.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import inspect
+import secrets
import warnings
from collections.abc import Awaitable, Callable
from pathlib import Path
@@ -482,6 +483,9 @@ class FastMCPProxy(FastMCP):
super().__init__(**kwargs)
+ if "name" not in kwargs:
+ kwargs["name"] = self.generate_name()
+
# Handle client and client_factory parameters
if client is not None and client_factory is not None:
raise ValueError("Cannot specify both 'client' and 'client_factory'")
@@ -546,6 +550,9 @@ class ProxyClient(Client[ClientTransportT]):
| str,
**kwargs,
):
+
+ if "name" not in kwargs:
+ kwargs["name"] = self.generate_name()
if "roots" not in kwargs:
kwargs["roots"] = default_proxy_roots_handler
if "sampling_handler" not in kwargs:
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 28b0b3836..bc7788288 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -199,10 +199,8 @@ class FastMCP(Generic[LifespanResultT]):
else:
self._has_lifespan = True
# Generate random ID if no name provided
- if name is None:
- name = f"FastMCP-{secrets.token_hex(4)}"
self._mcp_server = LowLevelServer[LifespanResultT](
- name=name,
+ name=name or self.generate_name(),
version=version,
instructions=instructions,
lifespan=_lifespan_wrapper(self, lifespan),
@@ -2205,118 +2203,14 @@ class FastMCP(Generic[LifespanResultT]):
return True
- def generate_hierarchy_diagram(self, format: Literal["mermaid"] = "mermaid") -> str:
- """Generate a diagram showing the hierarchy of servers, mounted servers, proxies, clients and transports.
+ @classmethod
+ def generate_name(cls, name: str | None = None) -> str:
+ class_name = cls.__name__
- Args:
- format: Output format, currently only "mermaid" is supported
-
- Returns:
- A string containing the diagram in the requested format
-
- Example:
- ```python
- server = FastMCP("MyServer")
- print(server.generate_hierarchy_diagram())
- ```
- """
- if format != "mermaid":
- raise ValueError("Only 'mermaid' format is currently supported")
-
- def get_server_type(server: FastMCP[Any]) -> str:
- """Determine the type of server for display"""
- from fastmcp.server.proxy import FastMCPProxy
-
- if isinstance(server, FastMCPProxy):
- return "Proxy"
- return "Server"
-
- lines = ["graph TD"]
- node_id = 0
-
- def add_node(name: str, node_type: str = "Server") -> str:
- """Add a node and return its ID"""
- nonlocal node_id
- current_id = f"N{node_id}"
- node_id += 1
-
- # Choose appropriate mermaid shape based on type
- if node_type == "Proxy":
- shape = f'{current_id}[["{name}
({node_type})"]'
- elif node_type == "Client":
- shape = f'{current_id}({{"{name}
({node_type})"}})'
- elif node_type == "Transport":
- shape = f'{current_id}[["{name}
({node_type})"]'
- else: # Server
- shape = f'{current_id}["{name}
({node_type})"]'
-
- lines.append(f" {shape}")
- return current_id
-
- def add_connection(from_id: str, to_id: str, label: str = "") -> None:
- """Add a connection between nodes"""
- if label:
- lines.append(f" {from_id} -->|{label}| {to_id}")
- else:
- lines.append(f" {from_id} --> {to_id}")
-
- # Add the main server
- main_server_id = add_node(self.name, get_server_type(self))
-
- # Add mounted servers recursively
- def process_server(server: FastMCP[Any], parent_id: str) -> None:
- for mounted in server._mounted_servers:
- server_type = get_server_type(mounted.server)
- mounted_id = add_node(mounted.server.name, server_type)
-
- # Add connection with prefix label if it exists
- prefix_label = (
- f"prefix: {mounted.prefix}" if mounted.prefix else "no prefix"
- )
- add_connection(parent_id, mounted_id, prefix_label)
-
- # Recursively process this mounted server's mounts
- process_server(mounted.server, mounted_id)
-
- # If this is a proxy, try to show its client info
- from fastmcp.server.proxy import FastMCPProxy
-
- if isinstance(mounted.server, FastMCPProxy):
- try:
- # Add a representation of the proxy's client factory
- client_id = add_node("Client Factory", "Client")
- add_connection(mounted_id, client_id, "uses")
- except Exception:
- # In case of any issues accessing proxy internals, skip
- pass
-
- # Process all mounted servers
- process_server(self, main_server_id)
-
- # If this is a proxy server, show its client connection
- from fastmcp.server.proxy import FastMCPProxy
-
- if isinstance(self, FastMCPProxy):
- try:
- client_id = add_node("Client Factory", "Client")
- add_connection(main_server_id, client_id, "proxies to")
- except Exception:
- # In case of any issues, skip
- pass
-
- # Add styling
- lines.extend(
- [
- "",
- " %% Styling",
- " classDef serverClass fill:#e1f5fe,stroke:#01579b,stroke-width:2px",
- " classDef proxyClass fill:#fff3e0,stroke:#e65100,stroke-width:2px",
- " classDef clientClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px",
- " classDef transportClass fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px",
- ]
- )
-
- return "\n".join(lines)
+ if name is None:
+ return f"{class_name}-{secrets.token_hex(2)}"
+ else:
+ return f"{class_name}-{name}-{secrets.token_hex(2)}"
@dataclass
diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py
index 48cb3fb9a..a1f3888f3 100644
--- a/src/fastmcp/utilities/mcp_config.py
+++ b/src/fastmcp/utilities/mcp_config.py
@@ -1,11 +1,18 @@
from typing import Any
-from fastmcp.client.transports import ClientTransport
+from fastmcp.client import Client
+from fastmcp.client.transports import (
+ ClientTransport,
+ SSETransport,
+ StdioTransport,
+ StreamableHttpTransport,
+)
from fastmcp.mcp_config import (
MCPConfig,
MCPServerTypes,
)
from fastmcp.server.server import FastMCP
+from fastmcp.server.proxy import ProxyClient
def mcp_config_to_servers_and_transports(
@@ -23,6 +30,8 @@ def mcp_server_type_to_servers_and_transports(
mcp_server: MCPServerTypes,
) -> tuple[str, FastMCP[Any], ClientTransport]:
"""A utility function to convert each entry of an MCP Config into a transport and server."""
+ import secrets
+
from fastmcp.mcp_config import (
TransformingRemoteMCPServer,
TransformingStdioMCPServer,
@@ -31,10 +40,21 @@ def mcp_server_type_to_servers_and_transports(
server: FastMCP[Any]
transport: ClientTransport
+ token = secrets.token_hex(2)
+
+ client_name = ProxyClient.generate_name(f"MCP_{name}_{token}")
+ server_name = FastMCP.generate_name(f"MCP_{name}_{token}")
+
if isinstance(mcp_server, TransformingRemoteMCPServer | TransformingStdioMCPServer):
- server, transport = mcp_server._to_server_and_underlying_transport()
+ server, transport = mcp_server._to_server_and_underlying_transport(
+ server_name=server_name,
+ client_name=client_name,
+ )
else:
transport = mcp_server.to_transport()
- server = FastMCP.as_proxy(backend=transport)
+ client: ProxyClient[StreamableHttpTransport | SSETransport | StdioTransport] = (
+ ProxyClient(transport=transport, name=client_name)
+ )
+ server = FastMCP.as_proxy(name=server_name, backend=client)
return name, server, transport