Skip to content

Add stateless server conformance test#222

Open
codefromthecrypt wants to merge 1 commit into
modelcontextprotocol:mainfrom
codefromthecrypt:feat/stateless-server-conformance
Open

Add stateless server conformance test#222
codefromthecrypt wants to merge 1 commit into
modelcontextprotocol:mainfrom
codefromthecrypt:feat/stateless-server-conformance

Conversation

@codefromthecrypt

@codefromthecrypt codefromthecrypt commented Apr 10, 2026

Copy link
Copy Markdown

Motivation and Context

The spec allows servers to omit Mcp-Session-Id entirely:

"A server using the Streamable HTTP transport MAY assign a session ID at initialization time"
Spec: Session Management

The word is MAY (RFC 2119). A compliant server can omit the header. This PR adds conformance coverage for that path, testing both directions.

Client scenario: stateless_server

Check Description
stateless-init-no-session Server responds to initialize with no Mcp-Session-Id. Client MUST NOT error.
stateless-no-session-header-sent Client's subsequent requests MUST NOT include Mcp-Session-Id (nothing to echo).
stateless-get-405 Server returns 405 for GET (no session, no SSE stream). SKIPPED if client never attempts GET.
stateless-delete-405 Server returns 405 for DELETE (no session to terminate). SKIPPED if client never attempts DELETE.
stateless-tools-call Tool call completes without session state.

Server scenario: stateless-server

Check Description
stateless-server-no-session-header Initialize response MUST NOT contain Mcp-Session-Id.
stateless-server-post-without-session Server accepts tools/list without Mcp-Session-Id in request.
stateless-server-get-405 Server returns 405 for GET.
stateless-server-delete-405 Server returns 405 for DELETE.
stateless-server-tools-call Tool call completes without session state.

How Has This Been Tested?

Client scenario - Python SDK (latest main)

SDK clients need a stateless_server handler (identical to tools_call).
One-line patch for Python SDK client.py:

 @register("tools_call")
+@register("stateless_server")
 async def run_tools_call(server_url: str) -> None:
$ npm start -- client \
    --command "cd ~/oss/python-sdk && uv run --frozen python .github/actions/conformance/client.py" \
    --scenario stateless_server

Checks:
  [stateless-no-session-header-sent] SUCCESS Client omits mcp-session-id when server did not provide one
  [stateless-tools-call            ] SUCCESS Validates that the client can call a tool on a stateless server
  [stateless-init-no-session       ] SUCCESS Server response contains no mcp-session-id header (stateless)
  [stateless-get-405               ] SKIPPED Stateless server returns 405 for GET (client did not attempt GET)
  [stateless-delete-405            ] SKIPPED Stateless server returns 405 for DELETE (client did not attempt DELETE)

Test Results:
Passed: 3/3, 0 failed, 0 warnings
OVERALL: PASSED

Server scenario - TypeScript SDK stateless example

# Terminal 1: start TS SDK stateless server
$ cd ~/oss/typescript-sdk
$ npx tsx src/examples/server/simpleStatelessStreamableHttp.ts
MCP Stateless Streamable HTTP Server listening on port 3000

# Terminal 2: run conformance test
$ npm start -- server --url http://localhost:3000/mcp --scenario stateless-server

Checks:
  [stateless-server-no-session-header   ] SUCCESS Stateless server omits Mcp-Session-Id from initialize response
  [stateless-server-post-without-session] SUCCESS Server accepts requests without Mcp-Session-Id header
  [stateless-server-get-405             ] SUCCESS Stateless server returns 405 for GET requests
  [stateless-server-delete-405          ] SUCCESS Stateless server returns 405 for DELETE requests
  [stateless-server-tools-call          ] SUCCESS Tool call completes successfully on a stateless server

Test Results:
Passed: 5/5, 0 failed, 0 warnings

Breaking Changes

None.

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

Design decisions:

  • FAILURE not WARNING: the server's choice to be stateless is MAY, but once it omits sessions, clients MUST handle it correctly.
  • GET/DELETE 405 checks use SKIPPED when client never attempts them, since clients are not required to issue GET or DELETE.

SDK prior art: Every major SDK already handles stateless servers correctly.

SDK Version init-no-session get-405 no-session-header-sent
Go v0.47.1 (2026-04-08) SendRequest createGETConnection sendHTTP
TypeScript 1.10.0 (2025-04-17) send _startOrAuthSse _commonHeaders
Python v1.8.0 (2025-05-08) _maybe_extract_session_id terminate_session _update_headers_with_session
Java v0.18.0 (2026-02-18) sendMessage reconnect reconnect
Kotlin 0.7.0 (2025-09-11) start start applyCommonHeaders
C# v0.2.0-preview.3 (2025-06-03) SendHttpRequestAsync ReceiveUnsolicitedMessages CopyAdditionalHeaders
Test both directions of the stateless (no session ID) transport path:
- Client scenario (stateless_server): mock stateless server verifies
  clients handle missing Mcp-Session-Id correctly
- Server scenario (stateless-server): test client verifies stateless
  servers omit session headers and return 405 for GET/DELETE

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant