Skip to content

Add stateless server conformance test#222

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

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

Conversation

@codefromthecrypt
Copy link
Copy Markdown

@codefromthecrypt codefromthecrypt commented Apr 10, 2026

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>
@codefromthecrypt codefromthecrypt force-pushed the feat/stateless-server-conformance branch from 7c6377d to a58f460 Compare April 10, 2026 10: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