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