Repository navigation
Description
Which component is this bug for?
opentelemetry-instrumentation-mcp (MCP is not listed in the form's component dropdown).
📜 Description
With content capture enabled, a successful MCP tool result containing two TextContent blocks reaches the application intact, and the server-side tool span records both blocks. The client-side tool span only records content[0], silently omitting the second block from traceloop.entity.output.
This is an observability-data omission; the application's result is not lost or changed.
👟 Reproduction steps
On unmodified main 8d9c93f37b352131f5871dcb6ecf5e79c19aeb40:
import asyncio
import os
from fastmcp import Client, FastMCP
from mcp.types import TextContent
from opentelemetry.instrumentation.mcp import McpInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
os.environ["TRACELOOP_TRACE_CONTENT"] = "true"
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
instrumentor = McpInstrumentor()
instrumentor.instrument(tracer_provider=provider)
server = FastMCP("multi-content-probe")
@server.tool()
async def two_blocks() -> list[TextContent]:
return [
TextContent(type="text", text="alpha-block"),
TextContent(type="text", text="omega-block"),
]
async def main():
async with Client(server) as client:
result = await client.call_tool("two_blocks", {})
print([block.text for block in result.content])
for span in exporter.get_finished_spans():
if span.name == "two_blocks.tool":
print(span.attributes.get("traceloop.workflow.name"),
span.attributes.get("traceloop.entity.output"))
try:
asyncio.run(main())
finally:
instrumentor.uninstrument()
provider.shutdown()👍 Expected behavior
When content capture is enabled, the client span should retain the complete ordered tool output, including both text blocks. With capture disabled, neither output should be recorded. The exact representation can preserve compatibility with the existing single-block format.
👎 Actual Behavior with Screenshots
Actual exported values from the in-memory tracer (synthetic markers only):
Application result: ['alpha-block', 'omega-block']
Server tool span: [{"type":"text","content":"alpha-block"},
{"type":"text","content":"omega-block"}]
Client tool span: {"result":"alpha-block"}
Both spans have OK status. _extract_clean_output() selects result.content[0] in the tools/call branch. The server wrapper iterates all returned content instead.
🤖 Python Version
Python 3.12.6 / macOS; instrumentation 0.62.4 from the commit above, FastMCP 2.14.3, MCP 1.26.0, OpenTelemetry SDK 1.45.2 / instrumentation and semantic conventions 0.66b2. uv pip check passed in this isolated dependency-consistent reproduction environment.
📃 Provide any additional context for the Bug.
- The primary agent independently ran the real in-process MCP client/server. Socket connections were blocked before importing the libraries: 0 network attempts, no LLM calls, VCR recording, provider mocks, or external telemetry service.
- Uninstrumented control: both blocks reach the application. Content-capture-off control: the application still receives both blocks and both span outputs are absent, as expected.
- The checked-in-lock environment also reproduces the omission, and its existing package suite reports 49 passed. That lock has a semantic-conventions requirement mismatch, so the observation was independently rechecked in the compatible environment above; it is not being attributed to that mismatch. No repository source or lockfile changes are proposed in this report.
- Related fix(mcp): tolerate non-dict result shapes in InstrumentedStreamWriter.send (#4038) #4396, fix(mcp): handle dict/object result shapes in stream writer error parsing #4039 and fix(mcp): don't let a tracing failure in send_request return None #4672 concern error-result shapes or tracing-failure guards. Their exact-head
_extract_clean_output()implementations still match main's first-block-only selection; their work is not included here.
👀 Have you spent some time to check if this bug has been raised before?
- I checked and didn't find similar issue
Searched open/closed issues, PRs and the relevant exact-head source and comments for this multi-block output omission; related work is listed above. This does not establish whether unpublished work exists.
Are you willing to submit PR?
Yes, after the topic and representation are approved through the contribution process. No implementation or PR has been started.
AI disclosure: OpenAI Codex investigated, ran the reproduction and prepared this issue under the account holder's explicit instruction. No human review or live-provider certification is claimed.
Activity
bosprimigenious commentedon Nov 2, 2026
I'd like to work on this once the topic and output representation are approved, following the contribution guide.
Proposed scope: retain all returned
CallToolResult.contentblocks in their original order when content capture is enabled, preserve the existing single-block representation where possible, and keep the capture-off gate and application return value unchanged. Add real in-process client/server regressions for two blocks, plus single/empty/error results and capture-off controls. I can follow your preferred representation for multi-block output to avoid surprising existing trace consumers.Please confirm whether this is an appropriate contribution and which output shape you prefer. No implementation or PR will be started before approval.
This proposal and the reproduction were prepared by OpenAI Codex under the account holder's instruction; no human review is claimed.