Skip to content

Conversation

elinacse
Copy link
Contributor

@elinacse elinacse commented Sep 15, 2025

  • I have added tests that cover my changes.
  • If adding a new instrumentation or changing an existing one, I've added screenshots from some observability platform showing the change.
  • PR name follows conventional commits format: feat(instrumentation): ... or fix(instrumentation): ....
  • (If applicable) I have updated the documentation accordingly.

Important

Adds span creation for JSONRpcResponse formats in MCP instrumentation, including wrappers for JSONRPCResponse initialization and MCP request handling.

  • Behavior:
    • Adds _jsonrpc_response_init_wrapper() in instrumentation.py to create spans for JSONRPCResponse initialization, setting attributes like MCP_RESPONSE_VALUE and MCP_REQUEST_ID.
    • Modifies send() in ContextSavingStreamWriter to create spans for MCP requests, setting attributes like MCP_METHOD_NAME and MCP_REQUEST_ARGUMENT.
  • Instrumentation:
    • Wraps JSONRPCResponse.__init__ in mcp.types with _jsonrpc_response_init_wrapper().
    • Updates _uninstrument() to unwrap JSONRPCResponse.__init__ in mcp.types.
  • Misc:
    • Removes logging of exceptions in _transport_wrapper() in instrumentation.py.

This description was created by Ellipsis for 97fe5df. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features
    • Propagates server/workflow name across MCP server and tool spans for clearer trace grouping.
    • Adds request IDs to MCP server spans for easier correlation.
    • Captures and serializes tool outputs more reliably (supports text/objects), with truncation to OTEL limits.
    • Mirrors output on tool spans when prompts are enabled.
  • Tests
    • Updated tests to reflect new result content structure and to validate workflow name propagation across server and tool spans.

Copy link

coderabbitai bot commented Sep 15, 2025

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

Introduces server name capture in FastMCP instrumentation, propagates it to server and tool spans, generates MCP_REQUEST_ID, and expands output logging/serialization on spans. Updates tests to reflect result.content structure and verify traceloop.workflow.name on server/tool spans.

Changes

Cohort / File(s) Summary
FastMCP instrumentation
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py
Wraps FastMCP.init to capture server name (_server_name) from args/kwargs; sets traceloop.workflow.name on mcp.server and tool spans; generates MCP_REQUEST_ID on parent span; broadens output logging with JSON serialization for multiple result shapes; mirrors output to tool span when prompts enabled; guards with dont_throw.
Tests
packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py, .../tests/test_fastmcp_attributes.py, .../tests/test_fastmcp_server_span.py
Adjusts expectations to use result.content[...] shape; adds assertions for traceloop.workflow.name on server and tool spans; extends attribute checks in comprehensive and error-handling tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor C as Client
  participant F as FastMCP Server
  participant I as MCP Instrumentation
  participant O as OTEL SDK
  participant T as Tool

  rect rgb(240,248,255)
  note over F,I: Initialization
  C->>F: new FastMCP(name=...)
  activate I
  I->>F: __init__ wrapper captures server name (_server_name)
  deactivate I
  end

  rect rgb(245,255,240)
  note over C,F: Request handling
  C->>F: Handle request
  I->>O: Start span "mcp.server"<br/>attrs: traceloop.workflow.name, MCP_REQUEST_ID
  F->>T: Execute tool
  I->>O: Start child span "<tool>.tool"<br/>attrs include traceloop.workflow.name
  T-->>F: Result
  I->>O: Serialize and record output on server span<br/>(and tool span if prompts enabled)
  O-->>I: Span end (status per existing logic)
  end

  alt Error
    I->>O: Record error on spans (existing handling)
  else Success
    I->>O: Ensure outputs attached when available
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • nirga
  • dinmukhamedm

Poem

A hop, a span, a gleaming trace—
I stamp a name in every place.
A carrot-tick, request ID,
Outputs logged for all to see.
From server burrow to tooly glen,
I bound through flows—and log again. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely describes the new feature of capturing spans for MCP responses in stdio mode and follows conventional commit styling, making the primary change clear to reviewers.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 035dd4a and f4eb666.

📒 Files selected for processing (3)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py (2 hunks)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py (2 hunks)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: nina-kollman
PR: traceloop/openllmetry#3388
File: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py:20-20
Timestamp: 2025-09-18T14:36:24.693Z
Learning: In packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py, the team is aware that the global self._server_name field is unsafe for multi-server or concurrent usage but has consciously decided not to support multiple FastMCP servers currently.
🔇 Additional comments (4)
packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py (2)

65-65: LGTM! Workflow name propagation validated correctly.

The assertion correctly verifies that the tool span includes the workflow name derived from the server name ("attribute-test-server.mcp").


170-171: LGTM! Error span workflow name validation is thorough.

The assertion correctly verifies that workflow name propagation works even for error cases, ensuring consistent span metadata across success and failure paths.

packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py (2)

26-27: LGTM! Result access pattern updated correctly.

The change properly reflects the new result structure (accessing via result.content[0]) and includes a defensive length check before accessing the content.


124-149: LGTM! Comprehensive workflow name validation.

The assertions thoroughly validate workflow name propagation across both server spans and server-side tool spans. The filtering logic appropriately distinguishes server-side from client-side spans, and the error messages provide clear diagnostic information.


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
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

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

Caution

Changes requested ❌

Reviewed everything up to 97fe5df in 2 minutes and 16 seconds. Click for details.
  • Reviewed 194 lines of code in 1 files
  • Skipped 0 files when reviewing.
  • Skipped posting 5 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:4
  • Draft comment:
    Removal of the logging import and logging in exception handlers reduces available debug info. Consider adding minimal debug-level logging to help troubleshoot instrumentation issues.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The removed logging was in exception handlers that are part of instrumentation code. The exceptions are caught and handled gracefully - the code continues working by yielding the original result. The logging wasn't providing critical information, just noting that an exception occurred. Since this is instrumentation code, silent failure is acceptable - we don't want instrumentation issues to impact the main application flow. The logging could help identify instrumentation configuration issues or bugs. Without any logging, problems might be harder to track down. The exceptions occur in non-critical instrumentation paths and the code handles them gracefully. Adding logging here would add noise without providing much value, since the exceptions don't indicate actual problems. The logging removal is fine - it removes unnecessary noise from exception handlers that gracefully handle non-critical instrumentation issues. The comment should be deleted.
2. packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:139
  • Draft comment:
    Swallowing exceptions in the _transport_wrapper without any logging may hinder diagnosing issues. Consider logging exceptions at a debug or trace level.
  • Reason this comment was not posted:
    Comment was not on a location in the diff, so it can't be submitted as a review comment.
3. packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:535
  • Draft comment:
    Changing ItemWithContext to a simple @DataClass (removing slots and frozen) might affect performance and immutability guarantees. Please document the rationale for this change.
  • Reason this comment was not posted:
    Comment did not seem useful. Confidence is useful = 0% <= threshold 50% The comment is asking the author to document the rationale for a change, which is not allowed according to the rules. It also implies a request for confirmation of intention, which is also not allowed. The comment does not provide a specific code suggestion or ask for a specific test to be written.
4. packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:119
  • Draft comment:
    Typo: The client unwrap call uses "streamablehttp_client", which appears inconsistent with the module name 'streamable_http'. Consider renaming this to "streamable_http_client".
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The comment suggests renaming "streamablehttp_client" to "streamable_http_client" to match the module name style. However, this appears to be a consistent naming pattern in the code. The function name is used correctly in both instrument and uninstrument calls. Making this change would be a purely stylistic change with no functional benefit, and could introduce bugs if not changed everywhere consistently. I could be wrong about this being intentional - maybe it really is an inconsistency that should be fixed. The module uses underscores while the function doesn't. Even if it is technically inconsistent, this kind of minor stylistic change has more risk than benefit. The code works as is, and changing function names can be risky. This isn't a clear enough issue to warrant a comment. Delete this comment. While there is a minor stylistic inconsistency, it's not worth the risk of changing working code just for naming style consistency.
5. packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:123
  • Draft comment:
    Potential inconsistency: The unwrap argument is "JSONRPCResponse.init" but the commit message refers to "JSONRpcResponse" (with a lowercase 'p'). Please confirm the correct casing for consistency.
  • Reason this comment was not posted:
    Comment looked like it was already resolved.

Workflow ID: wflow_3mL1DAJn47cGIvJx

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

with tracer.start_as_current_span("MCP_Tool_Response") as span:
# Serialize the result data
result_serialized = serialize(result_value)
span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, f"{result_serialized}")
Copy link
Contributor

Choose a reason for hiding this comment

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

In _jsonrpc_response_init_wrapper, wrapping the serialized result in an f-string is redundant, and using args[0] as a fallback for id_value assumes a specific init signature. Please document or validate the expected parameter order.

Suggested change
span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, f"{result_serialized}")
span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, result_serialized)

@elinacse
Copy link
Contributor Author

@nirga I have made changes to MCP instrumentation but I see you have an open PR for MCP . please review the changes .

CC: @gyliu513

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

276-304: Improve error handling in _execute_and_handle_result method.

The method accesses result.content[0].text without checking if the content list is non-empty or if the first item has a text attribute.

Add proper validation before accessing nested attributes:

     async def _execute_and_handle_result(self, span, method, args, kwargs, wrapped, clean_output=False):
         """Execute the wrapped function and handle the result"""
         try:
             result = await wrapped(*args, **kwargs)
             # Add output
             if clean_output:
                 clean_output_data = self._extract_clean_output(method, result)
                 if clean_output_data:
                     try:
                         span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_OUTPUT, json.dumps(clean_output_data))
                     except (TypeError, ValueError):
                         span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_OUTPUT, str(clean_output_data))
             else:
                 span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_OUTPUT, serialize(result))
             # Handle errors
             if hasattr(result, "isError") and result.isError:
-                if len(result.content) > 0:
-                    span.set_status(Status(StatusCode.ERROR, f"{result.content[0].text}"))
-                    error_type = get_error_type(result.content[0].text)
+                if hasattr(result, "content") and result.content and len(result.content) > 0:
+                    error_msg = ""
+                    if hasattr(result.content[0], "text"):
+                        error_msg = result.content[0].text
+                    elif hasattr(result.content[0], "__str__"):
+                        error_msg = str(result.content[0])
+                    
+                    if error_msg:
+                        span.set_status(Status(StatusCode.ERROR, error_msg))
+                        error_type = get_error_type(error_msg)
                     if error_type is not None:
                         span.set_attribute(ERROR_TYPE, error_type)
+                else:
+                    span.set_status(Status(StatusCode.ERROR, "Unknown error"))
             else:
                 span.set_status(Status(StatusCode.OK))
             return result
         except Exception as e:
             span.set_attribute(ERROR_TYPE, type(e).__name__)
             span.record_exception(e)
             span.set_status(Status(StatusCode.ERROR, str(e)))
             raise
🧹 Nitpick comments (5)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (5)

91-107: Consider improving the fallback mechanism for JSONRPCResponse wrapping.

The current implementation tries direct wrapping first and falls back to a post-import hook on exception. While this approach works, catching all exceptions might hide legitimate errors that should be surfaced.

Consider checking for the specific import/attribute error and only falling back in those cases:

-        # Try multiple response creation points
-        # Try direct wrapping instead of post-import hook
-        try:
-            wrap_function_wrapper(
-                "mcp.types",
-                "JSONRPCResponse.__init__",
-                self._jsonrpc_response_init_wrapper(tracer),
-            )
-        except Exception:
-            # Fallback to post-import hook
-            register_post_import_hook(
-                lambda _: wrap_function_wrapper(
-                    "mcp.types",
-                    "JSONRPCResponse.__init__",
-                    self._jsonrpc_response_init_wrapper(tracer),
-                ),
-                "mcp.types",
-            )
+        # Try wrapping JSONRPCResponse.__init__ with fallback to post-import hook
+        try:
+            wrap_function_wrapper(
+                "mcp.types",
+                "JSONRPCResponse.__init__",
+                self._jsonrpc_response_init_wrapper(tracer),
+            )
+        except (ImportError, AttributeError):
+            # Module not yet imported or class not yet available, use post-import hook
+            register_post_import_hook(
+                lambda _: wrap_function_wrapper(
+                    "mcp.types",
+                    "JSONRPCResponse.__init__",
+                    self._jsonrpc_response_init_wrapper(tracer),
+                ),
+                "mcp.types",
+            )

149-152: Avoid silently catching all exceptions in the transport wrapper.

Catching all exceptions with bare except Exception: can hide legitimate errors and make debugging difficult. The transport wrapper should either handle specific exceptions or at least log when unexpected exceptions occur.

Consider handling specific exceptions or adding logging:

                     try:
                         read_stream, write_stream, get_session_id_callback = result
                         yield InstrumentedStreamReader(
                             read_stream, tracer
                         ), InstrumentedStreamWriter(write_stream, tracer), get_session_id_callback
-                    except Exception:
+                    except (ValueError, TypeError, AttributeError):
+                        # Not a 3-tuple, yield as-is
                         yield result
-                except Exception:
+                except (ValueError, TypeError):
+                    # Not a 2-tuple, yield as-is
                     yield result

252-253: Remove empty except block or add logging.

The empty except block at Line 252-253 silently swallows exceptions, making debugging difficult.

Either remove the try-except if the code is expected to work, or add minimal logging:

                 elif hasattr(params, "__dict__") and "name" in params.__dict__:
                     entity_name = params.__dict__["name"]
                     span_name = f"{params.__dict__['name']}.tool"
-            except Exception:
-                pass
+            except AttributeError:
+                # Use default method name if params structure is unexpected
+                pass

345-346: Inconsistent empty except blocks in extraction methods.

Both _extract_clean_input and _extract_clean_output have empty except blocks that return empty dicts. This could hide actual errors.

Consider being more specific about which exceptions are expected:

-        except Exception:
+        except (AttributeError, TypeError, ValueError):
+            # Unable to extract clean input/output, return empty dict
             return {}

Also applies to: 391-392


554-584: Consider reducing span nesting complexity.

The ContextSavingStreamWriter.send method creates two nested spans (main MCP span and RequestStreamWriter span). This might create overly complex traces.

Consider whether both spans are necessary, or if the attributes could be combined into a single span:

     @dont_throw
     async def send(self, item: Any) -> Any:
         # Extract method name for main span
         method_name = "unknown"
         if hasattr(item, "request") and hasattr(item.request, "root") and hasattr(item.request.root, "method"):
             method_name = item.request.root.method
 
-        # Create main MCP span first
+        # Create MCP span with all relevant attributes
         main_span_name = f"{method_name}.mcp"
         with self._tracer.start_as_current_span(main_span_name) as main_span:
             main_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, method_name)
             main_span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.WORKFLOW.value)
-
-            with self._tracer.start_as_current_span("RequestStreamWriter") as span:
-                if hasattr(item, "request_id"):
-                    span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{item.request_id}")
-                if hasattr(item, "request"):
-                    if hasattr(item.request, "root"):
-                        if hasattr(item.request.root, "method"):
-                            span.set_attribute(
-                                SpanAttributes.MCP_METHOD_NAME,
-                                f"{item.request.root.method}",
-                            )
-                        if hasattr(item.request.root, "params"):
-                            span.set_attribute(
-                                SpanAttributes.MCP_REQUEST_ARGUMENT,
-                                f"{serialize(item.request.root.params)}",
-                            )
-
-                ctx = context.get_current()
-                item_with_context = ItemWithContext(item=item, ctx=ctx)
-                return await self.__wrapped__.send(item_with_context)
+            
+            if hasattr(item, "request_id"):
+                main_span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{item.request_id}")
+            if hasattr(item, "request") and hasattr(item.request, "root"):
+                if hasattr(item.request.root, "method"):
+                    main_span.set_attribute(
+                        SpanAttributes.MCP_METHOD_NAME,
+                        f"{item.request.root.method}",
+                    )
+                if hasattr(item.request.root, "params"):
+                    main_span.set_attribute(
+                        SpanAttributes.MCP_REQUEST_ARGUMENT,
+                        f"{serialize(item.request.root.params)}",
+                    )
+
+            ctx = context.get_current()
+            item_with_context = ItemWithContext(item=item, ctx=ctx)
+            return await self.__wrapped__.send(item_with_context)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8287b30 and 97fe5df.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
🧬 Code graph analysis (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py (1)
  • dont_throw (12-40)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

98-98: Do not catch blind exception: Exception

(BLE001)


149-149: Do not catch blind exception: Exception

(BLE001)


151-151: Do not catch blind exception: Exception

(BLE001)


179-179: Unused function argument: instance

(ARG001)

⏰ 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). (5)
  • GitHub Check: Test Packages (3.11)
  • GitHub Check: Test Packages (3.12)
  • GitHub Check: Test Packages (3.10)
  • GitHub Check: Build Packages (3.11)
  • GitHub Check: Lint
🔇 Additional comments (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

535-538: Confirm the removal of frozen and slots from ItemWithContext dataclass.

The dataclass decorator was changed from @dataclass(slots=True, frozen=True) to just @dataclass. This makes the ItemWithContext mutable and removes memory optimization.

Was this change intentional? The frozen and slots attributes provide immutability guarantees and memory efficiency. If mutability is required, please confirm this is necessary for the new implementation.


524-532: Fix issue with modifying params on non-JSONRPCRequest items.

The code modifies request.params without checking if it's a JSONRPCRequest first. This could cause issues if the request object doesn't support the expected operations.

Apply this fix to ensure params are only modified for JSONRPCRequest objects:

-        if not isinstance(request, JSONRPCRequest):
-            return await self.__wrapped__.send(item)
-        meta = None
-        if not request.params:
-            request.params = {}
-        meta = request.params.setdefault("_meta", {})
-
-        propagate.get_global_textmap().inject(meta)
-        return await self.__wrapped__.send(item)
+        if isinstance(request, JSONRPCRequest):
+            if not request.params:
+                request.params = {}
+            meta = request.params.setdefault("_meta", {})
+            propagate.get_global_textmap().inject(meta)
+        
+        return await self.__wrapped__.send(item)

Likely an incorrect or invalid review comment.

Copy link
Member

@nirga nirga left a comment

Choose a reason for hiding this comment

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

@elinacse looks like tests are failing

Copy link

@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: 0

♻️ Duplicate comments (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

143-146: Duplicate comment - Use more specific exception handling.

These blind exception catches should also be replaced with more specific exception types for better error diagnosis. The logging was removed from the transport wrapper making it harder to debug issues.


171-202: Duplicate comment - Remove unused parameter and improve argument extraction logic.

The instance parameter is unused and the argument extraction logic could be more robust as noted in the past review. Additionally, Line 182 has an unnecessary f-string wrapper around the serialized result.

🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

529-529: Consider using frozen=True for dataclass to maintain immutability.

The dataclass decorator was changed from @dataclass(slots=True, frozen=True) to @dataclass, removing the immutability constraint. For context objects that carry tracing information, immutability helps prevent accidental modification that could break trace propagation.

Apply this diff to restore immutability:

-@dataclass
+@dataclass(frozen=True)
 class ItemWithContext:
     item: Any
     ctx: context.Context
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 97fe5df and 0f76f4f.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
🧬 Code graph analysis (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py (1)
  • dont_throw (12-40)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • SpanAttributes (64-261)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

98-98: Do not catch blind exception: Exception

(BLE001)


143-143: Do not catch blind exception: Exception

(BLE001)


145-145: Do not catch blind exception: Exception

(BLE001)


173-173: Unused function argument: instance

(ARG001)

⏰ 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). (5)
  • GitHub Check: Test Packages (3.11)
  • GitHub Check: Test Packages (3.10)
  • GitHub Check: Build Packages (3.11)
  • GitHub Check: Test Packages (3.12)
  • GitHub Check: Lint
🔇 Additional comments (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

549-552: ItemWithContext wrapping verified — no compatibility issue

ItemWithContext is defined in the same module and the ResponseStreamWriter wraps items at packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py:549–552 while ContextAttachingStreamReader casts/unpacks them at .../instrumentation.py:566–572; ripgrep found no other usages — change is internally consistent.


518-526: Verify JSONRPCRequest.params is writable and dict-like before using setdefault

  • Scan: no JSONRPCRequest definition found in this repo (it's external), so .params mutability cannot be confirmed here.
  • Location: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (≈ lines 518–526).
  • Action: confirm mcp.types.JSONRPCRequest.params is a writable dict for supported mcp versions; otherwise make the code defensive (example approach):
    • params = getattr(request, "params", None)
    • if not isinstance(params, dict): try assigning request.params = dict(params) or {} inside try/except (handle AttributeError/TypeError)
    • then use meta = request.params.setdefault("_meta", {}) and inject; if assignment fails, skip propagation or use an alternative channel.

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

248-293: Remove @dont_throw from wrappers that invoke the wrapped callable — it swallows real exceptions

dont_throw is applied to enter/exit, stream reader/writer, and client wrappers that call wrapped(*args, **kwargs), which can absorb library exceptions; remove the decorator from those wrappers, ensure spans record exceptions, and re-raise so original error semantics are preserved.

Affected: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py — lines 248–293, 518–550, 564–587, 608–614

I can open a focused PR to remove these decorators and keep span/error recording — proceed?

♻️ Duplicate comments (3)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (3)

318-345: Avoid redundant f-strings around serialized JSON.

serialize(...) already returns a string. Wrapping with f"" is unnecessary and can double-quote simple strings.

-            span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}")
+            span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_INPUT, serialize(args[0]))

(Already addressed similarly in the JSONRPCResponse wrapper diff.)

Also applies to: 479-504


186-218: Critical: dont_throw here will swallow exceptions from JSONRPCResponse.init.

If the wrapped init fails, the decorator suppresses it, risking inconsistent state. Also remove redundant f-strings and unused parameter warning.

-    def _jsonrpc_response_init_wrapper(self, tracer):
-        @dont_throw
-        def traced_method(wrapped, instance, args, kwargs):
+    def _jsonrpc_response_init_wrapper(self, tracer):
+        def traced_method(wrapped, _, args, kwargs):
             result_value = kwargs.get("result", None)
             if result_value is None and len(args) > 1:
                 result_value = args[1]

             if result_value is not None and isinstance(result_value, dict) and "content" in result_value:
                 with tracer.start_as_current_span("MCP_Tool_Response") as span:
                     # Serialize the result data
                     result_serialized = serialize(result_value)
-                    span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, f"{result_serialized}")
+                    span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, result_serialized)

                     # Set span status
                     if result_value.get("isError", False):
                         span.set_status(Status(StatusCode.ERROR, "Tool execution error"))
                     else:
                         span.set_status(Status(StatusCode.OK))

                     # Add request ID if available
-                    id_value = kwargs.get("id", None)
-                    if id_value is None and len(args) > 0:
+                    id_value = kwargs.get("id")
+                    if id_value is None and args:
                         id_value = args[0]

                     if id_value is not None:
-                        span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{id_value}")
+                        span.set_attribute(SpanAttributes.MCP_REQUEST_ID, str(id_value))

             # Call the original method
             result = wrapped(*args, **kwargs)
             return result

186-218: Fix positional fallback for JSONRPCResponse.init

mcp >= 1.6.0 JSONRPCResponse signature is (jsonrpc, id, result); current code reads result from args[1] and id from args[0], which misbinds positional calls. Update to bind parameters (inspect.signature) or at minimum use the correct positional index.

File: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py Lines 186-218

+        import inspect
+        try:
+            bound = inspect.signature(wrapped).bind_partial(*args, **kwargs)
+            result_value = bound.arguments.get("result", result_value)
+            id_value = bound.arguments.get("id", None)
+        except Exception:
+            pass
🧹 Nitpick comments (7)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (7)

105-122: Narrow the blanket exception when wrapping JSONRPCResponse.init.

Catching all exceptions hides real errors and triggers BLE001. Only fall back on import/attribute resolution failures.

-        try:
+        try:
             wrap_function_wrapper(
                 "mcp.types",
                 "JSONRPCResponse.__init__",
                 self._jsonrpc_response_init_wrapper(tracer),
             )
-        except Exception:
+        except (ImportError, AttributeError):
             # Fallback to post-import hook
             register_post_import_hook(
                 lambda _: wrap_function_wrapper(
                     "mcp.types",
                     "JSONRPCResponse.__init__",
                     self._jsonrpc_response_init_wrapper(tracer),
                 ),
                 "mcp.types",
             )

146-161: Avoid blind catches in transport wrapper.

These catches may mask programming errors and trigger BLE001. Limit to tuple-unpack/type issues.

-                    except Exception:
+                    except (TypeError, AttributeError):
                         yield result
-                except Exception:
+                except (TypeError, AttributeError):
                     yield result

310-323: Set MCP attributes on tool spans per PR objective.

Populate method and arguments to aid correlation.

         with tracer.start_as_current_span(span_name) as span:
             # Set tool-specific attributes
             span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.TOOL.value)
             span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, entity_name)
+            span.set_attribute(SpanAttributes.MCP_METHOD_NAME, method)
+            try:
+                args_payload = None
+                if params:
+                    if hasattr(params, "arguments"):
+                        args_payload = params.arguments
+                    elif hasattr(params, "__dict__") and "arguments" in params.__dict__:
+                        args_payload = params.__dict__["arguments"]
+                if args_payload is not None:
+                    span.set_attribute(SpanAttributes.MCP_REQUEST_ARGUMENT, serialize(args_payload))
+            except Exception:
+                pass

325-330: Also set MCP attributes on non-tool method spans.

Aligns with stated behavior changes.

     async def _handle_mcp_method(self, tracer, method, args, kwargs, wrapped):
         """Handle non-tool MCP methods with simple serialization"""
         with tracer.start_as_current_span(f"{method}.mcp") as span:
-            span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_INPUT, f"{serialize(args[0])}")
+            span.set_attribute(SpanAttributes.MCP_METHOD_NAME, method)
+            span.set_attribute(SpanAttributes.MCP_REQUEST_ARGUMENT, serialize(args[0]))
+            span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_INPUT, serialize(args[0]))
             return await self._execute_and_handle_result(span, method, args, kwargs, wrapped, clean_output=False)

568-587: Safer typing checks, remove unused var, and avoid masking send errors.

  • Prefer isinstance over type(...) is ...
  • Drop unnecessary meta pre-init.
  • Remove dont_throw on this wrapper (see separate comment), or at minimum ensure it doesn’t swallow transport errors.
-    @dont_throw
     async def send(self, item: Any) -> Any:
         from mcp.types import JSONRPCMessage, JSONRPCRequest
@@
-        elif type(item) is JSONRPCMessage:
+        elif isinstance(item, JSONRPCMessage):
             request = cast(JSONRPCMessage, item).root
@@
-        if not isinstance(request, JSONRPCRequest):
+        if not isinstance(request, JSONRPCRequest):
             return await self.__wrapped__.send(item)
-        meta = None
-        if not request.params:
+        if not request.params:
             request.params = {}
-        meta = request.params.setdefault("_meta", {})
+        meta = request.params.setdefault("_meta", {})
 
         propagate.get_global_textmap().inject(meta)
         return await self.__wrapped__.send(item)

590-594: Restore immutability/slots on ItemWithContext.

Previous frozen/slots likely reduced churn and improved safety when passing context through streams.

-@dataclass
+@dataclass(slots=True, frozen=True)
 class ItemWithContext:
     item: Any
     ctx: context.Context

105-122: Tests missing for new JSONRPCResponse wrapping and context propagation.

Please add unit/integration tests:

  • Verifies span creation on JSONRPCResponse with content/isError and request id.
  • Ensures request _meta gets traceparent injected and is restored on the server.
  • Confirms exceptions from wrapped functions propagate (no swallowing).

I can draft pytest tests with minimal MCP stubs to validate these paths. Want me to scaffold them?

Also applies to: 186-218, 568-587, 608-614

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0f76f4f and 85ae1a2.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
🧬 Code graph analysis (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (3)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py (1)
  • dont_throw (12-40)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py (1)
  • traced_method (42-121)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • SpanAttributes (64-261)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

113-113: Do not catch blind exception: Exception

(BLE001)


158-158: Do not catch blind exception: Exception

(BLE001)


160-160: Do not catch blind exception: Exception

(BLE001)


188-188: Unused function argument: instance

(ARG001)

⏰ 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). (5)
  • GitHub Check: Test Packages (3.11)
  • GitHub Check: Test Packages (3.10)
  • GitHub Check: Test Packages (3.12)
  • GitHub Check: Build Packages (3.11)
  • GitHub Check: Lint
🔇 Additional comments (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

132-132: Good: unwrapping JSONRPCResponse.init during uninstrument.

This closes the loop for the new wrapper and keeps uninstrumentation idempotent.


219-247: Remove dont_throw and add MCP span attributes

dont_throw suppresses BaseSession.send_request exceptions — remove it from patch_mcp_client.traced_method and ensure spans set MCP_METHOD_NAME and MCP_REQUEST_ARGUMENT in _handle_tool_call and _handle_mcp_method.

File: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

-    def patch_mcp_client(self, tracer: Tracer):
-        @dont_throw
-        async def traced_method(wrapped, instance, args, kwargs):
+    def patch_mcp_client(self, tracer: Tracer):
+        async def traced_method(wrapped, instance, args, kwargs):
             meta = None
             method = None
             params = None
@@
-            if method == "tools/call":
-                return await self._handle_tool_call(tracer, method, params, args, kwargs, wrapped)
-            else:
-                return await self._handle_mcp_method(tracer, method, args, kwargs, wrapped)
+            if method == "tools/call":
+                return await self._handle_tool_call(tracer, method, params, args, kwargs, wrapped)
+            else:
+                return await self._handle_mcp_method(tracer, method, args, kwargs, wrapped)

Confirm no residual @dont_throw wrappers around calls to wrapped( by running and pasting output of:
rg -nP -C2 '(?s)@dont_throw\s*\n\sasync?\s+def\s+\w+(.):.?await?\s+wrapped\s('

Copy link

@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: 0

♻️ Duplicate comments (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

166-198: Tighten wrapper: remove unused param, drop redundant f-strings, prefer kwargs-first arg extraction.

Also aligns with prior feedback.

-    def _jsonrpc_response_init_wrapper(self, tracer):
-        @dont_throw
-        def traced_method(wrapped, instance, args, kwargs):
-            result_value = kwargs.get("result", None)
-            if result_value is None and len(args) > 1:
-                result_value = args[1]
+    def _jsonrpc_response_init_wrapper(self, tracer):
+        @dont_throw
+        def traced_method(wrapped, _, args, kwargs):
+            result_value = kwargs.get("result")
+            if result_value is None and len(args) > 1:
+                result_value = args[1]
@@
-                with tracer.start_as_current_span("MCP_Tool_Response") as span:
+                with tracer.start_as_current_span("MCP_Tool_Response") as span:
                     # Serialize the result data
                     result_serialized = serialize(result_value)
-                    span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, f"{result_serialized}")
+                    span.set_attribute(SpanAttributes.MCP_RESPONSE_VALUE, result_serialized)
@@
-                    id_value = kwargs.get("id", None)
-                    if id_value is None and len(args) > 0:
-                        id_value = args[0]
+                    id_value = kwargs.get("id")
+                    if id_value is None and args:
+                        id_value = args[0]
@@
-                    if id_value is not None:
-                        span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{id_value}")
+                    if id_value is not None:
+                        span.set_attribute(SpanAttributes.MCP_REQUEST_ID, str(id_value))

Please confirm the mcp.types.JSONRPCResponse.__init__ parameter order so the positional fallbacks are correct:

What are the parameters of mcp.types.JSONRPCResponse.__init__ in MCP v1.6+?

630-635: Don’t swallow transport errors; drop @dont_throw on ContextSavingStreamWriter.send.

Decorator hides underlying I/O failures.

-    @dont_throw
     async def send(self, item: Any) -> Any:
         # Removed RequestStreamWriter span creation - we don't need low-level protocol spans
         ctx = context.get_current()
         return await self.__wrapped__.send(ItemWithContext(item, ctx))

PR description says this method “creates spans for MCP requests,” but the current code explicitly removes span creation. Confirm intended behavior.

🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

111-126: Avoid blind except; catch specific errors when wrapping JSONRPCResponse.init.

Prevents swallowing unexpected issues and fixes Ruff BLE001.

-        try:
-            wrap_function_wrapper(
-                "mcp.types",
-                "JSONRPCResponse.__init__",
-                self._jsonrpc_response_init_wrapper(tracer),
-            )
-        except Exception:
-            # Fallback to post-import hook
-            register_post_import_hook(
-                lambda _: wrap_function_wrapper(
-                    "mcp.types",
-                    "JSONRPCResponse.__init__",
-                    self._jsonrpc_response_init_wrapper(tracer),
-                ),
-                "mcp.types",
-            )
+        try:
+            wrap_function_wrapper(
+                "mcp.types",
+                "JSONRPCResponse.__init__",
+                self._jsonrpc_response_init_wrapper(tracer),
+            )
+        except (ModuleNotFoundError, ImportError, AttributeError):
+            # Fallback to post-import hook when module not yet imported or attribute missing
+            register_post_import_hook(
+                lambda _: wrap_function_wrapper(
+                    "mcp.types",
+                    "JSONRPCResponse.__init__",
+                    self._jsonrpc_response_init_wrapper(tracer),
+                ),
+                "mcp.types",
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 85ae1a2 and b61d00f.

📒 Files selected for processing (1)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

117-117: Do not catch blind exception: Exception

(BLE001)


128-128: Unused method argument: kwargs

(ARG002)


168-168: Unused function argument: instance

(ARG001)

⏰ 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). (5)
  • GitHub Check: Test Packages (3.12)
  • GitHub Check: Test Packages (3.10)
  • GitHub Check: Lint
  • GitHub Check: Test Packages (3.11)
  • GitHub Check: Build Packages (3.11)
🔇 Additional comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

131-131: Unwrapping on uninstrument — LGTM.

@elinacse
Copy link
Contributor Author

elinacse commented Sep 17, 2025

@nirga @gyliu513 please review the PR . This addresses the issue #3364
I have tested the changes now getting response values for stdio mode .
Screenshot 2025-09-17 at 10 13 35 PM

@elinacse elinacse changed the title feat(mcp): Span Creation for JSONRpcResponse formats feat(mcp): MCP response Span Capture for Stdio Mode Sep 18, 2025
Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (3)

514-516: Do not suppress transport errors in InstrumentedStreamWriter.send.

@dont_throw will swallow failures from the underlying writer. Let exceptions propagate.

-    @dont_throw
     async def send(self, item: Any) -> Any:

198-220: Remove @dont_throw around Client.aenter wrapper; don’t swallow session setup failures.

If __aenter__ raises, dont_throw hides it, leaving a dangling span and inconsistent client state.

-        @dont_throw
         async def traced_method(wrapped, instance, args, kwargs):

224-242: Same for Client.aexit; let errors propagate.

Session teardown failures should not be suppressed by the decorator.

-        @dont_throw
         async def traced_method(wrapped, instance, args, kwargs):
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py (1)

65-176: Let tool-call exceptions propagate; remove outer @dont_throw.

The inner try/except already records and re-raises. The outer decorator suppresses errors, altering FastMCP behavior.

-        @dont_throw
         async def traced_method(wrapped, instance, args, kwargs):
♻️ Duplicate comments (1)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

558-589: Remove @dont_throw and drop nested RequestStreamWriter span (tests expect none).

Decorator suppresses transport failures. Also, tests assert zero RequestStreamWriter spans; keep only the main MCP span and set attributes there.

-    @dont_throw
     async def send(self, item: Any) -> Any:
@@
-        with self._tracer.start_as_current_span(main_span_name) as main_span:
-            main_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, method_name)
-            main_span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.WORKFLOW.value)
-
-            with self._tracer.start_as_current_span("RequestStreamWriter") as span:
-                if hasattr(item, "request_id"):
-                    span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{item.request_id}")
-                if hasattr(item, "request"):
-                    if hasattr(item.request, "root"):
-                        if hasattr(item.request.root, "method"):
-                            span.set_attribute(
-                                SpanAttributes.MCP_METHOD_NAME,
-                                f"{item.request.root.method}",
-                            )
-                        if hasattr(item.request.root, "params"):
-                            span.set_attribute(
-                                SpanAttributes.MCP_REQUEST_ARGUMENT,
-                                f"{serialize(item.request.root.params)}",
-                            )
-
-                ctx = context.get_current()
-                item_with_context = ItemWithContext(item=item, ctx=ctx)
-                return await self.__wrapped__.send(item_with_context)
+        with self._tracer.start_as_current_span(main_span_name) as main_span:
+            main_span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, method_name)
+            main_span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, TraceloopSpanKindValues.WORKFLOW.value)
+            if hasattr(item, "request_id"):
+                main_span.set_attribute(SpanAttributes.MCP_REQUEST_ID, f"{item.request_id}")
+            if hasattr(item, "request") and hasattr(item.request, "root"):
+                if hasattr(item.request.root, "method"):
+                    main_span.set_attribute(SpanAttributes.MCP_METHOD_NAME, f"{item.request.root.method}")
+                if hasattr(item.request.root, "params"):
+                    main_span.set_attribute(SpanAttributes.MCP_REQUEST_ARGUMENT, f"{serialize(item.request.root.params)}")
+            ctx = context.get_current()
+            item_with_context = ItemWithContext(item=item, ctx=ctx)
+            return await self.__wrapped__.send(item_with_context)
🧹 Nitpick comments (4)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (2)

139-142: Avoid broad except Exception; narrow the exception types.

Catching Exception twice hides unexpected errors and trips BLE001. Limit to specific destructuring errors (e.g., TypeError, ValueError) and let others surface.

-                    except Exception:
+                    except (TypeError, ValueError):
                         yield result
-                except Exception:
+                except (TypeError, ValueError):
                     yield result

520-529: Prefer isinstance over exact type checks for JSONRPCMessage.

type(item) is JSONRPCMessage can miss subclasses/wrapped types.

-        elif type(item) is JSONRPCMessage:
+        elif isinstance(item, JSONRPCMessage):
             request = cast(JSONRPCMessage, item).root
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py (2)

51-51: Minor: remove unused instance arg or prefix with _.

Silences ARG001 and clarifies intent.

-        def traced_method(wrapped, instance, args, kwargs):
+        def traced_method(wrapped, _, args, kwargs):

93-96: Prefer robust request IDs.

Time-based IDs can collide. Use UUIDv4.

-                import time
-                request_id = str(int(time.time() * 1000))  # milliseconds
+                import uuid
+                request_id = str(uuid.uuid4())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b61d00f and 035dd4a.

📒 Files selected for processing (5)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py (4 hunks)
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (4 hunks)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py (2 hunks)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py (2 hunks)
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules

Files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py
  • packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py
  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py
🧠 Learnings (1)
📚 Learning: 2025-09-18T14:36:24.693Z
Learnt from: nina-kollman
PR: traceloop/openllmetry#3388
File: packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py:20-20
Timestamp: 2025-09-18T14:36:24.693Z
Learning: In packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py, the team is aware that the global self._server_name field is unsafe for multi-server or concurrent usage but has consciously decided not to support multiple FastMCP servers currently.

Applied to files:

  • packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py
🧬 Code graph analysis (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py (2)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/utils.py (1)
  • dont_throw (12-40)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (2)
  • SpanAttributes (64-261)
  • TraceloopSpanKindValues (301-306)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (2)
  • SpanAttributes (64-261)
  • TraceloopSpanKindValues (301-306)
🪛 Ruff (0.13.3)
packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/fastmcp_instrumentation.py

51-51: Unused function argument: instance

(ARG001)

packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py

139-139: Do not catch blind exception: Exception

(BLE001)


141-141: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (5)
packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_server_span.py (1)

17-18: LGTM: result shape assertions updated correctly.

packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp.py (1)

26-27: LGTM: adapt to result.content shape.

packages/opentelemetry-instrumentation-mcp/tests/test_fastmcp_attributes.py (2)

65-66: LGTM: workflow name asserted on tool span.


170-172: LGTM: workflow name asserted on error tool span.

packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/instrumentation.py (1)

532-537: Handle non-dict request.params before mutation.

Blindly assigning {} then calling setdefault can fail if params isn’t a dict. For example:

-        if not request.params:
-            request.params = {}
-        meta = request.params.setdefault("_meta", {})
-
-        propagate.get_global_textmap().inject(meta)
+        if isinstance(getattr(request, "params", None), dict):
+            meta = request.params.setdefault("_meta", {})
+        elif hasattr(request, "params") and hasattr(request.params, "__dict__"):
+            meta = getattr(request.params, "_meta", None) or {}
+            setattr(request.params, "_meta", meta)
+        else:
+            request.params = {"_meta": {}}
+            meta = request.params["_meta"]
+        propagate.get_global_textmap().inject(meta)

Confirm the shape of JSONRPCRequest.params in the upstream mcp.types to ensure this covers all cases.

Comment on lines +48 to +61
def _fastmcp_init_wrapper(self):
"""Create wrapper for FastMCP initialization to capture server name."""
@dont_throw
def traced_method(wrapped, instance, args, kwargs):
# Call the original __init__ first
result = wrapped(*args, **kwargs)

if args and len(args) > 0:
self._server_name = f"{args[0]}.mcp"
elif 'name' in kwargs:
self._server_name = f"{kwargs['name']}.mcp"

return result
return traced_method
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t wrap FastMCP.init with dont_throw.

Swallowing exceptions from __init__ can instantiate a half-initialized server, breaking invariants. Remove the decorator and keep simple name capture.

-    def _fastmcp_init_wrapper(self):
+    def _fastmcp_init_wrapper(self):
         """Create wrapper for FastMCP initialization to capture server name."""
-        @dont_throw
         def traced_method(wrapped, instance, args, kwargs):
             # Call the original __init__ first
             result = wrapped(*args, **kwargs)
@@
             return result
         return traced_method
📝 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
def _fastmcp_init_wrapper(self):
"""Create wrapper for FastMCP initialization to capture server name."""
@dont_throw
def traced_method(wrapped, instance, args, kwargs):
# Call the original __init__ first
result = wrapped(*args, **kwargs)
if args and len(args) > 0:
self._server_name = f"{args[0]}.mcp"
elif 'name' in kwargs:
self._server_name = f"{kwargs['name']}.mcp"
return result
return traced_method
def _fastmcp_init_wrapper(self):
"""Create wrapper for FastMCP initialization to capture server name."""
def traced_method(wrapped, instance, args, kwargs):
# Call the original __init__ first
result = wrapped(*args, **kwargs)
if args and len(args) > 0:
self._server_name = f"{args[0]}.mcp"
elif 'name' in kwargs:
self._server_name = f"{kwargs['name']}.mcp"
return result
return traced_method
🧰 Tools
🪛 Ruff (0.13.3)

51-51: Unused function argument: instance

(ARG001)

Comment on lines 125 to 149
# Verify TRACELOOP_WORKFLOW_NAME is set correctly on server spans
mcp_server_spans = [span for span in spans if span.name == 'mcp.server']
assert len(mcp_server_spans) >= 1, (
f"Expected at least 1 mcp.server span, found {len(mcp_server_spans)}"
)

for server_span in mcp_server_spans:
workflow_name = server_span.attributes.get('traceloop.workflow.name')
assert workflow_name == 'test-server.mcp', (
f"Expected workflow name 'test-server.mcp', got '{workflow_name}'"
)

# Verify TRACELOOP_WORKFLOW_NAME is also set on tool spans
server_tool_spans = [span for span in spans if span.name == 'add_numbers.tool'
and span.attributes.get('traceloop.span.kind') == 'tool'
and 'traceloop.workflow.name' in span.attributes]
assert len(server_tool_spans) >= 1, (
f"Expected at least 1 server-side tool span with workflow name, found {len(server_tool_spans)}"
)

for tool_span in server_tool_spans:
workflow_name = tool_span.attributes.get('traceloop.workflow.name')
assert workflow_name == 'test-server.mcp', (
f"Expected workflow name 'test-server.mcp' on tool span, got '{workflow_name}'"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Tests assert no 'RequestStreamWriter' spans — instrumentation must align.

Current server writer still creates this span. Please remove it as suggested in instrumentation.py to satisfy these assertions.


🏁 Script executed:

#!/usr/bin/env bash
set -e
# Locate instrumentation.py in the mcp package
fd --type f --base-directory packages/opentelemetry-instrumentation-mcp -e py instrumentation.py
# Search for any references to RequestStreamWriter in the instrumentation code
rg -n "RequestStreamWriter" -C3 packages/opentelemetry-instrumentation-mcp

Length of output: 2436


Remove RequestStreamWriter span creation
Instrumentation still starts a span named “RequestStreamWriter” at opentelemetry/instrumentation/mcp/instrumentation.py:571–574; delete this block so tests asserting zero RequestStreamWriter spans will pass.

🤖 Prompt for AI Agents
In packages/opentelemetry-instrumentation-mcp/instrumentation.py around lines
571-574, there is code that starts a span named "RequestStreamWriter"; remove
that entire block so the instrumentation no longer creates a
"RequestStreamWriter" span (ensure no remaining calls create that exact span
name and adjust any surrounding try/finally or context manager logic so behavior
remains correct without the removed span).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants