From 21db5c6cc014e0b18a9e8d061fdb622ed1cd27d1 Mon Sep 17 00:00:00 2001 From: strawgate Date: Mon, 13 Apr 2026 00:26:45 -0500 Subject: [PATCH 1/3] OTEL: Instrument list operations and add method to delegate spans - Add server_span wrappers to list_tools, list_resources, list_resource_templates, and list_prompts in FastMCP server - Add client_span wrappers to list_tools_mcp, list_resources_mcp, list_resource_templates_mcp, and list_prompts_mcp in client mixins - Add optional `method` parameter to delegate_span in telemetry.py and set mcp.method.name on delegate spans - Update all delegate_span callers in fastmcp_provider.py to pass the MCP method name (tools/call, resources/read, prompts/get) - Add tests for new server list spans, client list spans, and delegate span method attributes Refs: #3887 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fastmcp/client/mixins/prompts.py | 16 +- src/fastmcp/client/mixins/resources.py | 32 +++- src/fastmcp/client/mixins/tools.py | 16 +- src/fastmcp/server/server.py | 24 ++- .../telemetry/test_client_list_tracing.py | 181 ++++++++++++++++++ .../server/telemetry/test_delegate_method.py | 80 ++++++++ tests/server/telemetry/test_list_tracing.py | 130 +++++++++++++ 7 files changed, 455 insertions(+), 24 deletions(-) create mode 100644 tests/client/telemetry/test_client_list_tracing.py create mode 100644 tests/server/telemetry/test_delegate_method.py create mode 100644 tests/server/telemetry/test_list_tracing.py diff --git a/src/fastmcp/client/mixins/prompts.py b/src/fastmcp/client/mixins/prompts.py index bb32cc280..de503b448 100644 --- a/src/fastmcp/client/mixins/prompts.py +++ b/src/fastmcp/client/mixins/prompts.py @@ -49,12 +49,18 @@ class ClientPromptsMixin: RuntimeError: If called while the client is not connected. McpError: If the request results in a TimeoutError | JSONRPCError """ - logger.debug(f"[{self.name}] called list_prompts") + with client_span( + "prompts/list", + "prompts/list", + "", + session_id=self.transport.get_session_id(), + ): + logger.debug(f"[{self.name}] called list_prompts") - result = await self._await_with_session_monitoring( - self.session.list_prompts(cursor=cursor) - ) - return result + result = await self._await_with_session_monitoring( + self.session.list_prompts(cursor=cursor) + ) + return result async def list_prompts( self: Client, diff --git a/src/fastmcp/client/mixins/resources.py b/src/fastmcp/client/mixins/resources.py index c97fbe90b..3cc845d50 100644 --- a/src/fastmcp/client/mixins/resources.py +++ b/src/fastmcp/client/mixins/resources.py @@ -48,12 +48,18 @@ class ClientResourcesMixin: RuntimeError: If called while the client is not connected. McpError: If the request results in a TimeoutError | JSONRPCError """ - logger.debug(f"[{self.name}] called list_resources") + with client_span( + "resources/list", + "resources/list", + "", + session_id=self.transport.get_session_id(), + ): + logger.debug(f"[{self.name}] called list_resources") - result = await self._await_with_session_monitoring( - self.session.list_resources(cursor=cursor) - ) - return result + result = await self._await_with_session_monitoring( + self.session.list_resources(cursor=cursor) + ) + return result async def list_resources( self: Client, @@ -118,12 +124,18 @@ class ClientResourcesMixin: RuntimeError: If called while the client is not connected. McpError: If the request results in a TimeoutError | JSONRPCError """ - logger.debug(f"[{self.name}] called list_resource_templates") + with client_span( + "resources/templates/list", + "resources/templates/list", + "", + session_id=self.transport.get_session_id(), + ): + logger.debug(f"[{self.name}] called list_resource_templates") - result = await self._await_with_session_monitoring( - self.session.list_resource_templates(cursor=cursor) - ) - return result + result = await self._await_with_session_monitoring( + self.session.list_resource_templates(cursor=cursor) + ) + return result async def list_resource_templates( self: Client, diff --git a/src/fastmcp/client/mixins/tools.py b/src/fastmcp/client/mixins/tools.py index 00a4a4265..07634b82e 100644 --- a/src/fastmcp/client/mixins/tools.py +++ b/src/fastmcp/client/mixins/tools.py @@ -53,12 +53,18 @@ class ClientToolsMixin: RuntimeError: If called while the client is not connected. McpError: If the request results in a TimeoutError | JSONRPCError """ - logger.debug(f"[{self.name}] called list_tools") + with client_span( + "tools/list", + "tools/list", + "", + session_id=self.transport.get_session_id(), + ): + logger.debug(f"[{self.name}] called list_tools") - result = await self._await_with_session_monitoring( - self.session.list_tools(cursor=cursor) - ) - return result + result = await self._await_with_session_monitoring( + self.session.list_tools(cursor=cursor) + ) + return result async def list_tools( self: Client, diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 415f12245..b50d4c2fb 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -606,7 +606,10 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + with server_span( + "tools/list", "tools/list", self.name, "tool", "" + ): + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message=mcp.types.ListToolsRequest(method="tools/list"), @@ -740,7 +743,10 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + with server_span( + "resources/list", "resources/list", self.name, "resource", "" + ): + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, @@ -871,7 +877,14 @@ class FastMCP( auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + with server_span( + "resources/templates/list", + "resources/templates/list", + self.name, + "resource_template", + "", + ): + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, @@ -997,7 +1010,10 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + with server_span( + "prompts/list", "prompts/list", self.name, "prompt", "" + ): + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, diff --git a/tests/client/telemetry/test_client_list_tracing.py b/tests/client/telemetry/test_client_list_tracing.py new file mode 100644 index 000000000..33f066f5f --- /dev/null +++ b/tests/client/telemetry/test_client_list_tracing.py @@ -0,0 +1,181 @@ +"""Tests for client OpenTelemetry tracing on list operations.""" + +from __future__ import annotations + +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import SpanKind + +from fastmcp import Client, FastMCP + + +class TestClientListToolsTracing: + """Tests for client tools/list tracing.""" + + async def test_list_tools_creates_client_span( + self, trace_exporter: InMemorySpanExporter + ): + server = FastMCP("test-server") + + @server.tool() + def greet(name: str) -> str: + return f"Hello, {name}!" + + client = Client(server) + async with client: + tools = await client.list_tools() + assert len(tools) == 1 + + spans = trace_exporter.get_finished_spans() + client_spans = [ + s + for s in spans + if s.name == "tools/list" + and s.attributes is not None + and "fastmcp.server.name" not in s.attributes + ] + assert len(client_spans) >= 1 + + span = client_spans[0] + assert span.kind == SpanKind.CLIENT + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "tools/list" + + async def test_list_tools_creates_both_client_and_server_spans( + self, trace_exporter: InMemorySpanExporter + ): + server = FastMCP("test-server") + + @server.tool() + def add(a: int, b: int) -> int: + return a + b + + client = Client(server) + async with client: + await client.list_tools() + + spans = trace_exporter.get_finished_spans() + tools_list_spans = [s for s in spans if s.name == "tools/list"] + assert len(tools_list_spans) >= 2 + + client_span = next( + ( + s + for s in tools_list_spans + if s.attributes is not None + and "fastmcp.server.name" not in s.attributes + ), + None, + ) + server_span = next( + ( + s + for s in tools_list_spans + if s.attributes is not None + and "fastmcp.server.name" in s.attributes + ), + None, + ) + + assert client_span is not None, "Client should create a span" + assert server_span is not None, "Server should create a span" + assert client_span.kind == SpanKind.CLIENT + assert server_span.kind == SpanKind.SERVER + + +class TestClientListResourcesTracing: + """Tests for client resources/list tracing.""" + + async def test_list_resources_creates_client_span( + self, trace_exporter: InMemorySpanExporter + ): + server = FastMCP("test-server") + + @server.resource("data://config") + def get_config() -> str: + return "config" + + client = Client(server) + async with client: + resources = await client.list_resources() + assert len(resources) >= 1 + + spans = trace_exporter.get_finished_spans() + client_spans = [ + s + for s in spans + if s.name == "resources/list" + and s.attributes is not None + and "fastmcp.server.name" not in s.attributes + ] + assert len(client_spans) >= 1 + + span = client_spans[0] + assert span.kind == SpanKind.CLIENT + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "resources/list" + + +class TestClientListResourceTemplatesTracing: + """Tests for client resources/templates/list tracing.""" + + async def test_list_resource_templates_creates_client_span( + self, trace_exporter: InMemorySpanExporter + ): + server = FastMCP("test-server") + + @server.resource("users://{user_id}/profile") + def get_profile(user_id: str) -> str: + return f"profile {user_id}" + + client = Client(server) + async with client: + templates = await client.list_resource_templates() + assert len(templates) >= 1 + + spans = trace_exporter.get_finished_spans() + client_spans = [ + s + for s in spans + if s.name == "resources/templates/list" + and s.attributes is not None + and "fastmcp.server.name" not in s.attributes + ] + assert len(client_spans) >= 1 + + span = client_spans[0] + assert span.kind == SpanKind.CLIENT + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "resources/templates/list" + + +class TestClientListPromptsTracing: + """Tests for client prompts/list tracing.""" + + async def test_list_prompts_creates_client_span( + self, trace_exporter: InMemorySpanExporter + ): + server = FastMCP("test-server") + + @server.prompt() + def greeting() -> str: + return "Hello!" + + client = Client(server) + async with client: + prompts = await client.list_prompts() + assert len(prompts) == 1 + + spans = trace_exporter.get_finished_spans() + client_spans = [ + s + for s in spans + if s.name == "prompts/list" + and s.attributes is not None + and "fastmcp.server.name" not in s.attributes + ] + assert len(client_spans) >= 1 + + span = client_spans[0] + assert span.kind == SpanKind.CLIENT + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "prompts/list" diff --git a/tests/server/telemetry/test_delegate_method.py b/tests/server/telemetry/test_delegate_method.py new file mode 100644 index 000000000..87227750b --- /dev/null +++ b/tests/server/telemetry/test_delegate_method.py @@ -0,0 +1,80 @@ +"""Tests for mcp.method.name attribute on delegate spans.""" + +from __future__ import annotations + +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from fastmcp import FastMCP + + +class TestDelegateSpanMethod: + """Tests that delegate spans include mcp.method.name.""" + + async def test_mounted_tool_delegate_has_method( + self, trace_exporter: InMemorySpanExporter + ): + child = FastMCP("child-server") + + @child.tool() + def child_tool() -> str: + return "result" + + parent = FastMCP("parent-server") + parent.mount(child, namespace="child") + + await parent.call_tool("child_child_tool", {}) + + spans = trace_exporter.get_finished_spans() + delegate_span = next( + (s for s in spans if s.name == "delegate child_tool"), None + ) + assert delegate_span is not None + assert delegate_span.attributes is not None + assert delegate_span.attributes["mcp.method.name"] == "tools/call" + + async def test_mounted_resource_delegate_has_method( + self, trace_exporter: InMemorySpanExporter + ): + child = FastMCP("child-server") + + @child.resource("data://config") + def child_config() -> str: + return "config data" + + parent = FastMCP("parent-server") + parent.mount(child, namespace="child") + + await parent.read_resource("data://child/config") + + spans = trace_exporter.get_finished_spans() + delegate_spans = [ + s + for s in spans + if s.name.startswith("delegate") and "data://config" in s.name + ] + assert len(delegate_spans) >= 1 + span = delegate_spans[0] + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "resources/read" + + async def test_mounted_prompt_delegate_has_method( + self, trace_exporter: InMemorySpanExporter + ): + child = FastMCP("child-server") + + @child.prompt() + def child_prompt() -> str: + return "Hello from child!" + + parent = FastMCP("parent-server") + parent.mount(child, namespace="child") + + await parent.render_prompt("child_child_prompt", {}) + + spans = trace_exporter.get_finished_spans() + delegate_span = next( + (s for s in spans if s.name == "delegate child_prompt"), None + ) + assert delegate_span is not None + assert delegate_span.attributes is not None + assert delegate_span.attributes["mcp.method.name"] == "prompts/get" diff --git a/tests/server/telemetry/test_list_tracing.py b/tests/server/telemetry/test_list_tracing.py new file mode 100644 index 000000000..92ff4f7fb --- /dev/null +++ b/tests/server/telemetry/test_list_tracing.py @@ -0,0 +1,130 @@ +"""Tests for server-level OpenTelemetry tracing on list operations.""" + +from __future__ import annotations + +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import SpanKind + +from fastmcp import FastMCP + + +class TestListToolsTracing: + async def test_list_tools_creates_span(self, trace_exporter: InMemorySpanExporter): + mcp = FastMCP("test-server") + + @mcp.tool() + def greet(name: str) -> str: + return f"Hello, {name}!" + + tools = await mcp.list_tools() + assert len(tools) == 1 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "tools/list"] + assert len(list_spans) >= 1 + + span = list_spans[0] + assert span.kind == SpanKind.SERVER + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "tools/list" + assert span.attributes["fastmcp.server.name"] == "test-server" + assert span.attributes["fastmcp.component.type"] == "tool" + + async def test_list_tools_empty_creates_span( + self, trace_exporter: InMemorySpanExporter + ): + mcp = FastMCP("test-server") + + tools = await mcp.list_tools() + assert len(tools) == 0 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "tools/list"] + assert len(list_spans) >= 1 + + +class TestListResourcesTracing: + async def test_list_resources_creates_span( + self, trace_exporter: InMemorySpanExporter + ): + mcp = FastMCP("test-server") + + @mcp.resource("config://app") + def get_config() -> str: + return "config" + + resources = await mcp.list_resources() + assert len(resources) >= 1 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "resources/list"] + assert len(list_spans) >= 1 + + span = list_spans[0] + assert span.kind == SpanKind.SERVER + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "resources/list" + assert span.attributes["fastmcp.server.name"] == "test-server" + assert span.attributes["fastmcp.component.type"] == "resource" + + +class TestListResourceTemplatesTracing: + async def test_list_resource_templates_creates_span( + self, trace_exporter: InMemorySpanExporter + ): + mcp = FastMCP("test-server") + + @mcp.resource("users://{user_id}/profile") + def get_profile(user_id: str) -> str: + return f"profile {user_id}" + + templates = await mcp.list_resource_templates() + assert len(templates) >= 1 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "resources/templates/list"] + assert len(list_spans) >= 1 + + span = list_spans[0] + assert span.kind == SpanKind.SERVER + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "resources/templates/list" + assert span.attributes["fastmcp.server.name"] == "test-server" + assert span.attributes["fastmcp.component.type"] == "resource_template" + + +class TestListPromptsTracing: + async def test_list_prompts_creates_span( + self, trace_exporter: InMemorySpanExporter + ): + mcp = FastMCP("test-server") + + @mcp.prompt() + def greeting(name: str) -> str: + return f"Hello, {name}!" + + prompts = await mcp.list_prompts() + assert len(prompts) == 1 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "prompts/list"] + assert len(list_spans) >= 1 + + span = list_spans[0] + assert span.kind == SpanKind.SERVER + assert span.attributes is not None + assert span.attributes["mcp.method.name"] == "prompts/list" + assert span.attributes["fastmcp.server.name"] == "test-server" + assert span.attributes["fastmcp.component.type"] == "prompt" + + async def test_list_prompts_empty_creates_span( + self, trace_exporter: InMemorySpanExporter + ): + mcp = FastMCP("test-server") + + prompts = await mcp.list_prompts() + assert len(prompts) == 0 + + spans = trace_exporter.get_finished_spans() + list_spans = [s for s in spans if s.name == "prompts/list"] + assert len(list_spans) >= 1 From 872ea73628c555355bfc049321254a3b69833674 Mon Sep 17 00:00:00 2001 From: strawgate Date: Mon, 13 Apr 2026 00:43:57 -0500 Subject: [PATCH 2/3] fix: correct indentation and span scope in list operation tracing Move server_span() inside the else branch (after middleware check) for list_tools, list_resources, list_resource_templates, and list_prompts, matching the existing pattern in call_tool/read_resource/render_prompt. Fix 2-space indentation to 4-space to match the rest of the file. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fastmcp/server/server.py | 192 ++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 94 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index b50d4c2fb..b653711aa 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -606,10 +606,7 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - with server_span( - "tools/list", "tools/list", self.name, "tool", "" - ): - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message=mcp.types.ListToolsRequest(method="tools/list"), @@ -623,31 +620,35 @@ class FastMCP( call_next=lambda context: self.list_tools(run_middleware=False), ) - # Get all tools, apply session transforms, then filter enabled - # and model-visible (app-only tools are hidden from the model). - tools = list(await super().list_tools()) - tools = await apply_session_transforms(tools) - tools = [t for t in tools if is_enabled(t) and _is_model_visible(t)] + # Core logic: list tools + with server_span( + "tools/list", "tools/list", self.name, "tool", "" + ): + # Get all tools, apply session transforms, then filter enabled + # and model-visible (app-only tools are hidden from the model). + tools = list(await super().list_tools()) + tools = await apply_session_transforms(tools) + tools = [t for t in tools if is_enabled(t) and _is_model_visible(t)] - # Rewrite per-tool Prefab renderer URIs based on the tool's - # mount-point address. The walk pairs each tool with the - # provider that yielded it, computes the hashed URI, and - # produces a model_copy with the URI in place. Original - # Tool objects are not mutated. - tools = self._rewrite_prefab_uris(tools) + # Rewrite per-tool Prefab renderer URIs based on the tool's + # mount-point address. The walk pairs each tool with the + # provider that yielded it, computes the hashed URI, and + # produces a model_copy with the URI in place. Original + # Tool objects are not mutated. + tools = self._rewrite_prefab_uris(tools) - skip_auth, token = _get_auth_context() - authorized: list[Tool] = [] - for tool in tools: - if not skip_auth and tool.auth is not None: - ctx = AuthContext(token=token, component=tool) - try: - if not await run_auth_checks(tool.auth, ctx): + skip_auth, token = _get_auth_context() + authorized: list[Tool] = [] + for tool in tools: + if not skip_auth and tool.auth is not None: + ctx = AuthContext(token=token, component=tool) + try: + if not await run_auth_checks(tool.auth, ctx): + continue + except AuthorizationError: continue - except AuthorizationError: - continue - authorized.append(tool) - return authorized + authorized.append(tool) + return authorized async def _get_tool( self, name: str, version: VersionSpec | None = None @@ -743,10 +744,7 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - with server_span( - "resources/list", "resources/list", self.name, "resource", "" - ): - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, @@ -760,32 +758,36 @@ class FastMCP( call_next=lambda context: self.list_resources(run_middleware=False), ) - # Get all resources, apply session transforms, then filter enabled - resources = list(await super().list_resources()) - resources = await apply_session_transforms(resources) - resources = [r for r in resources if is_enabled(r)] + # Core logic: list resources + with server_span( + "resources/list", "resources/list", self.name, "resource", "" + ): + # Get all resources, apply session transforms, then filter enabled + resources = list(await super().list_resources()) + resources = await apply_session_transforms(resources) + resources = [r for r in resources if is_enabled(r)] - # Append synthetic Prefab renderer resources — one per - # prefab tool, hashed by mount address. These don't live on - # any provider's storage; they're computed on demand. - from fastmcp.server.providers.prefab_synthesis import ( - synthesize_prefab_resources, - ) + # Append synthetic Prefab renderer resources — one per + # prefab tool, hashed by mount address. These don't live on + # any provider's storage; they're computed on demand. + from fastmcp.server.providers.prefab_synthesis import ( + synthesize_prefab_resources, + ) - resources.extend(await synthesize_prefab_resources(self)) + resources.extend(await synthesize_prefab_resources(self)) - skip_auth, token = _get_auth_context() - authorized: list[Resource] = [] - for resource in resources: - if not skip_auth and resource.auth is not None: - ctx = AuthContext(token=token, component=resource) - try: - if not await run_auth_checks(resource.auth, ctx): + skip_auth, token = _get_auth_context() + authorized: list[Resource] = [] + for resource in resources: + if not skip_auth and resource.auth is not None: + ctx = AuthContext(token=token, component=resource) + try: + if not await run_auth_checks(resource.auth, ctx): + continue + except AuthorizationError: continue - except AuthorizationError: - continue - authorized.append(resource) - return authorized + authorized.append(resource) + return authorized async def _get_resource( self, uri: str, version: VersionSpec | None = None @@ -877,14 +879,7 @@ class FastMCP( auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - with server_span( - "resources/templates/list", - "resources/templates/list", - self.name, - "resource_template", - "", - ): - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, @@ -900,23 +895,31 @@ class FastMCP( ), ) - # Get all templates, apply session transforms, then filter enabled - templates = list(await super().list_resource_templates()) - templates = await apply_session_transforms(templates) - templates = [t for t in templates if is_enabled(t)] + # Core logic: list resource templates + with server_span( + "resources/templates/list", + "resources/templates/list", + self.name, + "resource_template", + "", + ): + # Get all templates, apply session transforms, then filter enabled + templates = list(await super().list_resource_templates()) + templates = await apply_session_transforms(templates) + templates = [t for t in templates if is_enabled(t)] - skip_auth, token = _get_auth_context() - authorized: list[ResourceTemplate] = [] - for template in templates: - if not skip_auth and template.auth is not None: - ctx = AuthContext(token=token, component=template) - try: - if not await run_auth_checks(template.auth, ctx): + skip_auth, token = _get_auth_context() + authorized: list[ResourceTemplate] = [] + for template in templates: + if not skip_auth and template.auth is not None: + ctx = AuthContext(token=token, component=template) + try: + if not await run_auth_checks(template.auth, ctx): + continue + except AuthorizationError: continue - except AuthorizationError: - continue - authorized.append(template) - return authorized + authorized.append(template) + return authorized async def _get_resource_template( self, uri: str, version: VersionSpec | None = None @@ -1010,10 +1013,7 @@ class FastMCP( and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ - with server_span( - "prompts/list", "prompts/list", self.name, "prompt", "" - ): - async with fastmcp.server.context.Context(fastmcp=self) as ctx: + async with fastmcp.server.context.Context(fastmcp=self) as ctx: if run_middleware: mw_context = MiddlewareContext( message={}, @@ -1027,23 +1027,27 @@ class FastMCP( call_next=lambda context: self.list_prompts(run_middleware=False), ) - # Get all prompts, apply session transforms, then filter enabled - prompts = list(await super().list_prompts()) - prompts = await apply_session_transforms(prompts) - prompts = [p for p in prompts if is_enabled(p)] + # Core logic: list prompts + with server_span( + "prompts/list", "prompts/list", self.name, "prompt", "" + ): + # Get all prompts, apply session transforms, then filter enabled + prompts = list(await super().list_prompts()) + prompts = await apply_session_transforms(prompts) + prompts = [p for p in prompts if is_enabled(p)] - skip_auth, token = _get_auth_context() - authorized: list[Prompt] = [] - for prompt in prompts: - if not skip_auth and prompt.auth is not None: - ctx = AuthContext(token=token, component=prompt) - try: - if not await run_auth_checks(prompt.auth, ctx): + skip_auth, token = _get_auth_context() + authorized: list[Prompt] = [] + for prompt in prompts: + if not skip_auth and prompt.auth is not None: + ctx = AuthContext(token=token, component=prompt) + try: + if not await run_auth_checks(prompt.auth, ctx): + continue + except AuthorizationError: continue - except AuthorizationError: - continue - authorized.append(prompt) - return authorized + authorized.append(prompt) + return authorized async def _get_prompt( self, name: str, version: VersionSpec | None = None From e1b2c36e5142c1f09b08064bac0bba6a82823af4 Mon Sep 17 00:00:00 2001 From: William Easton Date: Mon, 13 Apr 2026 08:20:01 -0500 Subject: [PATCH 3/3] Fix ruff format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fastmcp/server/server.py | 8 ++------ tests/client/telemetry/test_client_list_tracing.py | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index b653711aa..2f00e2017 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -621,9 +621,7 @@ class FastMCP( ) # Core logic: list tools - with server_span( - "tools/list", "tools/list", self.name, "tool", "" - ): + with server_span("tools/list", "tools/list", self.name, "tool", ""): # Get all tools, apply session transforms, then filter enabled # and model-visible (app-only tools are hidden from the model). tools = list(await super().list_tools()) @@ -1028,9 +1026,7 @@ class FastMCP( ) # Core logic: list prompts - with server_span( - "prompts/list", "prompts/list", self.name, "prompt", "" - ): + with server_span("prompts/list", "prompts/list", self.name, "prompt", ""): # Get all prompts, apply session transforms, then filter enabled prompts = list(await super().list_prompts()) prompts = await apply_session_transforms(prompts) diff --git a/tests/client/telemetry/test_client_list_tracing.py b/tests/client/telemetry/test_client_list_tracing.py index 33f066f5f..45038bbad 100644 --- a/tests/client/telemetry/test_client_list_tracing.py +++ b/tests/client/telemetry/test_client_list_tracing.py @@ -70,8 +70,7 @@ class TestClientListToolsTracing: ( s for s in tools_list_spans - if s.attributes is not None - and "fastmcp.server.name" in s.attributes + if s.attributes is not None and "fastmcp.server.name" in s.attributes ), None, )