Skip to content

Commit b39b410

Browse files
authored
Improve debug logging for nested Servers / Clients
Improve debug logging for nested Servers / Clients
2 parents 1f3f82d + a6c5ff5 commit b39b410

File tree

9 files changed

+103
-25
lines changed

9 files changed

+103
-25
lines changed

src/fastmcp/client/client.py

Lines changed: 30 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,8 @@ def __init__(
223225
client_info: mcp.types.Implementation | None = None,
224226
auth: httpx.Auth | Literal["oauth"] | str | None = None,
225227
) -> None:
228+
self.name = name or self.generate_name()
229+
226230
self.transport = cast(ClientTransportT, infer_transport(transport))
227231
if auth is not None:
228232
self.transport._set_auth(auth)
@@ -339,6 +343,8 @@ def new(self) -> Client[ClientTransportT]:
339343
# Reset session state to fresh state
340344
new_client._session_state = ClientSessionState()
341345

346+
new_client.name += f":{secrets.token_hex(2)}"
347+
342348
return new_client
343349

344350
@asynccontextmanager
@@ -538,6 +544,8 @@ async def list_resources_mcp(self) -> mcp.types.ListResourcesResult:
538544
Raises:
539545
RuntimeError: If called while the client is not connected.
540546
"""
547+
logger.debug(f"[{self.name}] called list_resources")
548+
541549
result = await self.session.list_resources()
542550
return result
543551

@@ -565,6 +573,8 @@ async def list_resource_templates_mcp(
565573
Raises:
566574
RuntimeError: If called while the client is not connected.
567575
"""
576+
logger.debug(f"[{self.name}] called list_resource_templates")
577+
568578
result = await self.session.list_resource_templates()
569579
return result
570580

@@ -597,6 +607,8 @@ async def read_resource_mcp(
597607
Raises:
598608
RuntimeError: If called while the client is not connected.
599609
"""
610+
logger.debug(f"[{self.name}] called read_resource: {uri}")
611+
600612
if isinstance(uri, str):
601613
uri = AnyUrl(uri) # Ensure AnyUrl
602614
result = await self.session.read_resource(uri)
@@ -651,6 +663,8 @@ async def list_prompts_mcp(self) -> mcp.types.ListPromptsResult:
651663
Raises:
652664
RuntimeError: If called while the client is not connected.
653665
"""
666+
logger.debug(f"[{self.name}] called list_prompts")
667+
654668
result = await self.session.list_prompts()
655669
return result
656670

@@ -683,6 +697,8 @@ async def get_prompt_mcp(
683697
Raises:
684698
RuntimeError: If called while the client is not connected.
685699
"""
700+
logger.debug(f"[{self.name}] called get_prompt: {name}")
701+
686702
# Serialize arguments for MCP protocol - convert non-string values to JSON
687703
serialized_arguments: dict[str, str] | None = None
688704
if arguments:
@@ -740,6 +756,8 @@ async def complete_mcp(
740756
Raises:
741757
RuntimeError: If called while the client is not connected.
742758
"""
759+
logger.debug(f"[{self.name}] called complete: {ref}")
760+
743761
result = await self.session.complete(ref=ref, argument=argument)
744762
return result
745763

@@ -775,6 +793,8 @@ async def list_tools_mcp(self) -> mcp.types.ListToolsResult:
775793
Raises:
776794
RuntimeError: If called while the client is not connected.
777795
"""
796+
logger.debug(f"[{self.name}] called list_tools")
797+
778798
result = await self.session.list_tools()
779799
return result
780800

@@ -817,6 +837,7 @@ async def call_tool_mcp(
817837
Raises:
818838
RuntimeError: If called while the client is not connected.
819839
"""
840+
logger.debug(f"[{self.name}] called call_tool: {name}")
820841

821842
if isinstance(timeout, int | float):
822843
timeout = datetime.timedelta(seconds=float(timeout))
@@ -889,7 +910,7 @@ async def call_tool(
889910
else:
890911
data = result.structuredContent
891912
except Exception as e:
892-
logger.error(f"Error parsing structured content: {e}")
913+
logger.error(f"[{self.name}] Error parsing structured content: {e}")
893914

894915
return CallToolResult(
895916
content=result.content,
@@ -898,6 +919,14 @@ async def call_tool(
898919
is_error=result.isError,
899920
)
900921

922+
@classmethod
923+
def generate_name(cls, name: str | None = None) -> str:
924+
class_name = cls.__name__
925+
if name is None:
926+
return f"{class_name}-{secrets.token_hex(2)}"
927+
else:
928+
return f"{class_name}-{name}-{secrets.token_hex(2)}"
929+
901930

902931
@dataclass
903932
class CallToolResult:

src/fastmcp/client/transports.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,8 @@ def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):
902902

903903
# otherwise create a composite client
904904
else:
905-
self._composite_server = FastMCP[Any]()
905+
name = FastMCP.generate_name("MCPRouter")
906+
self._composite_server = FastMCP[Any](name=name)
906907

907908
for name, server, transport in mcp_config_to_servers_and_transports(
908909
self.config

src/fastmcp/mcp_config.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import datetime
2828
import re
2929
from pathlib import Path
30-
from typing import TYPE_CHECKING, Annotated, Any, Literal
30+
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
3131
from urllib.parse import urlparse
3232

3333
import httpx
@@ -91,14 +91,24 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
9191

9292
def _to_server_and_underlying_transport(
9393
self,
94+
server_name: str | None = None,
95+
client_name: str | None = None,
9496
) -> tuple[FastMCP[Any], ClientTransport]:
9597
"""Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport."""
9698
from fastmcp import FastMCP
99+
from fastmcp.client import Client
100+
from fastmcp.client.transports import (
101+
ClientTransport, # pyright: ignore[reportUnusedImport]
102+
)
97103

98104
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
105+
transport = cast(ClientTransport, transport)
106+
107+
client: Client[ClientTransport] = Client(transport=transport, name=client_name)
99108

100109
wrapped_mcp_server = FastMCP.as_proxy(
101-
transport,
110+
name=server_name,
111+
backend=client,
102112
tool_transformations=self.tools,
103113
include_tags=self.include_tags,
104114
exclude_tags=self.exclude_tags,

src/fastmcp/server/proxy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ def __init__(
546546
| str,
547547
**kwargs,
548548
):
549+
if "name" not in kwargs:
550+
kwargs["name"] = self.generate_name()
549551
if "roots" not in kwargs:
550552
kwargs["roots"] = default_proxy_roots_handler
551553
if "sampling_handler" not in kwargs:

src/fastmcp/server/server.py

Lines changed: 27 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,9 @@ def __init__(
197198
lifespan = default_lifespan
198199
else:
199200
self._has_lifespan = True
201+
# Generate random ID if no name provided
200202
self._mcp_server = LowLevelServer[LifespanResultT](
201-
name=name or "FastMCP",
203+
name=name or self.generate_name(),
202204
version=version,
203205
instructions=instructions,
204206
lifespan=_lifespan_wrapper(self, lifespan),
@@ -519,7 +521,7 @@ def _get_additional_http_routes(self) -> list[BaseRoute]:
519521
return routes
520522

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

524526
async with fastmcp.server.context.Context(fastmcp=self):
525527
tools = await self._list_tools()
@@ -563,7 +565,7 @@ async def _handler(
563565
return await self._apply_middleware(mw_context, _handler)
564566

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

568570
async with fastmcp.server.context.Context(fastmcp=self):
569571
resources = await self._list_resources()
@@ -608,7 +610,7 @@ async def _handler(
608610
return await self._apply_middleware(mw_context, _handler)
609611

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

613615
async with fastmcp.server.context.Context(fastmcp=self):
614616
templates = await self._list_resource_templates()
@@ -653,7 +655,7 @@ async def _handler(
653655
return await self._apply_middleware(mw_context, _handler)
654656

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

658660
async with fastmcp.server.context.Context(fastmcp=self):
659661
prompts = await self._list_prompts()
@@ -712,7 +714,9 @@ async def _mcp_call_tool(
712714
Returns:
713715
List of MCP Content objects containing the tool results
714716
"""
715-
logger.debug("Handler called: call_tool %s with %s", key, arguments)
717+
logger.debug(
718+
f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
719+
)
716720

717721
async with fastmcp.server.context.Context(fastmcp=self):
718722
try:
@@ -754,7 +758,7 @@ async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceConten
754758
755759
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
756760
"""
757-
logger.debug("Handler called: read_resource %s", uri)
761+
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
758762

759763
async with fastmcp.server.context.Context(fastmcp=self):
760764
try:
@@ -809,7 +813,9 @@ async def _mcp_get_prompt(
809813
810814
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
811815
"""
812-
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
816+
logger.debug(
817+
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
818+
)
813819

814820
async with fastmcp.server.context.Context(fastmcp=self):
815821
try:
@@ -1966,9 +1972,11 @@ async def import_server(
19661972
self._prompt_manager.add_prompt(prompt)
19671973

19681974
if prefix:
1969-
logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
1975+
logger.debug(
1976+
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
1977+
)
19701978
else:
1971-
logger.debug(f"Imported server {server.name}")
1979+
logger.debug(f"[{self.name}] Imported server {server.name}")
19721980

19731981
@classmethod
19741982
def from_openapi(
@@ -2195,6 +2203,15 @@ def _should_enable_component(
21952203

21962204
return True
21972205

2206+
@classmethod
2207+
def generate_name(cls, name: str | None = None) -> str:
2208+
class_name = cls.__name__
2209+
2210+
if name is None:
2211+
return f"{class_name}-{secrets.token_hex(2)}"
2212+
else:
2213+
return f"{class_name}-{name}-{secrets.token_hex(2)}"
2214+
21982215

21992216
@dataclass
22002217
class MountedServer:

src/fastmcp/utilities/mcp_config.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from typing import Any
22

3-
from fastmcp.client.transports import ClientTransport
3+
from fastmcp.client.transports import (
4+
ClientTransport,
5+
SSETransport,
6+
StdioTransport,
7+
StreamableHttpTransport,
8+
)
49
from fastmcp.mcp_config import (
510
MCPConfig,
611
MCPServerTypes,
712
)
13+
from fastmcp.server.proxy import FastMCPProxy, ProxyClient
814
from fastmcp.server.server import FastMCP
915

1016

@@ -23,6 +29,7 @@ def mcp_server_type_to_servers_and_transports(
2329
mcp_server: MCPServerTypes,
2430
) -> tuple[str, FastMCP[Any], ClientTransport]:
2531
"""A utility function to convert each entry of an MCP Config into a transport and server."""
32+
2633
from fastmcp.mcp_config import (
2734
TransformingRemoteMCPServer,
2835
TransformingStdioMCPServer,
@@ -31,10 +38,19 @@ def mcp_server_type_to_servers_and_transports(
3138
server: FastMCP[Any]
3239
transport: ClientTransport
3340

41+
client_name = ProxyClient.generate_name(f"MCP_{name}")
42+
server_name = FastMCPProxy.generate_name(f"MCP_{name}")
43+
3444
if isinstance(mcp_server, TransformingRemoteMCPServer | TransformingStdioMCPServer):
35-
server, transport = mcp_server._to_server_and_underlying_transport()
45+
server, transport = mcp_server._to_server_and_underlying_transport(
46+
server_name=server_name,
47+
client_name=client_name,
48+
)
3649
else:
3750
transport = mcp_server.to_transport()
38-
server = FastMCP.as_proxy(backend=transport)
51+
client: ProxyClient[StreamableHttpTransport | SSETransport | StdioTransport] = (
52+
ProxyClient(transport=transport, name=client_name)
53+
)
54+
server = FastMCP.as_proxy(name=server_name, backend=client)
3955

4056
return name, server, transport

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("FastMCPProxy-")
9191

9292

9393
async def test_as_proxy_with_server(fastmcp_server):

0 commit comments

Comments
 (0)