|
5 | 5 | import inspect |
6 | 6 | import json |
7 | 7 | import re |
| 8 | +import secrets |
8 | 9 | import warnings |
9 | 10 | from collections.abc import AsyncIterator, Awaitable, Callable |
10 | 11 | from contextlib import ( |
@@ -197,8 +198,11 @@ def __init__( |
197 | 198 | lifespan = default_lifespan |
198 | 199 | else: |
199 | 200 | 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)}" |
200 | 204 | self._mcp_server = LowLevelServer[LifespanResultT]( |
201 | | - name=name or "FastMCP", |
| 205 | + name=name, |
202 | 206 | version=version, |
203 | 207 | instructions=instructions, |
204 | 208 | lifespan=_lifespan_wrapper(self, lifespan), |
@@ -519,7 +523,7 @@ def _get_additional_http_routes(self) -> list[BaseRoute]: |
519 | 523 | return routes |
520 | 524 |
|
521 | 525 | 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") |
523 | 527 |
|
524 | 528 | async with fastmcp.server.context.Context(fastmcp=self): |
525 | 529 | tools = await self._list_tools() |
@@ -563,7 +567,7 @@ async def _handler( |
563 | 567 | return await self._apply_middleware(mw_context, _handler) |
564 | 568 |
|
565 | 569 | 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") |
567 | 571 |
|
568 | 572 | async with fastmcp.server.context.Context(fastmcp=self): |
569 | 573 | resources = await self._list_resources() |
@@ -608,7 +612,7 @@ async def _handler( |
608 | 612 | return await self._apply_middleware(mw_context, _handler) |
609 | 613 |
|
610 | 614 | 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") |
612 | 616 |
|
613 | 617 | async with fastmcp.server.context.Context(fastmcp=self): |
614 | 618 | templates = await self._list_resource_templates() |
@@ -653,7 +657,7 @@ async def _handler( |
653 | 657 | return await self._apply_middleware(mw_context, _handler) |
654 | 658 |
|
655 | 659 | 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") |
657 | 661 |
|
658 | 662 | async with fastmcp.server.context.Context(fastmcp=self): |
659 | 663 | prompts = await self._list_prompts() |
@@ -712,7 +716,9 @@ async def _mcp_call_tool( |
712 | 716 | Returns: |
713 | 717 | List of MCP Content objects containing the tool results |
714 | 718 | """ |
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 | + ) |
716 | 722 |
|
717 | 723 | async with fastmcp.server.context.Context(fastmcp=self): |
718 | 724 | try: |
@@ -754,7 +760,7 @@ async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceConten |
754 | 760 |
|
755 | 761 | Delegates to _read_resource, which should be overridden by FastMCP subclasses. |
756 | 762 | """ |
757 | | - logger.debug("Handler called: read_resource %s", uri) |
| 763 | + logger.debug(f"[{self.name}] Handler called: read_resource %s", uri) |
758 | 764 |
|
759 | 765 | async with fastmcp.server.context.Context(fastmcp=self): |
760 | 766 | try: |
@@ -809,7 +815,9 @@ async def _mcp_get_prompt( |
809 | 815 |
|
810 | 816 | Delegates to _get_prompt, which should be overridden by FastMCP subclasses. |
811 | 817 | """ |
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 | + ) |
813 | 821 |
|
814 | 822 | async with fastmcp.server.context.Context(fastmcp=self): |
815 | 823 | try: |
@@ -1966,9 +1974,11 @@ async def import_server( |
1966 | 1974 | self._prompt_manager.add_prompt(prompt) |
1967 | 1975 |
|
1968 | 1976 | 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 | + ) |
1970 | 1980 | else: |
1971 | | - logger.debug(f"Imported server {server.name}") |
| 1981 | + logger.debug(f"[{self.name}] Imported server {server.name}") |
1972 | 1982 |
|
1973 | 1983 | @classmethod |
1974 | 1984 | def from_openapi( |
@@ -2195,6 +2205,119 @@ def _should_enable_component( |
2195 | 2205 |
|
2196 | 2206 | return True |
2197 | 2207 |
|
| 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 | + |
2198 | 2321 |
|
2199 | 2322 | @dataclass |
2200 | 2323 | class MountedServer: |
|
0 commit comments