Skip to content

Add OpenTelemetry tracing support#2869

Merged
chrisguidry merged 10 commits intomainfrom
otel-2813
Jan 14, 2026
Merged

Add OpenTelemetry tracing support#2869
chrisguidry merged 10 commits intomainfrom
otel-2813

Conversation

@chrisguidry
Copy link
Copy Markdown
Collaborator

@chrisguidry chrisguidry commented Jan 13, 2026

Adds OpenTelemetry tracing for observability into FastMCP server and client operations.

Server spans are created for tool calls, resource reads, and prompt renders with attributes like component key, component type, provider type, session ID, and auth context. Client spans wrap outgoing calls with trace context propagation via W3C headers in request meta.

# Tracing is always active - to export traces, configure an OTel SDK
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)

# Then use fastmcp normally - spans export to your configured backend
from fastmcp import FastMCP, Client

Components provide their own span attributes through a get_span_attributes() method that subclasses override - this lets LocalProvider, FastMCPProvider, and ProxyProvider each include relevant context (original names, backend URIs, etc).

image

Closes #2813

🤖 Generated with Claude Code

@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. client Related to the FastMCP client SDK or client-side functionality. documentation Updates to docs, examples, or guides. Primary change is documentation-related. labels Jan 13, 2026
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Test times out on Windows when logging output triggers a lazy import of Rich library's Windows renderer module.

Root Cause: On Windows, when Rich's logging handler attempts to output debug logs, it lazily imports rich._windows_renderer. This import is intercepted by beartype's import hook (visible in the stack trace at beartype.claw._importlib._clawimpload.py:359), which performs file I/O to read and process the module. This file I/O operation hangs, causing the test to timeout after 5 seconds.

The issue is specific to Windows and occurs when:

  1. A test calls logger.debug() for the first time in a test process
  2. Rich's console tries to determine if legacy Windows rendering is needed
  3. The rich._windows_renderer module is imported on-demand
  4. Beartype's import hook intercepts and stalls during file reading

Suggested Solution: Pre-import rich._windows_renderer in the import_rich_rule fixture to force the import to happen before tests run, similar to how rich.rule is already pre-imported.

Specific changes needed:

# tests/conftest.py, lines 30-35
@pytest.fixture(autouse=True)
def import_rich_rule():
    # What a hack
    import rich.rule  # noqa: F401
    
    # Pre-import Windows renderer to avoid lazy import timeout with beartype on Windows
    if sys.platform == "win32":
        try:
            import rich._windows_renderer  # noqa: F401
        except ImportError:
            pass  # Module might not exist in all Rich versions
    
    yield

This will ensure the problematic import happens during fixture setup (where it doesn't timeout) rather than during test execution.

Detailed Analysis

Stack Trace Analysis:

The timeout occurs at this call stack:

oauth_proxy.py:1747 logger.debug(...)
→ rich/logging.py:178 emit(record)
→ rich/console.py:870 __exit__
→ rich/console.py:2068 _write_buffer  
→ from rich._windows_renderer import legacy_windows_render
→ beartype/claw/_importlib/_clawimpload.py:359 get_code
→ [HANGS HERE]

Why the existing workaround helps:

The comment "What a hack" at line 32 of conftest.py acknowledges that pre-importing Rich modules works around similar issues. This PR adds more logging (specifically the debug logs at oauth_proxy.py:1742-1749), which triggers more Rich console operations and exposes this Windows-specific race condition.

Why this only affects Windows:

The _windows_renderer module is Windows-specific and only imported when Rich detects it's running on Windows. Linux and macOS tests use different code paths that don't trigger this lazy import.

Related Issues:

  • Rich #3437: Documents issues with Rich's Windows console detection under pytest
  • Rich #1734: Previous test freezing issues in Rich (resolved in 10.15.2, but shows Rich has had threading/import issues on Windows)
Related Files
  • tests/conftest.py:31-35 - Contains existing workaround for Rich module import issues
  • src/fastmcp/server/auth/oauth_proxy.py:1747 - The logger.debug() call that triggers the timeout
  • tests/server/auth/test_oauth_proxy.py:1694-1714 - The failing test

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Test test_get_routes_calls_set_mcp_path times out on Windows when logging output triggers a lazy import of Rich library's Windows renderer module.

Root Cause: On Windows, when Rich's logging handler attempts to output debug logs, it lazily imports rich._windows_renderer. This import is intercepted by beartype's import hook (visible in the stack trace at beartype.claw._importlib._clawimpload.py:359), which performs file I/O to read and process the module. This file I/O operation hangs, causing the test to timeout after 5 seconds.

The issue is specific to Windows and occurs when:

  1. A test calls logger.debug() for the first time in a test process
  2. Rich's console tries to determine if legacy Windows rendering is needed
  3. The rich._windows_renderer module is imported on-demand
  4. Beartype's import hook intercepts and stalls during file reading

Suggested Solution: Pre-import rich._windows_renderer in the import_rich_rule fixture to force the import to happen before tests run, similar to how rich.rule is already pre-imported.

Specific changes needed:

# tests/conftest.py, lines 30-35
@pytest.fixture(autouse=True)
def import_rich_rule():
    # What a hack
    import rich.rule  # noqa: F401
    
    # Pre-import Windows renderer to avoid lazy import timeout with beartype on Windows
    if sys.platform == "win32":
        try:
            import rich._windows_renderer  # noqa: F401
        except ImportError:
            pass  # Module might not exist in all Rich versions
    
    yield

This will ensure the problematic import happens during fixture setup (where it doesn't timeout) rather than during test execution.

Detailed Analysis

Stack Trace Analysis:

The timeout occurs at this call stack:

oauth_proxy.py:1747 logger.debug(...)
→ rich/logging.py:178 emit(record)
→ rich/console.py:870 __exit__
→ rich/console.py:2068 _write_buffer  
→ from rich._windows_renderer import legacy_windows_render
→ beartype/claw/_importlib/_clawimpload.py:359 get_code
→ [HANGS HERE]

Why the existing workaround helps:

The comment "What a hack" at line 32 of conftest.py acknowledges that pre-importing Rich modules works around similar issues. This PR adds more logging (specifically the debug logs at oauth_proxy.py:1742-1749), which triggers more Rich console operations and exposes this Windows-specific race condition.

Why this only affects Windows:

The _windows_renderer module is Windows-specific and only imported when Rich detects it's running on Windows. Linux and macOS tests use different code paths that don't trigger this lazy import.

Related Issues:

  • Rich #3437: Documents issues with Rich's Windows console detection under pytest
  • Rich #1734: Previous test freezing issues in Rich (resolved in 10.15.2, but shows Rich has had threading/import issues on Windows)
Related Files
  • tests/conftest.py:31-35 - Contains existing workaround for Rich module import issues
  • src/fastmcp/server/auth/oauth_proxy.py:1747 - The logger.debug() call that triggers the timeout
  • tests/server/auth/test_oauth_proxy.py:1694-1714 - The failing test

Comment thread docs/servers/telemetry.mdx Outdated
icon: chart-line
---

FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for all MCP operations, providing visibility into server behavior, request handling, and provider delegation chains.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say for all tool, prompt, resource, and resource template operations. We're deferring the other lower-level stuff to the mcp lower-level SDK

Comment thread docs/servers/telemetry.mdx Outdated

## How It Works

FastMCP uses the OpenTelemetry API (not the SDK) for instrumentation. This means:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to say "not the SDK" in this

Comment thread docs/servers/telemetry.mdx
Comment thread examples/diagnostics/__init__.py Outdated
@@ -0,0 +1 @@
"""FastMCP Diagnostics example - for testing tracing, errors, and observability."""
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this file, this isn't used as a package

@chrisguidry chrisguidry marked this pull request as ready for review January 14, 2026 14:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 14, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This pull request introduces native OpenTelemetry instrumentation throughout FastMCP. It adds telemetry context managers and span creation utilities at the core infrastructure level (src/fastmcp/telemetry.py), server-side RPC paths (src/fastmcp/server/server.py, src/fastmcp/server/telemetry.py), and client-side operations (src/fastmcp/client/client.py, src/fastmcp/client/telemetry.py). Span attributes are now collected from component classes via new get_span_attributes() methods. Trace context is propagated through request metadata using inject/extract utilities. Provider delegation paths in both FastMCP and proxy providers are wrapped with telemetry spans. Documentation, examples, and a diagnostics server with tracing support are included.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add OpenTelemetry tracing support' directly and concisely describes the primary change: introducing OpenTelemetry tracing capabilities to the FastMCP project.
Description check ✅ Passed The PR description provides clear context about OpenTelemetry tracing implementation, includes code examples, and explicitly references the linked issue #2813. However, it does not include a completed contributors/review checklist as specified in the template.
Linked Issues check ✅ Passed The PR comprehensively implements native OTEL integration at the Provider layer as required by #2813, capturing tracing across all provider implementations including nested/mounted and proxy servers.
Out of Scope Changes check ✅ Passed All changes are within scope: telemetry instrumentation added across client/server, components, providers (LocalProvider, FastMCPProvider, ProxyProvider), documentation, and examples directly support the OTEL integration objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
examples/diagnostics/__init__.py (1)

1-1: Unnecessary __init__.py for examples directory.

As noted in previous comments, this file can be removed since the examples directory isn't used as an importable package.

🧹 Nitpick comments (8)
examples/run_with_tracing.py (2)

22-25: Add return type annotation to main function.

Per coding guidelines, Python ≥3.10 code requires full type annotations.

Suggested fix
-def main():
+def main() -> None:

54-54: Use spread operator for cleaner list construction.

As flagged by static analysis (Ruff RUF005), the spread operator is more idiomatic.

Suggested fix
-    sys.argv = ["fastmcp", "run"] + sys.argv[1:]
+    sys.argv = ["fastmcp", "run", *sys.argv[1:]]
docs/servers/telemetry.mdx (2)

291-301: Add return type annotation to test helper function.

Per coding guidelines, all Python code examples should include full type annotations.

Suggested fix
-async def test_tool_creates_span(trace_exporter):
+async def test_tool_creates_span(trace_exporter: InMemorySpanExporter) -> None:
     mcp = FastMCP("test")

     `@mcp.tool`()
     def hello() -> str:
         return "world"

269-289: Testing section provides practical guidance.

The in-memory exporter fixture pattern is a good practice for testing telemetry without external dependencies. Consider adding trace.set_tracer_provider(TracerProvider()) in teardown to reset to a no-op provider, preventing state leakage between tests.

Enhanced fixture with cleanup
 `@pytest.fixture`
 def trace_exporter():
     exporter = InMemorySpanExporter()
     provider = TracerProvider()
     provider.add_span_processor(SimpleSpanProcessor(exporter))
+    original_provider = trace.get_tracer_provider()
     trace.set_tracer_provider(provider)
     yield exporter
     exporter.clear()
+    trace.set_tracer_provider(original_provider)
examples/diagnostics/server.py (2)

46-52: Synchronous blocking call in async context manager.

The readiness check uses synchronous httpx.get() and time.sleep() inside an async context manager. This blocks the event loop. Consider using httpx.AsyncClient and asyncio.sleep().

Proposed async readiness check
+import asyncio
+
 # Wait for server to be ready
-for _ in range(50):
-    try:
-        httpx.get(f"http://localhost:{ECHO_SERVER_PORT}/sse", timeout=0.1)
-        break
-    except Exception:
-        time.sleep(0.1)
+async with httpx.AsyncClient() as http_client:
+    for _ in range(50):
+        try:
+            await http_client.get(f"http://localhost:{ECHO_SERVER_PORT}/sse", timeout=0.1)
+            break
+        except Exception:
+            await asyncio.sleep(0.1)

58-62: Subprocess output not captured or logged on termination failure.

If the subprocess fails to terminate within the timeout, the exception propagates without any diagnostics. Consider logging proc.stdout/proc.stderr on failure for debugging.

Suggested improvement
     try:
         yield
     finally:
-        proc.terminate()
-        proc.wait(timeout=5)
+        proc.terminate()
+        try:
+            proc.wait(timeout=5)
+        except subprocess.TimeoutExpired:
+            proc.kill()
+            stdout, stderr = proc.communicate()
+            print(f"Echo server did not terminate gracefully. stderr: {stderr.decode()}")
src/fastmcp/server/providers/proxy.py (2)

116-154: Synchronous context manager wrapping async code.

client_span is a synchronous context manager (from the external snippet showing @contextmanager), but the code inside contains await statements. This works but the span will remain open during I/O suspension. This is expected behavior for tracing async operations, but ensure client_span doesn't acquire any non-reentrant locks.

Additionally, the context parameter at line 113 is shadowed at line 123 by context = get_context(). The original parameter is unused.

Consider removing unused parameter shadowing
     async def run(
         self,
         arguments: dict[str, Any],
-        context: Context | None = None,
+        context: Context | None = None,  # noqa: ARG002 - required by interface
     ) -> ToolResult:

Or if the interface doesn't require it:

     async def run(
         self,
         arguments: dict[str, Any],
-        context: Context | None = None,
     ) -> ToolResult:

336-358: Code duplication: resource content processing repeated.

The content processing loop (lines 336-358) is nearly identical to the one in ProxyResource.read (lines 237-257). Consider extracting a helper function.

Suggested helper extraction
def _process_resource_contents(
    result: Sequence[TextResourceContents | BlobResourceContents],
) -> list[ResourceContent]:
    """Convert MCP resource contents to FastMCP ResourceContent."""
    contents: list[ResourceContent] = []
    for item in result:
        if isinstance(item, TextResourceContents):
            contents.append(
                ResourceContent(
                    content=item.text,
                    mime_type=item.mimeType,
                    meta=item.meta,
                )
            )
        elif isinstance(item, BlobResourceContents):
            contents.append(
                ResourceContent(
                    content=base64.b64decode(item.blob),
                    mime_type=item.mimeType,
                    meta=item.meta,
                )
            )
        else:
            raise ResourceError(f"Unsupported content type: {type(item)}")
    return contents

Then use it in both ProxyResource.read and ProxyTemplate.create_resource.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee3bec and 7f356c4.

⛔ Files ignored due to path filters (12)
  • loq.toml is excluded by none and included by none
  • pyproject.toml is excluded by none and included by none
  • tests/client/telemetry/__init__.py is excluded by none and included by none
  • tests/client/telemetry/test_client_tracing.py is excluded by none and included by none
  • tests/conftest.py is excluded by none and included by none
  • tests/server/telemetry/__init__.py is excluded by none and included by none
  • tests/server/telemetry/test_provider_tracing.py is excluded by none and included by none
  • tests/server/telemetry/test_server_tracing.py is excluded by none and included by none
  • tests/telemetry/__init__.py is excluded by none and included by none
  • tests/telemetry/test_module.py is excluded by none and included by none
  • tests/test_mcp_config.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (19)
  • docs/docs.json
  • docs/servers/telemetry.mdx
  • examples/diagnostics/__init__.py
  • examples/diagnostics/client_with_tracing.py
  • examples/diagnostics/server.py
  • examples/run_with_tracing.py
  • src/fastmcp/client/client.py
  • src/fastmcp/client/telemetry.py
  • src/fastmcp/client/transports.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/resources/resource.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/telemetry.py
  • src/fastmcp/telemetry.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/utilities/components.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/utilities/components.py
  • src/fastmcp/client/transports.py
  • examples/diagnostics/client_with_tracing.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/client/telemetry.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/server/telemetry.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/client/client.py
  • src/fastmcp/resources/resource.py
  • src/fastmcp/telemetry.py
  • examples/run_with_tracing.py
  • examples/diagnostics/__init__.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • examples/diagnostics/server.py
  • src/fastmcp/server/server.py
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/servers/telemetry.mdx
**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

**/__init__.py: Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules
Core types that define a module's purpose should be exported; specialized features can live in submodules

Files:

  • examples/diagnostics/__init__.py
🧠 Learnings (6)
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Maintain consistency across all four MCP object types (Tools, Resources, Resource Templates, and Prompts) when implementing similar features

Applied to files:

  • docs/servers/telemetry.mdx
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Applies to src/tools/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Tools (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/telemetry.mdx
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Applies to src/prompts/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Prompts (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/telemetry.mdx
📚 Learning: 2026-01-12T16:24:55.006Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:55.006Z
Learning: Applies to src/resources/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Resources (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/telemetry.mdx
📚 Learning: 2026-01-13T03:11:40.917Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.917Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • src/fastmcp/telemetry.py
  • examples/diagnostics/__init__.py
📚 Learning: 2025-11-03T17:36:13.363Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 2355
File: docs/clients/client.mdx:226-246
Timestamp: 2025-11-03T17:36:13.363Z
Learning: In FastMCP documentation, prefer showing the happy path in onboarding examples without over-explaining edge cases or adding defensive checks, as this reduces cognitive burden for new users learning the API.

Applied to files:

  • examples/diagnostics/__init__.py
🧬 Code graph analysis (11)
examples/diagnostics/client_with_tracing.py (1)
examples/run_with_tracing.py (1)
  • main (22-55)
src/fastmcp/resources/template.py (6)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/resource.py (1)
  • get_span_attributes (412-416)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (389-393)
src/fastmcp/utilities/components.py (1)
  • get_span_attributes (186-191)
src/fastmcp/client/telemetry.py (1)
src/fastmcp/telemetry.py (1)
  • get_tracer (38-47)
src/fastmcp/server/providers/proxy.py (5)
src/fastmcp/client/telemetry.py (1)
  • client_span (12-37)
src/fastmcp/exceptions.py (2)
  • ToolError (18-19)
  • ResourceError (14-15)
src/fastmcp/resources/resource.py (1)
  • get_span_attributes (412-416)
src/fastmcp/resources/template.py (1)
  • get_span_attributes (318-322)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/prompts/prompt.py (6)
src/fastmcp/resources/resource.py (1)
  • get_span_attributes (412-416)
src/fastmcp/resources/template.py (1)
  • get_span_attributes (318-322)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (389-393)
src/fastmcp/utilities/components.py (1)
  • get_span_attributes (186-191)
src/fastmcp/server/telemetry.py (3)
src/fastmcp/telemetry.py (2)
  • extract_trace_context (82-111)
  • get_tracer (38-47)
src/fastmcp/server/dependencies.py (2)
  • get_access_token (376-427)
  • get_context (283-290)
src/fastmcp/server/context.py (2)
  • client_id (431-437)
  • request_context (264-290)
src/fastmcp/tools/tool.py (6)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/resource.py (1)
  • get_span_attributes (412-416)
src/fastmcp/resources/template.py (1)
  • get_span_attributes (318-322)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/utilities/components.py (1)
  • get_span_attributes (186-191)
src/fastmcp/client/client.py (2)
src/fastmcp/client/telemetry.py (1)
  • client_span (12-37)
src/fastmcp/telemetry.py (1)
  • inject_trace_context (50-73)
src/fastmcp/resources/resource.py (6)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/template.py (1)
  • get_span_attributes (318-322)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (389-393)
src/fastmcp/utilities/components.py (1)
  • get_span_attributes (186-191)
src/fastmcp/server/providers/fastmcp_provider.py (7)
src/fastmcp/server/telemetry.py (1)
  • delegate_span (93-116)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/resource.py (1)
  • get_span_attributes (412-416)
src/fastmcp/resources/template.py (1)
  • get_span_attributes (318-322)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (389-393)
src/fastmcp/utilities/components.py (1)
  • get_span_attributes (186-191)
src/fastmcp/server/server.py (6)
src/fastmcp/server/telemetry.py (1)
  • server_span (55-89)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/resource.py (2)
  • get_span_attributes (412-416)
  • key (381-383)
src/fastmcp/resources/template.py (2)
  • get_span_attributes (318-322)
  • key (285-287)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (389-393)
src/fastmcp/utilities/components.py (2)
  • get_span_attributes (186-191)
  • key (94-102)
🪛 Ruff (0.14.11)
examples/diagnostics/client_with_tracing.py

96-96: Do not catch blind exception: Exception

(BLE001)


104-104: Do not catch blind exception: Exception

(BLE001)


111-111: Do not catch blind exception: Exception

(BLE001)


120-120: Do not catch blind exception: Exception

(BLE001)


132-132: Do not catch blind exception: Exception

(BLE001)


139-139: Do not catch blind exception: Exception

(BLE001)


145-145: Do not catch blind exception: Exception

(BLE001)


152-152: Do not catch blind exception: Exception

(BLE001)

src/fastmcp/server/providers/proxy.py

232-234: Avoid specifying long messages outside the exception class

(TRY003)


256-256: Avoid specifying long messages outside the exception class

(TRY003)

examples/run_with_tracing.py

54-54: Consider ["fastmcp", "run", *sys.argv[1:]] instead of concatenation

Replace with ["fastmcp", "run", *sys.argv[1:]]

(RUF005)

examples/diagnostics/server.py

30-30: subprocess call: check for execution of untrusted input

(S603)


31-40: Starting a process with a partial executable path

(S607)


51-51: Do not catch blind exception: Exception

(BLE001)


106-106: Avoid specifying long messages outside the exception class

(TRY003)


112-112: Avoid specifying long messages outside the exception class

(TRY003)


118-118: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/server.py

1494-1494: Avoid specifying long messages outside the exception class

(TRY003)


1495-1495: Avoid specifying long messages outside the exception class

(TRY003)


1588-1588: Avoid specifying long messages outside the exception class

(TRY003)


1589-1589: Avoid specifying long messages outside the exception class

(TRY003)


1595-1595: Avoid specifying long messages outside the exception class

(TRY003)


1609-1609: Avoid specifying long messages outside the exception class

(TRY003)


1610-1610: Avoid specifying long messages outside the exception class

(TRY003)


1699-1699: Avoid specifying long messages outside the exception class

(TRY003)


1700-1700: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (41)
src/fastmcp/client/transports.py (2)

125-128: LGTM!

Clean extension point for transports to expose session IDs. The default None return is appropriate for transports that don't support session identification.


311-317: LGTM!

Defensive exception handling is appropriate here since session ID retrieval is optional telemetry metadata - failures shouldn't propagate and break the transport.

src/fastmcp/utilities/components.py (1)

185-191: LGTM!

Well-designed extension point for telemetry. The pattern of calling super() and merging attributes enables consistent span metadata across the component hierarchy.

src/fastmcp/resources/resource.py (1)

412-416: LGTM!

Consistent implementation following the established telemetry pattern across components. Properly chains to super() and adds resource-specific span attributes.

src/fastmcp/resources/template.py (1)

318-322: LGTM!

Consistent telemetry implementation matching the pattern used by other components. The resource_template type clearly distinguishes templates from concrete resources in traces.

src/fastmcp/tools/tool.py (1)

389-393: LGTM! Consistent span attributes implementation.

The implementation correctly follows the established pattern across other components (Prompt, Resource, ResourceTemplate) by calling super().get_span_attributes() and merging component-specific attributes using the dict union operator.

docs/docs.json (1)

131-131: LGTM! Navigation entry correctly added.

The new telemetry documentation page is properly positioned alphabetically within the Features group.

src/fastmcp/prompts/prompt.py (1)

395-399: LGTM! Consistent implementation matching sibling components.

The get_span_attributes method follows the same pattern as Tool, Resource, and ResourceTemplate, ensuring uniform telemetry attributes across all component types. Based on learnings, this maintains consistency across all four MCP object types.

examples/run_with_tracing.py (1)

40-45: LGTM with note: insecure=True is appropriate for local development.

The OTLP exporter configuration with insecure=True is correct for local development scenarios demonstrated in this example. The script's docstring clearly indicates this is for localhost usage with otel-desktop-viewer.

docs/servers/telemetry.mdx (5)

1-6: LGTM! Proper frontmatter with required fields.

The YAML frontmatter correctly includes the required title and description fields per coding guidelines.


8-17: Clear and accurate introduction.

The explanation of OpenTelemetry API vs SDK usage and the "bring your own SDK" approach is well-articulated. The list of supported backends sets appropriate expectations.


56-75: Well-structured server spans documentation.

The tables clearly document span names and attributes with appropriate examples. The component types (tool, resource, resource_template, prompt) align with the get_span_attributes implementations across the codebase.


223-231: Add type annotation to error handling example.

Per coding guidelines, code examples should include proper type annotations.

Suggested fix
 `@mcp.tool`()
-def risky_operation() -> str:
+def risky_operation() -> str:  # Already has return annotation, but no param annotations needed here
     raise ValueError("Something went wrong")

Actually, this example is correct - no parameters means no parameter annotations needed, and the return type is present. No change required.


113-130: Clear span hierarchy visualization.

The ASCII diagrams effectively illustrate the parent-child relationship between spans for mounted servers and proxy providers. This helps users understand trace propagation across provider boundaries.

src/fastmcp/client/telemetry.py (1)

1-40: LGTM! Clean implementation of client-side telemetry helper.

The client_span context manager correctly:

  • Creates a CLIENT span with appropriate MCP attributes
  • Records exceptions on the span before re-raising
  • Conditionally includes session ID when available

The use of bare Exception in Line 34 is acceptable here since the purpose is telemetry recording, and the exception is always re-raised.

src/fastmcp/client/client.py (3)

882-917: LGTM! Proper trace context propagation for resource reads.

The instrumentation correctly:

  • Creates a CLIENT span with resource URI as the component key
  • Injects trace context into the meta dict for server-side propagation
  • Falls back to the simpler session.read_resource when no trace context is available

1103-1148: LGTM! Consistent instrumentation pattern for prompt operations.

The implementation follows the same well-structured pattern as read_resource_mcp, with proper span creation and trace context propagation.


1395-1419: LGTM! Tool call instrumentation is well implemented.

The implementation correctly wraps the tool call with a CLIENT span and propagates trace context.

Minor observation: Line 1416 could be simplified from meta=propagated_meta if propagated_meta else None to just meta=propagated_meta since inject_trace_context already returns None when there's nothing to inject. However, this is a very minor stylistic point and the current code is clear about intent.

src/fastmcp/server/server.py (3)

1476-1495: LGTM! Server-side tool call tracing correctly implemented.

The span correctly:

  • Encompasses the entire tool execution including lookup
  • Sets component-specific attributes from tool.get_span_attributes()
  • Preserves existing exception handling patterns (FastMCPError, ValidationError, generic Exception)

The server_span context manager will automatically record exceptions and set error status before re-raising.


1570-1610: LGTM! Resource read tracing handles both concrete and template paths.

The implementation correctly:

  • Wraps the entire read operation in a single span
  • Sets appropriate span attributes for whichever path succeeds (concrete resource or template)
  • Preserves the existing fallback logic from resources to templates
  • Re-raises NotFoundError with proper context when neither is found

1684-1700: LGTM! Prompt rendering tracing follows the established pattern.

The implementation is consistent with tool and resource tracing, correctly wrapping the prompt render operation with appropriate span attributes.

examples/diagnostics/client_with_tracing.py (2)

32-45: LGTM! Standard OpenTelemetry setup for examples.

The setup_tracing() function correctly configures OTLP export with sensible defaults for local development. The insecure=True parameter is appropriate for localhost development scenarios documented in the usage instructions.


91-153: Bare Exception catches are acceptable for this diagnostic example.

The static analysis flags except Exception at multiple locations. In this context, catching broad exceptions is intentional - the script is designed to exercise all server components and report which ones succeed/fail, continuing execution regardless of individual failures.

For production client code, more specific exception handling (e.g., ToolError, ResourceError, McpError) would be preferable, but for a diagnostic/example script demonstrating tracing behavior, the current approach is appropriate.

src/fastmcp/server/providers/fastmcp_provider.py (4)

115-140: LGTM! Tool delegation tracing correctly implemented.

The delegate_span wraps _run() appropriately, and get_span_attributes() provides useful debugging context (original name). The run() method correctly remains unwrapped since it calls _server.call_tool which creates its own span.


193-204: LGTM! Resource delegation tracing follows the established pattern.


265-290: LGTM! Prompt delegation tracing correctly mirrors the tool pattern.


372-421: LGTM! Resource template delegation tracing is complete and consistent.

The get_span_attributes() correctly includes original_uri_template to distinguish template-based reads from concrete resource reads.

src/fastmcp/server/telemetry.py (3)

12-26: LGTM - Auth span attributes extraction is well-structured.

The function correctly handles the RuntimeError that can occur when no context is available. The deferred import of get_access_token avoids circular dependencies.


54-89: LGTM - Server span context manager is well-implemented.

The span correctly sets RPC semantic conventions, records exceptions, and sets error status. Exception re-raising after recording is correct. Passing None to the context parameter when _get_parent_trace_context() returns None is valid as OpenTelemetry's start_as_current_span accepts None and uses the current context as fallback.


92-116: LGTM - Delegate span context manager follows the same pattern.

Consistent with server_span, correctly records exceptions and sets error status. The implicit INTERNAL span kind (default) is appropriate for internal delegation.

src/fastmcp/telemetry.py (5)

1-22: LGTM - Well-documented module with clear SDK configuration example.

The docstring clearly explains that telemetry is a no-op without an SDK, and provides a practical example for users to configure OpenTelemetry.


38-47: LGTM - Tracer accessor correctly delegates to OpenTelemetry API.

The function properly wraps otel_get_tracer with the instrumentation name, providing no-op behavior when no SDK is configured.


50-73: LGTM - Trace context injection handles edge cases well.

The function correctly returns None when there's no trace context to inject and the input meta was None, avoiding unnecessary empty dicts in requests.


76-79: LGTM - Utility for recording span errors.

Simple helper that encapsulates the common pattern of recording an exception and setting error status.


82-111: LGTM - Trace context extraction correctly preserves existing spans.

The check for an already-valid span context (e.g., from HTTP propagation) before extracting from meta prevents accidental context overwrites. This is the correct behavior for distributed tracing.

examples/diagnostics/server.py (1)

70-118: LGTM - Example components are well-structured for testing observability.

The components cover successful operations and intentional failures across tools, resources, templates, and prompts. The ValueError exceptions with descriptive messages are appropriate for this diagnostics/testing use case. The static analysis warnings about long exception messages (TRY003) can be safely ignored here since these are intentional test fixtures.

src/fastmcp/server/providers/proxy.py (5)

156-160: LGTM - Span attributes correctly expose proxy metadata.

The get_span_attributes method properly merges parent attributes with proxy-specific attributes including backend name.


223-258: LGTM - Resource read with proper span wrapping and content processing.

The span correctly wraps the remote resource read, and the content processing handles both text and blob resource contents appropriately. The base64 decoding for blob content is correct.


260-264: LGTM - Resource span attributes properly include backend URI.


375-379: LGTM - Template span attributes include backend URI template.


435-459: LGTM - Prompt rendering with span wrapping is well-implemented.

The span correctly captures the prompt operation, and the message conversion properly maps backend prompt messages to local Message objects. The get_span_attributes method follows the same pattern as other proxy components.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/fastmcp/server/telemetry.py
Comment thread src/fastmcp/server/telemetry.py Outdated
chrisguidry and others added 6 commits January 14, 2026 09:45
Adds opt-in distributed tracing via OpenTelemetry for observability into
FastMCP server and client operations.

Server spans are created for tool calls, resource reads, and prompt
renders with attributes like component key, component type, provider
type, session ID, and auth context. Client spans wrap outgoing calls
with trace context propagation via W3C headers in request meta.

Components provide their own span attributes through a `get_span_attributes()`
method that subclasses override - this lets LocalProvider, FastMCPProvider,
and ProxyProvider each include relevant context (original names, backend URIs).

To enable: configure an OpenTelemetry SDK with a TracerProvider before
importing fastmcp. Traces export to any OTLP-compatible backend.

Closes ENG-2813

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test was checking caplog.records length but OpenTelemetry emits
internal warning logs that were getting captured. Filter to only the
test's logger to avoid flaky failures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Lead with opentelemetry-instrument as the default approach
- Move programmatic configuration lower in the page
- Remove unimplemented metrics section
- Fix attribute values (resource_template not template)
- Add auth attributes (enduser.id, enduser.scope)
- Add provider-specific delegation attributes
- Link to OpenTelemetry Python docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Lightweight single-binary alternative to Jaeger for local development.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
A few improvements based on code review:

- Don't override existing trace context in `extract_trace_context` - if we're
  already in a valid trace (e.g., from HTTP propagation), preserve it rather
  than extracting from MCP meta
- Add exception recording to `delegate_span` to match `server_span` pattern
- Remove unused `get_meter` function (metrics not implemented yet)
- Return `None` instead of `{}` from `inject_trace_context` when nothing to inject
- Clean up trivial tests that were just testing OpenTelemetry's own API

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Be specific about which operations are traced (tools, prompts, resources, resource templates)
- Remove "(not the SDK)" parenthetical
- Consolidate attribute documentation - remove redundancy in Tracing section
- Delete unnecessary examples/diagnostics/__init__.py

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add return type annotation to main() in run_with_tracing.py
- Use spread operator for argv construction
- Add type annotations to docs test example
- Use async httpx client and asyncio.sleep in diagnostics server
- Improve subprocess termination handling with timeout fallback

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Type checking failure in - attempting to directly await a union type.

Root Cause: The test at attempts to directly , but the type checker correctly identifies that has type . When called, this returns , which is a union type that isn't directly awaitable - you need to check if it's awaitable first.

Suggested Solution: Update the test to follow the established pattern used throughout the codebase (see ProxyTool._get_client, ProxyResource._get_client, etc.). Replace the direct await with:

# At line 217 in tests/server/providers/proxy/test_proxy_server.py
client = proxy.client_factory()
if inspect.isawaitable(client):
    client = await client
assert isinstance(client, Client)

This matches the pattern used in 15+ other places in the codebase where ClientFactoryT is handled.

Detailed Analysis

Type Error Details:

error[invalid-await]: `Unknown | Client[Unknown] | Awaitable[Client[Unknown]]` is not awaitable
     --> tests/server/providers/proxy/test_proxy_server.py:217:20
  217 |     client = await proxy.client_factory()
      |                    ^^^^^^^^^^^^^^^^^^^^^^

Established Pattern in Codebase:
This pattern appears in multiple locations:

  • src/fastmcp/server/providers/proxy.py:75-80 (ProxyTool._get_client)
  • src/fastmcp/server/providers/proxy.py:170-174 (ProxyResource._get_client)
  • src/fastmcp/server/providers/proxy.py:256-260 (ProxyResourceTemplate._get_client)
  • src/fastmcp/server/providers/proxy.py:365-369 (ProxyPrompt._get_client)
  • src/fastmcp/server/providers/proxy.py:466-470 (ProxyProvider._get_client)

All of these follow the same pattern: call the factory, check inspect.isawaitable(), then conditionally await.

Related Files
  • tests/server/providers/proxy/test_proxy_server.py:217 - Test that needs fixing
  • src/fastmcp/server/providers/proxy.py:57 - ClientFactoryT type definition
  • src/fastmcp/server/providers/proxy.py:659 - Where FastMCPProxy stores client_factory

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Type checking failure in test_async_client_factory - attempting to directly await a ClientFactoryT union type.

Root Cause: The test at tests/server/providers/proxy/test_proxy_server.py:217 attempts to directly await proxy.client_factory(), but the type checker correctly identifies that client_factory has type ClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]]. When called, this returns Client | Awaitable[Client], which is a union type that isn't directly awaitable - you need to check if it's awaitable first.

Suggested Solution: Update the test to follow the established pattern used throughout the codebase (see ProxyTool._get_client, ProxyResource._get_client, etc.). Replace the direct await with:

# At line 217 in tests/server/providers/proxy/test_proxy_server.py
client = proxy.client_factory()
if inspect.isawaitable(client):
    client = await client
assert isinstance(client, Client)

This matches the pattern used in 15+ other places in the codebase where ClientFactoryT is handled.

Detailed Analysis

Type Error Details:

error[invalid-await]: `Unknown | Client[Unknown] | Awaitable[Client[Unknown]]` is not awaitable
     --> tests/server/providers/proxy/test_proxy_server.py:217:20
  217 |     client = await proxy.client_factory()
      |                    ^^^^^^^^^^^^^^^^^^^^^^

Established Pattern in Codebase:
This pattern appears in multiple locations:

  • src/fastmcp/server/providers/proxy.py:75-80 (ProxyTool._get_client)
  • src/fastmcp/server/providers/proxy.py:170-174 (ProxyResource._get_client)
  • src/fastmcp/server/providers/proxy.py:256-260 (ProxyResourceTemplate._get_client)
  • src/fastmcp/server/providers/proxy.py:365-369 (ProxyPrompt._get_client)
  • src/fastmcp/server/providers/proxy.py:466-470 (ProxyProvider._get_client)

All of these follow the same pattern: call the factory, check inspect.isawaitable(), then conditionally await.

Related Files
  • tests/server/providers/proxy/test_proxy_server.py:217 - Test that needs fixing
  • src/fastmcp/server/providers/proxy.py:57 - ClientFactoryT type definition
  • src/fastmcp/server/providers/proxy.py:659 - Where FastMCPProxy stores client_factory

- Fix potential None session_id in span attributes
- Add return type annotation to _get_parent_trace_context
- Fix type checker issue with ClientFactoryT await pattern

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f356c461f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1407 to +1411
# Inject trace context into meta for propagation to server
propagated_meta = inject_trace_context(meta)

result = await self._await_with_session_monitoring(
self.session.call_tool(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate trace context for task-based calls

Tracing is only injected on the standard request path in call_tool_mcp, but call_tool(..., task=True) routes to _call_tool_as_task which builds a CallToolRequest directly and never reaches this instrumentation. That means background task invocations won’t emit client spans or carry fastmcp.traceparent to the server, so SEP-1686 task traces become orphaned even when tracing is enabled. Consider wrapping the task helpers (_call_tool_as_task, _read_resource_as_task, _get_prompt_as_task) in client_span and passing inject_trace_context(...) into request meta.

Useful? React with 👍 / 👎.

chrisguidry and others added 2 commits January 14, 2026 10:01
Validates that session_id is captured on both client and server
spans when using HTTP transport, and that they share the same ID.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
examples/diagnostics/server.py (2)

54-55: Avoid catching bare Exception - use specific exception types.

Per coding guidelines, bare except or overly broad except Exception should be avoided. For HTTP connection retries, catching specific exceptions is clearer and prevents masking unexpected errors.

♻️ Suggested fix
             try:
                 await client.get(
                     f"http://localhost:{ECHO_SERVER_PORT}/sse", timeout=0.1
                 )
                 break
-            except Exception:
+            except (httpx.RequestError, httpx.TimeoutException):
                 await asyncio.sleep(0.1)

46-59: Consider checking subprocess health and raising on startup failure.

If the subprocess crashes during startup, the loop completes without error and the proxy mount proceeds anyway, leading to confusing failures later. Consider checking proc.poll() in the loop and raising an error if the server never becomes ready.

♻️ Suggested improvement
     # Wait for server to be ready (async to avoid blocking event loop)
+    server_ready = False
     async with httpx.AsyncClient() as client:
         for _ in range(50):
+            if proc.poll() is not None:
+                stderr = proc.stderr.read() if proc.stderr else b""
+                raise RuntimeError(f"Echo server exited unexpectedly: {stderr.decode()}")
             try:
                 await client.get(
                     f"http://localhost:{ECHO_SERVER_PORT}/sse", timeout=0.1
                 )
+                server_ready = True
                 break
-            except Exception:
+            except (httpx.RequestError, httpx.TimeoutException):
                 await asyncio.sleep(0.1)
+
+    if not server_ready:
+        proc.terminate()
+        raise RuntimeError("Echo server failed to start within timeout")
 
     # Mount proxy to the running echo server
examples/diagnostics/client_with_tracing.py (2)

32-45: Add return type annotation.

Per coding guidelines, Python ≥3.10 requires full type annotations. The setup_tracing function is missing its return type.

Proposed fix
-def setup_tracing():
+def setup_tracing() -> None:
     """Set up OpenTelemetry tracing with OTLP export."""

48-48: Add return type annotation.

The main function is missing its return type annotation.

Proposed fix
-async def main():
+async def main() -> None:
src/fastmcp/server/providers/proxy.py (1)

236-258: Consider extracting content processing into a shared helper.

The content processing logic (lines 237-256) is duplicated nearly verbatim in ProxyTemplate.create_resource() (lines 337-356). Extracting this into a helper function would reduce duplication and centralize future maintenance.

Example helper extraction
def _process_resource_contents(
    result: Sequence[TextResourceContents | BlobResourceContents],
    source_uri: str,
) -> list[ResourceContent]:
    """Convert MCP resource contents to ResourceContent objects."""
    contents: list[ResourceContent] = []
    for item in result:
        if isinstance(item, TextResourceContents):
            contents.append(
                ResourceContent(content=item.text, mime_type=item.mimeType, meta=item.meta)
            )
        elif isinstance(item, BlobResourceContents):
            contents.append(
                ResourceContent(content=base64.b64decode(item.blob), mime_type=item.mimeType, meta=item.meta)
            )
        else:
            raise ResourceError(f"Unsupported content type: {type(item)}")
    return contents
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f356c4 and b94b7e9.

⛔ Files ignored due to path filters (13)
  • loq.toml is excluded by none and included by none
  • pyproject.toml is excluded by none and included by none
  • tests/client/telemetry/__init__.py is excluded by none and included by none
  • tests/client/telemetry/test_client_tracing.py is excluded by none and included by none
  • tests/conftest.py is excluded by none and included by none
  • tests/server/providers/proxy/test_proxy_server.py is excluded by none and included by none
  • tests/server/telemetry/__init__.py is excluded by none and included by none
  • tests/server/telemetry/test_provider_tracing.py is excluded by none and included by none
  • tests/server/telemetry/test_server_tracing.py is excluded by none and included by none
  • tests/telemetry/__init__.py is excluded by none and included by none
  • tests/telemetry/test_module.py is excluded by none and included by none
  • tests/test_mcp_config.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (18)
  • docs/docs.json
  • docs/servers/telemetry.mdx
  • examples/diagnostics/client_with_tracing.py
  • examples/diagnostics/server.py
  • examples/run_with_tracing.py
  • src/fastmcp/client/client.py
  • src/fastmcp/client/telemetry.py
  • src/fastmcp/client/transports.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/resources/resource.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/telemetry.py
  • src/fastmcp/telemetry.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/utilities/components.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/fastmcp/client/telemetry.py
  • src/fastmcp/prompts/prompt.py
  • examples/run_with_tracing.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/client/transports.py
  • src/fastmcp/resources/resource.py
  • docs/servers/telemetry.mdx
  • src/fastmcp/client/client.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/telemetry.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • examples/diagnostics/client_with_tracing.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/telemetry.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/server/server.py
  • src/fastmcp/utilities/components.py
  • examples/diagnostics/server.py
🧬 Code graph analysis (4)
examples/diagnostics/client_with_tracing.py (1)
examples/run_with_tracing.py (1)
  • main (22-55)
src/fastmcp/server/providers/proxy.py (4)
src/fastmcp/client/telemetry.py (1)
  • client_span (12-37)
src/fastmcp/exceptions.py (2)
  • ToolError (18-19)
  • ResourceError (14-15)
src/fastmcp/resources/resource.py (3)
  • get_span_attributes (412-416)
  • ResourceContent (36-115)
  • ResourceResult (118-206)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/server/server.py (2)
src/fastmcp/server/telemetry.py (1)
  • server_span (56-90)
src/fastmcp/utilities/components.py (2)
  • get_span_attributes (186-191)
  • key (94-102)
src/fastmcp/utilities/components.py (6)
src/fastmcp/server/providers/proxy.py (4)
  • get_span_attributes (156-160)
  • get_span_attributes (260-264)
  • get_span_attributes (375-379)
  • get_span_attributes (455-459)
src/fastmcp/prompts/prompt.py (1)
  • get_span_attributes (395-399)
src/fastmcp/resources/resource.py (2)
  • get_span_attributes (412-416)
  • key (381-383)
src/fastmcp/resources/template.py (2)
  • get_span_attributes (318-322)
  • key (285-287)
src/fastmcp/server/providers/fastmcp_provider.py (4)
  • get_span_attributes (136-140)
  • get_span_attributes (200-204)
  • get_span_attributes (286-290)
  • get_span_attributes (417-421)
src/fastmcp/tools/tool.py (1)
  • get_span_attributes (397-401)
🪛 Ruff (0.14.11)
examples/diagnostics/client_with_tracing.py

96-96: Do not catch blind exception: Exception

(BLE001)


104-104: Do not catch blind exception: Exception

(BLE001)


111-111: Do not catch blind exception: Exception

(BLE001)


120-120: Do not catch blind exception: Exception

(BLE001)


132-132: Do not catch blind exception: Exception

(BLE001)


139-139: Do not catch blind exception: Exception

(BLE001)


145-145: Do not catch blind exception: Exception

(BLE001)


152-152: Do not catch blind exception: Exception

(BLE001)

src/fastmcp/server/providers/proxy.py

232-234: Avoid specifying long messages outside the exception class

(TRY003)


256-256: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/server.py

1493-1493: Avoid specifying long messages outside the exception class

(TRY003)


1494-1494: Avoid specifying long messages outside the exception class

(TRY003)


1587-1587: Avoid specifying long messages outside the exception class

(TRY003)


1588-1588: Avoid specifying long messages outside the exception class

(TRY003)


1594-1594: Avoid specifying long messages outside the exception class

(TRY003)


1608-1608: Avoid specifying long messages outside the exception class

(TRY003)


1609-1609: Avoid specifying long messages outside the exception class

(TRY003)


1698-1698: Avoid specifying long messages outside the exception class

(TRY003)


1699-1699: Avoid specifying long messages outside the exception class

(TRY003)

examples/diagnostics/server.py

30-30: subprocess call: check for execution of untrusted input

(S603)


31-40: Starting a process with a partial executable path

(S607)


54-54: Do not catch blind exception: Exception

(BLE001)


113-113: Avoid specifying long messages outside the exception class

(TRY003)


119-119: Avoid specifying long messages outside the exception class

(TRY003)


125-125: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
🔇 Additional comments (22)
docs/docs.json (1)

131-131: LGTM!

The navigation entry for the telemetry documentation is correctly placed in alphabetical order within the Features group and follows the existing naming convention.

examples/diagnostics/server.py (2)

77-98: LGTM!

The successful components are well-typed with clear docstrings. Good examples for demonstrating tracing of successful operations.


104-125: LGTM!

The error components are properly typed and serve their purpose of demonstrating error tracing. Using ValueError with inline messages is acceptable for this diagnostic/example context.

src/fastmcp/utilities/components.py (1)

186-191: LGTM!

The base implementation provides the correct extensibility hook for telemetry span attributes. The pattern of returning {"fastmcp.component.key": self.key} and encouraging subclasses to merge via super().get_span_attributes() | {...} is consistent across all subclass implementations (Tool, Prompt, Resource, ResourceTemplate, and provider wrappers).

examples/diagnostics/client_with_tracing.py (1)

91-121: Bare Exception catches are acceptable here.

Static analysis flags the except Exception blocks, but this is intentional for an example script demonstrating how errors appear in traces. The script explicitly handles expected failures to show tracing behavior across success and error scenarios. No changes needed.

src/fastmcp/server/providers/fastmcp_provider.py (5)

115-120: LGTM!

The delegate_span wrapping correctly instruments the delegation to the child server's call_tool. Using a synchronous context manager around an async call is appropriate since the span creation/cleanup is synchronous.


136-140: LGTM!

The span attributes correctly extend the base class with provider-specific context (FastMCPProvider type and the original tool name before any namespace transforms).


193-204: LGTM!

Resource delegation and span attributes follow the same consistent pattern as tools.


265-290: LGTM!

Prompt delegation and span attributes implementation is consistent with tools and resources.


372-375: LGTM!

Resource template delegation correctly uses the expanded original_uri for the span name while including the template pattern in span attributes for traceability.

Also applies to: 417-421

src/fastmcp/server/server.py (3)

1475-1494: LGTM!

The server_span integration correctly instruments tool calls with:

  • Span name including the tool name for easy identification
  • Standard MCP attributes (rpc.system, rpc.service, rpc.method)
  • Component-specific attributes via tool.get_span_attributes()
  • Automatic exception recording via the context manager

The existing exception handling is preserved and the span will properly record errors.


1569-1609: LGTM!

The resource read instrumentation correctly handles both concrete resources and template fallback within a single span. The span.set_attributes() call appropriately updates attributes based on whether a concrete resource or template is used, ensuring the final span reflects the actual component that served the request.


1683-1699: LGTM!

Prompt rendering instrumentation follows the same consistent pattern as tool calls, with proper span attributes and exception handling.

src/fastmcp/server/telemetry.py (5)

13-27: LGTM!

The function correctly extracts auth attributes with appropriate null checks and graceful error handling when no token is available.


30-41: LGTM!

Correctly addresses the past review comment - now checks both ctx.request_context is not None and ctx.session_id is not None before adding the session ID attribute.


44-52: LGTM!

The function correctly extracts parent trace context for distributed tracing propagation, with proper type annotation and error handling.


55-90: LGTM!

The server_span context manager provides comprehensive SERVER span instrumentation with:

  • Standard OpenTelemetry RPC semantic conventions (rpc.system, rpc.service, rpc.method)
  • FastMCP-specific attributes for component identification
  • Auth and session context enrichment
  • Automatic exception recording with proper status propagation

The bare Exception catch is intentional here - it ensures all exceptions are recorded on the span for observability before being re-raised.


93-117: LGTM!

The delegate_span provides appropriate INTERNAL span instrumentation for provider delegation, with consistent exception handling patterns.

src/fastmcp/server/providers/proxy.py (4)

156-160: LGTM!

The get_span_attributes() method follows the established pattern from other providers and correctly exposes the backend identifier for telemetry.


260-264: LGTM!

Consistent with the established span attribute pattern.


375-379: LGTM, but verify telemetry coverage for create_resource.

The get_span_attributes() implementation is correct. However, note that ProxyTemplate.create_resource() (lines 313-373) does not wrap its client.read_resource() call with client_span, unlike ProxyResource.read(), ProxyTool.run(), and ProxyPrompt.render(). If tracing template resource creation is desired, consider adding telemetry wrapping there as well.


435-459: LGTM!

The telemetry wrapping and message conversion are implemented correctly. The get_span_attributes() method follows the established pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +147 to +148
if result.isError:
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe cast assumes error content structure.

If the backend returns isError=True with an empty content list or non-TextContent item, this will raise IndexError or fail the cast. Consider defensive handling:

Suggested fix
             if result.isError:
-                raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
+                error_text = "Tool execution failed"
+                if result.content and hasattr(result.content[0], "text"):
+                    error_text = result.content[0].text
+                raise ToolError(error_text)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if result.isError:
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
if result.isError:
error_text: str = "Tool execution failed"
if result.content and hasattr(result.content[0], "text"):
error_text = result.content[0].text
raise ToolError(error_text)

@chrisguidry chrisguidry merged commit 24467ea into main Jan 14, 2026
13 checks passed
@chrisguidry chrisguidry deleted the otel-2813 branch January 14, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Related to the FastMCP client SDK or client-side functionality. documentation Updates to docs, examples, or guides. Primary change is documentation-related. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add native OTEL integration

1 participant