mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 13:04:18 +02:00
Auto generate names
This commit is contained in:
parent
9822024424
commit
f786bc465a
6 changed files with 75 additions and 124 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue