Skip to content

feat: propagate HTTP context for MCP requests and prefix all span names with mcp - #683

Merged
liustve merged 13 commits into
mainfrom
mcp-enhancements
Mar 19, 2026
Merged

liustve merged 13 commits into
mainfrom
mcp-enhancements

Conversation

@liustve

@liustve liustve commented Mar 19, 2026 •

Copy link
Copy Markdown
Contributor

Description of changes:

  • prefixes all MCP span names with mcp for easier identification in traces similar to http spans

  • unsuppress and fix HTTP span context propagation by wrapping StreamableHTTPTransport._handle_post_request to restore trace context from the JSON-RPC _meta field, so http spans are correctly parented under their MCP request span instead of the session span

example trace:
image

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@liustve
liustve requested a review from a team as a code owner March 19, 2026 05:59
wangzlei
wangzlei previously approved these changes Mar 19, 2026
wangzlei
wangzlei previously approved these changes Mar 19, 2026
@liustve
liustve merged commit 4c4acf1 into main Mar 19, 2026
17 checks passed
@liustve
liustve deleted the mcp-enhancements branch March 19, 2026 22:31
liustve added a commit that referenced this pull request Jul 22, 2026
… headers (#827)

## Summary

Even with `OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION=true` (the default
from #695), inject `traceparent`/`tracestate`/`baggage` into the
outgoing HTTP request headers so MCP servers that read trace context
only from HTTP headers can join the caller's trace.

Fixes #826.

## Background

The default from #695 correctly deduplicates the redundant HTTP CLIENT
span, but it does so via `suppress_http_instrumentation()`, which also
short-circuits the HTTP client instrumentation's own header injection —
the only mechanism that would otherwise set `traceparent` on the wire.
MCP servers that only participate in HTTP-header propagation (API
Gateways, service meshes, non-Python MCP servers, any HTTP endpoint
without ADOT MCP instrumentation) therefore receive no context and start
a fresh trace, silently breaking E2E propagation for a large class of
real-world topologies.

#826 has before/after trace evidence from a real AWS deployment showing
the client trace dead-ending at `mcp.session` while every server-side
hop lands on its own fresh trace ID.

## Design

Compatible with the [OTel MCP semantic
convention](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md):

> MCP server instrumentation SHOULD, by default, use context extracted
from MCP `params._meta` as a parent for MCP server span and SHOULD link
current ambient context, if it's present.

The spec designates `_meta` as the MCP parent channel and HTTP context
as an orthogonal transport signal (a span link on ADOT-instrumented
servers). Nothing in the spec forbids injecting into HTTP headers as
well; the existing ADOT server-side wrapper only extracts from `_meta`,
so double-parenting on ADOT-instrumented servers is not a risk. This
change adds HTTP-header propagation as a compatible fallback for
non-ADOT servers while leaving the existing `_meta` propagation and the
span-suppression semantics unchanged.

## Implementation

`ClientWrapper.wrap_http_client` now wraps the transport's
`httpx_client_factory` (or supplies its own pre-built `http_client` for
the newer `streamable_http_client(url, http_client=...)` API) so every
`httpx.AsyncClient` the MCP transport uses has a request event hook that
calls `self._propagators.inject(carrier=request.headers)`.

The hook takes the OTel context *at request-send time*, so per-tool-call
parents (which change during a shared MCP session) are honored
correctly. If httpx or MCP is not importable, or the wrapped function
signature doesn't match, the patcher no-ops silently.

The change is purely additive with respect to
`OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION`:

| `OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION` | HTTP CLIENT span |
`traceparent` HTTP header |
| --- | --- | --- |
| `true` (default) — before this PR | suppressed ✓ | **absent ✗** (bug)
|
| `true` (default) — after this PR | suppressed ✓ | **present ✓** |
| `false` — before this PR | present | present |
| `false` — after this PR | present | present |

Span suppression, ping suppression, active-parent respect,
tool/prompt/resource spans, and error handling across stdio+http+sse
transports are unchanged.

## Testing

**New tests** in `test_mcp_instrumentor.py`:

- `test_http_headers_carry_trace_context_regardless_of_suppression` —
exercises `OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION` in `{true, false,
default}` and asserts every outgoing MCP HTTP request carries a
well-formed W3C `traceparent` header (validates format: `00-<32hex
trace-id>-<16hex span-id>-<2hex flags>`).
- `test_http_headers_prebuilt_client_gets_hook` — verifies the hook is
installed on a user-supplied pre-built `http_client` (newer
`streamable_http_client(url, http_client=...)` API).

**Existing tests** (15 total including the two new ones):
```
$ python -m pytest test_mcp_instrumentor.py
======================== 15 passed, 1 warning in 20.89s ========================
```

**Lint**:
```
$ black --check aws-opentelemetry-distro/src/.../_wrappers.py .../test_mcp_instrumentor.py
All done! ✨ 🍰 ✨
2 files would be left unchanged.

$ flake8 aws-opentelemetry-distro/src/.../_wrappers.py .../test_mcp_instrumentor.py
(no output)

$ pylint --rcfile pyproject.toml aws-opentelemetry-distro/src/.../_wrappers.py
Your code has been rated at 9.01/10 (previous run: 9.01/10, +0.00)
```

## Related PRs

- #683 — first added HTTP context propagation for MCP requests (via
`_meta` + `_handle_post_request` restoration)
- #695 — introduced `OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION` defaulting
to `true` (this is the PR whose default silently broke non-ADOT-server
topologies)

---------

Co-authored-by: Steve Liu <liustve@amazon.com>
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.

2 participants