Skip to content

Add stateless doc, prefer stateless mode and disable legacy SSE by default#1468

Merged
halter73 merged 43 commits intomainfrom
halter73/stateless-docs
Mar 27, 2026
Merged

Add stateless doc, prefer stateless mode and disable legacy SSE by default#1468
halter73 merged 43 commits intomainfrom
halter73/stateless-docs

Conversation

@halter73
Copy link
Copy Markdown
Contributor

Recommend stateless mode as the default for HTTP-based MCP servers across documentation, samples, and error messages.

Docs:

  • Add comprehensive sessions conceptual doc covering stateless (recommended), stateful, and stdio session behaviors
  • Update getting-started, transports, filters, and other conceptual docs to use stateless mode in examples
  • Add Sampling to docs table of contents
  • Clarify ConfigureSessionOptions runs per-request in stateless mode

Samples:

  • Convert ProtectedMcpServer to stateless mode
  • Add comments to AspNetCoreMcpServer and EverythingServer explaining why they require sessions

Error messages:

  • Improve missing Mcp-Session-Id errors to suggest stateless mode and link to session documentation

Tests:

  • Add tests for progress notifications and ConfigureSessionOptions in stateless mode
  • Verify error messages reference stateless mode guidance

…r messages

Recommend stateless mode as the default for HTTP-based MCP servers
across documentation, samples, and error messages.

Docs:
- Add comprehensive sessions conceptual doc covering stateless
  (recommended), stateful, and stdio session behaviors
- Update getting-started, transports, filters, and other conceptual
  docs to use stateless mode in examples
- Add Sampling to docs table of contents
- Clarify ConfigureSessionOptions runs per-request in stateless mode

Samples:
- Convert ProtectedMcpServer to stateless mode
- Add comments to AspNetCoreMcpServer and EverythingServer explaining
  why they require sessions

Error messages:
- Improve missing Mcp-Session-Id errors to suggest stateless mode and
  link to session documentation

Tests:
- Add tests for progress notifications and ConfigureSessionOptions in
  stateless mode
- Verify error messages reference stateless mode guidance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the SDK’s guidance around HTTP server sessions by adding a dedicated “Sessions” conceptual doc, shifting most docs/samples to recommend stateless mode by default, and improving HTTP error messages to point users at stateless mode when Mcp-Session-Id is missing.

Changes:

  • Added a comprehensive Sessions conceptual doc (stateless vs. stateful vs. stdio) and updated docs TOC.
  • Updated multiple docs and samples to prefer HttpServerTransportOptions.Stateless = true by default, with notes on when stateful sessions are required.
  • Improved Streamable HTTP missing-session error messages and added/updated tests to validate the guidance appears.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs Adds assertions that missing-session 400s include stateless guidance.
tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs Adds stateless-mode tests for progress notifications and ConfigureSessionOptions behavior.
src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Enhances 400 error messages to recommend stateless mode and link to sessions docs.
src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs Clarifies ConfigureSessionOptions invocation semantics in stateless mode.
samples/ProtectedMcpServer/Program.cs Switches sample to stateless mode with explanatory comments.
samples/EverythingServer/Program.cs Adds explanation why the sample must remain stateful.
samples/AspNetCoreMcpServer/Program.cs Adds explanation why the sample must remain stateful.
docs/concepts/transports/transports.md Updates examples to stateless mode and adds stateless guidance in narrative/table.
docs/concepts/toc.yml Adds Sessions and Sampling entries to conceptual TOC.
docs/concepts/sessions/sessions.md New comprehensive sessions documentation page.
docs/concepts/progress/samples/server/Program.cs Updates example to stateless mode.
docs/concepts/logging/logging.md Links stateless behavior to sessions doc.
docs/concepts/index.md Adds Sessions to concepts index table.
docs/concepts/httpcontext/samples/Program.cs Updates example to stateless mode.
docs/concepts/getting-started.md Updates getting-started HTTP server snippet to stateless mode.
docs/concepts/filters.md Updates auth/filters example to stateless mode.
docs/concepts/elicitation/elicitation.md Links stateless limitation note to sessions doc.

halter73 and others added 2 commits March 25, 2026 18:16
- Fix relative links to sessions doc from subdirectories
- Fix doc URLs in error messages to use .html extension
- Strengthen ConfigureSessionOptions test with two requests proving
  per-request behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TokenProgress.Report() uses fire-and-forget (no await), so in
stateless mode the SSE stream can close before notifications flush.
Rewrite the test using TCS coordination: the tool reports progress
then waits, giving the notification time to flush before the stream
closes. A SynchronousProgress<T> helper avoids the thread pool
posting race inherent to Progress<T>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
stephentoub
stephentoub previously approved these changes Mar 26, 2026
Add quick stateless-vs-stateful decision guide and explain why
stateless is recommended but not the default. Document the lack of
handler backpressure as a deployment footgun for stateful mode.
Normalize cross-doc links to use xref instead of relative paths.

Also document stale HttpContext risk with SSE transport.
halter73 and others added 17 commits March 26, 2026 10:08
Every WithHttpTransport() call in samples and docs now explicitly sets
Stateless = true or Stateless = false. This prepares for a potential
future default change and makes the intent clear in code users may copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 'Service lifetimes and DI scopes' section to sessions.md covering
how ScopeRequests controls per-handler scoping in stateful HTTP, how
stateless HTTP reuses ASP.NET Core's request scope, and how stdio
defaults to per-handler scoping but is configurable. Includes summary
table and cross-link from the stdio section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Group sections by purpose: mode selection (stateless/stateful/comparison),
transport details (HTTP lifecycle, deployment considerations, stdio),
server configuration (options, ConfigureSessionOptions, DI scopes),
security (user binding), and advanced features (migration, resumability).

Move comparison table near the decision tree. Move deployment footguns
under HTTP transport. Move stateless trade-offs into the stateless
section. Combine 'When to use stateful' and 'When stateful shines'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Any active HTTP request (POST or GET) prevents a session from being
counted as idle, not just GET/SSE. Fix docs and API comment on
MaxIdleSessionCount. Also remove redundant 'async' from 'async scope'
in DI documentation since nearly all ASP.NET Core scopes are async.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split Streamable HTTP into stateless and stateful columns, fix SSE
server example that incorrectly showed Stateless = true (SSE endpoints
are not mapped in stateless mode), and add cross-reference to sessions
doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73 halter73 force-pushed the halter73/stateless-docs branch from 0454970 to b7e68dc Compare March 27, 2026 08:40
halter73 and others added 2 commits March 27, 2026 01:49
…vior

Rewrite sessions.md intro to lead with the Stateless property recommendation,
clarify that sessions enabled is the current C# SDK default (not a protocol
requirement), and note the spec requires clients use sessions when servers
request them. Replace middleware example with minimal API endpoint filter.
Fix AllowNewSessionForNonInitializeRequests docs to call out spec non-compliance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restructure document flow: move client-side behavior up, fold security
into server configuration, move legacy SSE to its own section near the
end. Replace middleware example with minimal API endpoint filter using
Activity.AddTag for the transport session ID. Migrate SSE anchors
across transports.md, list-of-diagnostics.md, and filters.md.

Fix endpoint filter test to avoid strict request count assertion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub
Copy link
Copy Markdown
Contributor

Any way we could add an analyzer to help mitigate the breaks? eg detect that Stateless = false isn't being used anywhere and then warn on any use of the problematic methods?

@halter73 halter73 changed the title Add sessions doc and prefer stateless mode in docs, samples, and error messages Add sessions doc, prefer stateless mode and disable legacy SSE by default Mar 27, 2026
@halter73
Copy link
Copy Markdown
Contributor Author

Regarding the analyzer, I just opened #1471 to look into that. I currently have that as a PR based on this one (halter73/stateless-docs), because I don't want the complexities around that to hold up the doc and SSE default change. I think the analyzer can be done as a quick follow in a pretty nonbreaking way in a follow up patch. At least, certainly compared to just going ahead and making the break immediately!

…transports.md

Add 'Forward and backward compatibility' section to sessions.md
explaining why servers should set Stateless explicitly. Add 'How
Streamable HTTP delivers messages' section defining solicited (POST
response streams) vs unsolicited (GET stream) message delivery. Enhance
transports.md with message flow overview, SSE backpressure explanation,
and backpressure row in the transport comparison table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73
Copy link
Copy Markdown
Contributor Author

I added an additional section on forward and backward compatibility:

Forward and backward compatibility

The Stateless property is the single most important setting for forward-proofing your MCP server. The current C# SDK default is Stateless = false (sessions enabled), but we expect this default to change once mechanisms like MRTR bring server-to-client interactions (sampling, elicitation, roots) to stateless mode. We recommend every server set Stateless explicitly rather than relying on the default:

  • Stateless = true — the best forward-compatible choice. Your server opts out of sessions entirely. No matter how the SDK default changes in the future, your behavior stays the same. If you don't need unsolicited notifications, server-to-client requests, or session-scoped state, this is the setting to use today.

  • Stateless = false — the right choice when your server depends on sessions for features like sampling, elicitation, roots, unsolicited notifications, or per-client isolation. Setting this explicitly protects your server from a future default change. The MCP specification requires that clients use sessions when a server's initialize response includes an Mcp-Session-Id header, so compliant clients will always honor your server's session. Once MRTR or a similar mechanism is available, you may be able to migrate server-to-client interactions to stateless mode and drop sessions entirely — but until then, explicit Stateless = false is the safe choice. See Stateless alternatives for server-to-client interactions for more on MRTR.

[!TIP]
If you're not sure which to pick, start with Stateless = true. You can switch to Stateless = false later if you discover you need server-to-client requests or unsolicited notifications. Either way, setting the property explicitly means your server's behavior won't silently change when the SDK default is updated.

halter73 and others added 2 commits March 27, 2026 08:10
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move Activity.AddTag before next() so child spans created during
request processing inherit the transport session ID. Accept that the
first initialize request won't have the tag (no request header yet).
Update test to match the simplified pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that /sse endpoint clients are using the legacy transport which
is now disabled by default. Cover client-side migration (change endpoint
URL), server-side migration (EnableLegacySse opt-in), and transition
period (both transports served simultaneously by MapMcp).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73
Copy link
Copy Markdown
Contributor Author

I also added this comment about how to deal with the legacy SSE breaking change:

Migrating from legacy SSE

If your clients connect to a /sse endpoint (e.g., https://my-server.example.com/sse), they are using the legacy SSE transport — regardless of any Stateless or session settings on the server. The /sse and /message endpoints are now disabled by default (xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse is false and marked [Obsolete] with diagnostic MCP9003). Upgrading the server SDK without updating clients will break SSE connections.

Client-side migration. Change the client Endpoint from the /sse path to the root MCP endpoint — the same URL your server passes to MapMcp(). For example:

// Before (legacy SSE):
Endpoint = new Uri("https://my-server.example.com/sse")

// After (Streamable HTTP):
Endpoint = new Uri("https://my-server.example.com/")

With the default xref:ModelContextProtocol.Client.HttpTransportMode.AutoDetect transport mode, the client automatically tries Streamable HTTP first. You can also set TransportMode = HttpTransportMode.StreamableHttp explicitly if you know the server supports it.

Server-side migration. If you previously relied on /sse being mapped automatically, you now need EnableLegacySse = true (suppressing the MCP9003 warning) to keep serving those endpoints. The recommended path is to migrate all clients to Streamable HTTP and then remove EnableLegacySse.

Transition period. If some clients still need SSE while others have already migrated to Streamable HTTP, set EnableLegacySse = true with Stateless = false. Both transports are served simultaneously by MapMcp() — Streamable HTTP on the root endpoint and SSE on /sse and /message. Once all clients have migrated, remove EnableLegacySse and optionally switch to Stateless = true.

Rename docs/concepts/sessions/ to docs/concepts/stateless/ and update
the uid, title, toc.yml entry, all xref links, and error message URLs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73 halter73 changed the title Add sessions doc, prefer stateless mode and disable legacy SSE by default Add stateless doc, prefer stateless mode and disable legacy SSE by default Mar 27, 2026
mikekistler
mikekistler previously approved these changes Mar 27, 2026
Copy link
Copy Markdown
Contributor

@mikekistler mikekistler left a comment

Choose a reason for hiding this comment

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

Looks good. 👍

There may still be some minor points of clarification but we can handle those in follow-up PRs.

# Conflicts:
#	docs/list-of-diagnostics.md
#	src/Common/Obsoletions.cs
Copy link
Copy Markdown
Contributor

@jeffhandley jeffhandley left a comment

Choose a reason for hiding this comment

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

I haven't reviewed all of the docs changes/additions, but I sign off on the behavioral changes being made here along with the new (obsolete-out-of-the-gate) property.

This makes me realize we should be more reluctant to pick any server behavioral defaults going forward.

I was very tempted to recommend we stage this even more granularly by:

  1. Introduce the EnableLegacySse property in 1 release
  2. Change the behavior in the next release

But that'd be overkill, and we're giving a call-to-action anyway.

I also asked @halter73 about marking WithHttpTransport as [Obsolete] and introducing a (worse) API that forces the specification of stateful vs. stateless. We decided against it, at least for now. Presumably that would've needed to be WithStatelessHttpTransport and WithStatefulHttpTransport or something else awkward.

@halter73 halter73 merged commit 8eb4023 into main Mar 27, 2026
9 of 11 checks passed
@halter73 halter73 deleted the halter73/stateless-docs branch March 27, 2026 20:56
@jeffhandley jeffhandley added the breaking-change This issue or PR introduces a breaking change label Mar 27, 2026
@jeffhandley
Copy link
Copy Markdown
Contributor

This PR has been labeled breaking-change during the release audit. Legacy SSE endpoints (/sse and /message) are no longer mapped by default — this is a behavioral breaking change for servers whose clients connect via SSE. Migration: set HttpServerTransportOptions.EnableLegacySse = true.

@jeffhandley jeffhandley mentioned this pull request Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change This issue or PR introduces a breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants