Skip to content

feat(mcp): expose session_id on MCPServerStreamableHttp#2708

Merged
seratch merged 4 commits intoopenai:mainfrom
adityasingh2400:fix/expose-mcp-session-id-v2
Mar 19, 2026
Merged

feat(mcp): expose session_id on MCPServerStreamableHttp#2708
seratch merged 4 commits intoopenai:mainfrom
adityasingh2400:fix/expose-mcp-session-id-v2

Conversation

@adityasingh2400
Copy link
Copy Markdown
Contributor

Summary

Fixes #924.

MCPServerStreamableHttp uses streamablehttp_client which returns a 3-tuple (read, write, get_session_id). However, the connect() method in the base class _MCPServerWithClientSession discarded the third element:

# Before (base class connect())
read, write, *_ = transport  # get_session_id silently dropped ❌

This made it impossible to retrieve the server-assigned MCP session ID, which is required to resume a stateful session in stateless worker setups (e.g. serverless functions, load-balanced workers).

Changes

src/agents/mcp/server.py

  1. _MCPServerWithClientSession.__init__ — initializes _get_session_id: GetSessionIdCallback | None = None so the attribute is always present on the instance.

  2. _MCPServerWithClientSession.connect() — captures the callback instead of discarding it:

    # After
    read, write, *rest = transport
    # Capture the session-id callback when present (streamablehttp_client only).
    self._get_session_id = rest[0] if rest and callable(rest[0]) else None

    This is backward-compatible: SSE transport returns a 2-tuple, so rest will be empty and _get_session_id stays None.

  3. MCPServerStreamableHttp.session_id property — new public property that delegates to the callback:

    @property
    def session_id(self) -> str | None:
        if self._get_session_id is None:
            return None
        return self._get_session_id()

Usage example

# Worker 1: start a session and capture its ID
async with MCPServerStreamableHttp(params={"url": mcp_url}) as server:
    await runner.run(agent, "Do something")
    session_id = server.session_id  # e.g. "550e8400-e29b-41d4-a716-446655440000"
    # persist session_id to Redis / DB

# Worker 2 (new process / serverless invocation): resume the session
async with MCPServerStreamableHttp(
    params={"url": mcp_url, "headers": {"Mcp-Session-Id": session_id}}
) as server:
    await runner.run(agent, "Continue where we left off")

Tests

Added tests/mcp/test_streamable_http_session_id.py with 6 test cases:

Test What it checks
test_session_id_is_none_before_connect Property returns None on a fresh instance
test_session_id_returns_none_when_callback_is_none None callback → None session ID
test_session_id_returns_callback_value Callback is called and its return value is surfaced
test_session_id_returns_none_when_callback_returns_none Server-issued None passes through
test_session_id_reflects_updated_callback_value Property is dynamic (re-calls the callback)
test_connect_captures_get_session_id_callback connect() stores the callback from the transport tuple

All 120 existing MCP tests continue to pass.

Notes

  • The _isolated_client_session helper (used for per-call isolated sessions in MCPServerStreamableHttp) intentionally keeps *_ — those ephemeral sessions are not meant to be resumed.
  • No changes to __all__ or public API exports are needed; session_id is accessed directly on the instance.

@github-actions github-actions bot added enhancement New feature or request feature:mcp labels Mar 18, 2026
@seratch seratch marked this pull request as draft March 19, 2026 02:09
@seratch
Copy link
Copy Markdown
Member

seratch commented Mar 19, 2026

This looks safe to add. Can you resolve the conflicts?

@seratch seratch added this to the 0.14.x milestone Mar 19, 2026
@adityasingh2400 adityasingh2400 force-pushed the fix/expose-mcp-session-id-v2 branch from 2253918 to 8d3aa15 Compare March 19, 2026 03:44
@adityasingh2400 adityasingh2400 force-pushed the fix/expose-mcp-session-id-v2 branch from db03700 to a52f729 Compare March 19, 2026 04:06
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Thanks for the review @seratch! Conflicts are resolved — the branch is now rebased cleanly on top of the latest main. Ready when you are. 🙏

@seratch seratch marked this pull request as ready for review March 19, 2026 05:14
Copy link
Copy Markdown
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix the lint error?

Run make format-check
uv run ruff format --check
Would reformat: tests/mcp/test_streamable_http_session_id.py
1 file would be reformatted, 499 files already formatted
make: *** [Makefile:12: format-check] Error 1
Error: Process completed with exit code 2.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f173b5281d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Fixed the lint error!

@seratch seratch marked this pull request as draft March 19, 2026 07:16
Ubuntu and others added 2 commits March 19, 2026 16:11
Capture the get_session_id callback returned by streamablehttp_client
in the base connect() method and expose it as a session_id property
on MCPServerStreamableHttp.

Closes openai#924
@adityasingh2400 adityasingh2400 force-pushed the fix/expose-mcp-session-id-v2 branch from f173b52 to 17861ad Compare March 19, 2026 16:12
…ion ID

Per seratch review: after __aexit__ or a failed connect, session_id was
returning a stale ID because cleanup() only cleared self.session but left
self._get_session_id intact. Now reset both in the finally block so
session_id reliably returns None once disconnected.

Also adds a regression test: test_session_id_is_none_after_cleanup.
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Fixed! cleanup() now resets _get_session_id = None in the finally block alongside self.session = None, so session_id reliably returns None after disconnect. Added a regression test (test_session_id_is_none_after_cleanup) covering this case.

@seratch seratch marked this pull request as ready for review March 19, 2026 16:15
@seratch
Copy link
Copy Markdown
Member

seratch commented Mar 19, 2026

Can you fix this?

uv run ruff check
tests/mcp/test_streamable_http_session_id.py:98:101: E501 Line too long (104 > 100)
   |
96 | @pytest.mark.asyncio
97 | async def test_session_id_is_none_after_cleanup():
98 |     """session_id must return None after the server is disconnected (cleanup clears _get_session_id)."""
   |                                                                                                     ^^^^ E501
99 |     server = MCPServerStreamableHttp(params={"url": "http://localhost:8000/mcp"})
   |

Found 1 error.
make: *** [Makefile:16: lint] Error 1
Error: Process completed with exit code 2.

@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Fixed — shortened the docstring to stay within the 100-char limit.

@seratch seratch modified the milestones: 0.14.x, 0.12.x Mar 19, 2026
@seratch seratch merged commit 58374fe into openai:main Mar 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature:mcp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI SDK does not expose MCP session ID, making stateful streamable HTTP sessions impossible

2 participants