Skip to content

feat: first-class MCP tool-call instrumentation#24

Merged
PabloPardoGarcia merged 2 commits into
mainfrom
pablo/gla2-27-mcp-instrumentation
Jul 6, 2026
Merged

feat: first-class MCP tool-call instrumentation#24
PabloPardoGarcia merged 2 commits into
mainfrom
pablo/gla2-27-mcp-instrumentation

Conversation

@PabloPardoGarcia

Copy link
Copy Markdown
Member

Summary

Last Phase 4 ticket: first-class spans for MCP tool calls — the differentiator piece. Generic SDKs handle MCP unevenly (OpenInference's MCP package only propagates context across the transport; it creates no spans), so this instrumentor is ours, not bundled.

Every ClientSession.call_tool() an agent makes becomes:

span name                = "execute_tool add"
openinference.span.kind  = "TOOL"
gen_ai.operation.name    = "execute_tool"
gen_ai.tool.name         = "add"
input.value              = '{"a": 2, "b": 3}'
output.value             = "5"            # structuredContent preferred, text fallback
status                   = OK | ERROR     # raised exceptions AND isError tool results

Design

  • A registry citizen, not a special case: MCPInstrumentor duck-types the instrumentor interface and registers under "mcp", so it inherits everything from the auto-instrumentation machinery unchanged — auto-enable on global init when mcp is installed, instruments=["mcp"] opt-in for scoped clients, re-binding after shutdown/re-init, never-steal, and the extras-smoke CI job (now 6 entries).
  • No new dependency: the module's top-level import mcp makes an env without the package look "not installed" to the registry — same semantics as a missing third-party instrumentor. A [mcp] extra exists so the registry's install hint works.
  • Privacy for free: arguments/results land in input.value/output.value, which the masking exporter already treats as content — mask and capture_content=False cover tool I/O with zero extra code (test included).

Tests

6 tests against a real FastMCP server over MCP's in-memory transport (no fakes, no network): span shape + attributes, nesting under the caller's current span, isError results marking ERROR status, capture_content=False stripping tool I/O while keeping gen_ai.tool.name, uninstrument restoring the original method, and registry membership. The all-extras smoke job validates the entry like any other.

117 tests pass; ruff/format/mypy clean.

Wrap mcp.ClientSession.call_tool so every MCP tool invocation an agent
makes becomes a TOOL-kind span: "execute_tool <name>" with
gen_ai.tool.name, arguments/result as input.value/output.value (covered
by the existing mask / capture_content controls), latency, and error
status — both raised exceptions and isError tool results. Generic SDKs
cover MCP unevenly (the OpenInference MCP package only propagates
context, it creates no spans), so this instrumentor is ours.

Registered in the instrumentation registry under "mcp": auto-enabled on
global init when the mcp package is installed, explicit opt-in via
instruments=["mcp"], re-binding on shutdown/re-init, and the extras
smoke test all apply unchanged. The top-level `import mcp` makes an
environment without the package look "not installed" to the registry.

Tests run against a real FastMCP server over the in-memory transport:
span shape, nesting under the current span, error results, content
stripping, and uninstrument restoring the original method.
@PabloPardoGarcia PabloPardoGarcia force-pushed the pablo/gla2-27-mcp-instrumentation branch from 7019b30 to 361bf63 Compare July 6, 2026 07:34
@PabloPardoGarcia PabloPardoGarcia merged commit 623004f into main Jul 6, 2026
7 checks passed
@PabloPardoGarcia PabloPardoGarcia deleted the pablo/gla2-27-mcp-instrumentation branch July 6, 2026 07:37
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.

1 participant