Skip to content

Propagate contextvars through anyio streams#1996

Draft
aabmass wants to merge 5 commits intomodelcontextprotocol:mainfrom
aabmass:propagate-contextvars
Draft

Propagate contextvars through anyio streams#1996
aabmass wants to merge 5 commits intomodelcontextprotocol:mainfrom
aabmass:propagate-contextvars

Conversation

@aabmass
Copy link

@aabmass aabmass commented Feb 4, 2026

Part of #421

This PR propagates contextvars.Context through internal anyio memory streams and restores it in long running async tasks, so that they run with the correct context.

  • For MCP clients, this captures the caller's active context and restores it before using the transport.
  • For MCP servers, it captures the active context from the transport and restores it before using the Server's handlers

Motivation and Context

The goal is to make contextvars "work" with the MCP SDK as users expect. The main motivation is OpenTelemetry #421, but there are other use cases e.g. stdlib decimal, starlette-context, etc.

How Has This Been Tested?

Added several integration tests which are failing at main. I also manually tested e2e with the some of the samples by using OpenTelemetry httpx instrumentation (I can share screenshots).

Tests failures at main

collected 4 items

tests/server/test_streamable_http_streams_closed.py::test_streamable_http_server_cleanup PASSED                                                        [ 25%]
tests/test_context_propagation.py::test_memory_transport_client_to_server PASSED                                                                       [ 50%]
tests/test_context_propagation.py::test_memory_transport_client_to_server ERROR                                                                        [ 50%] [ 50%]
tests/test_context_propagation.py:40 test_memory_transport_client_to_server - Failed: some snapshots in this test have incorrect values. If you ju…
tests/test_context_propagation.py::test_streamable_http_asgi_to_mcpserver PASSED                                                                       [ 75%]
tests/test_context_propagation.py::test_streamable_http_mcpclient_to_httpx PASSED                                                                      [100%]
tests/test_context_propagation.py::test_streamable_http_mcpclient_to_httpx ERROR                                                                       [100%] [100%]
tests/test_context_propagation.py:71 test_streamable_http_mcpclient_to_httpx - Failed: some snapshots in this test have incorrect values. If you j…

══════════════════════════════════════════════════════════════════════ inline-snapshot ═══════════════════════════════════════════════════════════════════════
─────────────────────────────────────────────────────────────────────── Fix snapshots ────────────────────────────────────────────────────────────────────────
╭──────────────────────────────────────────────────────────── tests/test_context_propagation.py ─────────────────────────────────────────────────────────────╮
│ @@ -45,7 +45,7 @@                                                                                                                                          │
│                                                                                                                                                            │
│              result = await client.call_tool(name="my_tool")                                                                                               │
│                                                                                                                                                            │
│              assert isinstance(result, types.CallToolResult)                                                                                               │
│ -            assert result.content == snapshot([types.TextContent(text="client_value")])                                                                   │
│ +            assert result.content == snapshot([types.TextContent(text="initial")])                                                                        │
│                                                                                                                                                            │
│                                                                                                                                                            │
│  @pytest.mark.anyio                                                                                                                                        │
│ @@ -91,8 +91,8 @@                                                                                                                                          │
│                                                                                                                                                            │
│      ):                                                                                                                                                    │
│          with set_test_contextvar("client_value_initialize"):                                                                                              │
│              await session.initialize()                                                                                                                    │
│ -            assert captured_context_var == snapshot("client_value_initialize")                                                                            │
│ +            assert captured_context_var == snapshot("initial")                                                                                            │
│                                                                                                                                                            │
│          with set_test_contextvar("client_value_call_tool"):                                                                                               │
│              await session.call_tool("my_tool")                                                                                                            │
│ -            assert captured_context_var == snapshot("client_value_call_tool")                                                                             │
│ +            assert captured_context_var == snapshot("initial")                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Do you want to fix these snapshots? [y/n] (n): n

Breaking Changes

No

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

N/A

@aabmass aabmass force-pushed the propagate-contextvars branch from c0ccbcd to b5cb58e Compare February 4, 2026 22:45
TODO:
- Update a recipe to show it working
- Consider adding an integration test of some kind
@aabmass aabmass force-pushed the propagate-contextvars branch from b5cb58e to 5f61a7b Compare February 4, 2026 23:01
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