Visitar URL original
🐛 Bug Report: MCP client tool span omits content blocks after the first · Issue #4682 · traceloop/openllmetry · GitHub
Skip to content

🐛 Bug Report: MCP client tool span omits content blocks after the first #4682

Open

Description

@bosprimigenious

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

  1. bosprimigenious commented on Nov 2, 2026

    @bosprimigenious
    Author

    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.content blocks 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions