Auto generate names

This commit is contained in:
William Easton 2025-08-24 10:45:42 -05:00
commit f786bc465a
No known key found for this signature in database
6 changed files with 75 additions and 124 deletions

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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:

View file

@ -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}<br/>({node_type})"]'
elif node_type == "Client":
shape = f'{current_id}({{"{name}<br/>({node_type})"}})'
elif node_type == "Transport":
shape = f'{current_id}[["{name}<br/>({node_type})"]'
else: # Server
shape = f'{current_id}["{name}<br/>({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

View file

@ -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