Skip to content

Commit 9822024

Browse files
feat: improve debugging for nested servers with server/client names and hierarchy visualization
- Add random ID generation for unnamed FastMCP servers (FastMCP-xxxxxx format) - Add name field to Client class with random ID support - Update all server/client logging to include server/client names in brackets - Add generate_hierarchy_diagram() method to visualize server/proxy/client relationships - Handle proxy classes automatically through inheritance - Update tests to accommodate new random naming behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: William Easton <[email protected]>
1 parent 007af5d commit 9822024

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

src/fastmcp/client/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import copy
55
import datetime
6+
import secrets
67
from contextlib import AsyncExitStack, asynccontextmanager
78
from dataclasses import dataclass, field
89
from pathlib import Path
@@ -212,6 +213,7 @@ def __init__(
212213
| dict[str, Any]
213214
| str
214215
),
216+
name: str | None = None,
215217
roots: RootsList | RootsHandler | None = None,
216218
sampling_handler: ClientSamplingHandler | None = None,
217219
elicitation_handler: ElicitationHandler | None = None,
@@ -223,6 +225,11 @@ def __init__(
223225
client_info: mcp.types.Implementation | None = None,
224226
auth: httpx.Auth | Literal["oauth"] | str | None = None,
225227
) -> None:
228+
# Generate random ID if no name provided
229+
if name is None:
230+
name = f"FastMCP-Client-{secrets.token_hex(4)}"
231+
self.name = name
232+
226233
self.transport = cast(ClientTransportT, infer_transport(transport))
227234
if auth is not None:
228235
self.transport._set_auth(auth)
@@ -889,7 +896,7 @@ async def call_tool(
889896
else:
890897
data = result.structuredContent
891898
except Exception as e:
892-
logger.error(f"Error parsing structured content: {e}")
899+
logger.error(f"[{self.name}] Error parsing structured content: {e}")
893900

894901
return CallToolResult(
895902
content=result.content,

src/fastmcp/server/server.py

Lines changed: 133 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import inspect
66
import json
77
import re
8+
import secrets
89
import warnings
910
from collections.abc import AsyncIterator, Awaitable, Callable
1011
from contextlib import (
@@ -197,8 +198,11 @@ def __init__(
197198
lifespan = default_lifespan
198199
else:
199200
self._has_lifespan = True
201+
# Generate random ID if no name provided
202+
if name is None:
203+
name = f"FastMCP-{secrets.token_hex(4)}"
200204
self._mcp_server = LowLevelServer[LifespanResultT](
201-
name=name or "FastMCP",
205+
name=name,
202206
version=version,
203207
instructions=instructions,
204208
lifespan=_lifespan_wrapper(self, lifespan),
@@ -519,7 +523,7 @@ def _get_additional_http_routes(self) -> list[BaseRoute]:
519523
return routes
520524

521525
async def _mcp_list_tools(self) -> list[MCPTool]:
522-
logger.debug("Handler called: list_tools")
526+
logger.debug(f"[{self.name}] Handler called: list_tools")
523527

524528
async with fastmcp.server.context.Context(fastmcp=self):
525529
tools = await self._list_tools()
@@ -563,7 +567,7 @@ async def _handler(
563567
return await self._apply_middleware(mw_context, _handler)
564568

565569
async def _mcp_list_resources(self) -> list[MCPResource]:
566-
logger.debug("Handler called: list_resources")
570+
logger.debug(f"[{self.name}] Handler called: list_resources")
567571

568572
async with fastmcp.server.context.Context(fastmcp=self):
569573
resources = await self._list_resources()
@@ -608,7 +612,7 @@ async def _handler(
608612
return await self._apply_middleware(mw_context, _handler)
609613

610614
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
611-
logger.debug("Handler called: list_resource_templates")
615+
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
612616

613617
async with fastmcp.server.context.Context(fastmcp=self):
614618
templates = await self._list_resource_templates()
@@ -653,7 +657,7 @@ async def _handler(
653657
return await self._apply_middleware(mw_context, _handler)
654658

655659
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
656-
logger.debug("Handler called: list_prompts")
660+
logger.debug(f"[{self.name}] Handler called: list_prompts")
657661

658662
async with fastmcp.server.context.Context(fastmcp=self):
659663
prompts = await self._list_prompts()
@@ -712,7 +716,9 @@ async def _mcp_call_tool(
712716
Returns:
713717
List of MCP Content objects containing the tool results
714718
"""
715-
logger.debug("Handler called: call_tool %s with %s", key, arguments)
719+
logger.debug(
720+
f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
721+
)
716722

717723
async with fastmcp.server.context.Context(fastmcp=self):
718724
try:
@@ -754,7 +760,7 @@ async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceConten
754760
755761
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
756762
"""
757-
logger.debug("Handler called: read_resource %s", uri)
763+
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
758764

759765
async with fastmcp.server.context.Context(fastmcp=self):
760766
try:
@@ -809,7 +815,9 @@ async def _mcp_get_prompt(
809815
810816
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
811817
"""
812-
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
818+
logger.debug(
819+
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
820+
)
813821

814822
async with fastmcp.server.context.Context(fastmcp=self):
815823
try:
@@ -1966,9 +1974,11 @@ async def import_server(
19661974
self._prompt_manager.add_prompt(prompt)
19671975

19681976
if prefix:
1969-
logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
1977+
logger.debug(
1978+
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
1979+
)
19701980
else:
1971-
logger.debug(f"Imported server {server.name}")
1981+
logger.debug(f"[{self.name}] Imported server {server.name}")
19721982

19731983
@classmethod
19741984
def from_openapi(
@@ -2195,6 +2205,119 @@ def _should_enable_component(
21952205

21962206
return True
21972207

2208+
def generate_hierarchy_diagram(self, format: Literal["mermaid"] = "mermaid") -> str:
2209+
"""Generate a diagram showing the hierarchy of servers, mounted servers, proxies, clients and transports.
2210+
2211+
Args:
2212+
format: Output format, currently only "mermaid" is supported
2213+
2214+
Returns:
2215+
A string containing the diagram in the requested format
2216+
2217+
Example:
2218+
```python
2219+
server = FastMCP("MyServer")
2220+
print(server.generate_hierarchy_diagram())
2221+
```
2222+
"""
2223+
if format != "mermaid":
2224+
raise ValueError("Only 'mermaid' format is currently supported")
2225+
2226+
def get_server_type(server: FastMCP[Any]) -> str:
2227+
"""Determine the type of server for display"""
2228+
from fastmcp.server.proxy import FastMCPProxy
2229+
2230+
if isinstance(server, FastMCPProxy):
2231+
return "Proxy"
2232+
return "Server"
2233+
2234+
lines = ["graph TD"]
2235+
node_id = 0
2236+
2237+
def add_node(name: str, node_type: str = "Server") -> str:
2238+
"""Add a node and return its ID"""
2239+
nonlocal node_id
2240+
current_id = f"N{node_id}"
2241+
node_id += 1
2242+
2243+
# Choose appropriate mermaid shape based on type
2244+
if node_type == "Proxy":
2245+
shape = f'{current_id}[["{name}<br/>({node_type})"]'
2246+
elif node_type == "Client":
2247+
shape = f'{current_id}({{"{name}<br/>({node_type})"}})'
2248+
elif node_type == "Transport":
2249+
shape = f'{current_id}[["{name}<br/>({node_type})"]'
2250+
else: # Server
2251+
shape = f'{current_id}["{name}<br/>({node_type})"]'
2252+
2253+
lines.append(f" {shape}")
2254+
return current_id
2255+
2256+
def add_connection(from_id: str, to_id: str, label: str = "") -> None:
2257+
"""Add a connection between nodes"""
2258+
if label:
2259+
lines.append(f" {from_id} -->|{label}| {to_id}")
2260+
else:
2261+
lines.append(f" {from_id} --> {to_id}")
2262+
2263+
# Add the main server
2264+
main_server_id = add_node(self.name, get_server_type(self))
2265+
2266+
# Add mounted servers recursively
2267+
def process_server(server: FastMCP[Any], parent_id: str) -> None:
2268+
for mounted in server._mounted_servers:
2269+
server_type = get_server_type(mounted.server)
2270+
mounted_id = add_node(mounted.server.name, server_type)
2271+
2272+
# Add connection with prefix label if it exists
2273+
prefix_label = (
2274+
f"prefix: {mounted.prefix}" if mounted.prefix else "no prefix"
2275+
)
2276+
add_connection(parent_id, mounted_id, prefix_label)
2277+
2278+
# Recursively process this mounted server's mounts
2279+
process_server(mounted.server, mounted_id)
2280+
2281+
# If this is a proxy, try to show its client info
2282+
from fastmcp.server.proxy import FastMCPProxy
2283+
2284+
if isinstance(mounted.server, FastMCPProxy):
2285+
try:
2286+
# Add a representation of the proxy's client factory
2287+
client_id = add_node("Client Factory", "Client")
2288+
add_connection(mounted_id, client_id, "uses")
2289+
except Exception:
2290+
# In case of any issues accessing proxy internals, skip
2291+
pass
2292+
2293+
# Process all mounted servers
2294+
process_server(self, main_server_id)
2295+
2296+
# If this is a proxy server, show its client connection
2297+
from fastmcp.server.proxy import FastMCPProxy
2298+
2299+
if isinstance(self, FastMCPProxy):
2300+
try:
2301+
client_id = add_node("Client Factory", "Client")
2302+
add_connection(main_server_id, client_id, "proxies to")
2303+
except Exception:
2304+
# In case of any issues, skip
2305+
pass
2306+
2307+
# Add styling
2308+
lines.extend(
2309+
[
2310+
"",
2311+
" %% Styling",
2312+
" classDef serverClass fill:#e1f5fe,stroke:#01579b,stroke-width:2px",
2313+
" classDef proxyClass fill:#fff3e0,stroke:#e65100,stroke-width:2px",
2314+
" classDef clientClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px",
2315+
" classDef transportClass fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px",
2316+
]
2317+
)
2318+
2319+
return "\n".join(lines)
2320+
21982321

21992322
@dataclass
22002323
class MountedServer:

tests/server/proxy/test_proxy_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def test_create_proxy(fastmcp_server):
8787

8888
assert isinstance(server, FastMCPProxy)
8989
assert isinstance(server, FastMCP)
90-
assert server.name == "FastMCP"
90+
assert server.name.startswith("FastMCP-")
9191

9292

9393
async def test_as_proxy_with_server(fastmcp_server):

tests/server/test_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
class TestCreateServer:
3030
async def test_create_server(self):
3131
mcp = FastMCP(instructions="Server instructions")
32-
assert mcp.name == "FastMCP"
32+
assert mcp.name.startswith("FastMCP-")
3333
assert mcp.instructions == "Server instructions"
3434

3535
async def test_non_ascii_description(self):

0 commit comments

Comments
 (0)