Skip to content

Run stateful and stateless conformance endpoints in a single server process#1672

Open
halter73 wants to merge 1 commit into
mainfrom
halter73-single-process-conformance-server
Open

Run stateful and stateless conformance endpoints in a single server process#1672
halter73 wants to merge 1 commit into
mainfrom
halter73-single-process-conformance-server

Conversation

@halter73

Copy link
Copy Markdown
Contributor

Follow-up to #1671 (squash-merged). This final commit was prepared on top of that PR but not included in the merge; this PR lands it on its own.

The conformance tests previously ran two ConformanceServer instances: a stateful one (the shared fixture, at the default endpoint) and a stateless one that each test spun up on its own fixed port — a dedicated StatelessMrtrConformanceServerFixture for MRTR, plus a StatelessConformanceServer helper that the SEP-2243 and caching tests started per-test on hand-picked port ranges. Differentiating the stateful server per target framework while pinning the stateless servers to fixed ports was inconsistent and prone to TCP TIME_WAIT conflicts on Windows.

This borrows TestSseServer's HandleStatelessMcp trick to host both lifecycles on a single Kestrel port:

  • The ConformanceServer now maps the stateful MCP server at / and a separate stateless MCP server at /stateless. The stateless server lives in its own ServiceCollection/ApplicationBuilder so its isolated HttpServerTransportOptions.Stateless value doesn't collide with the stateful registration. The --stateless/MCP_CONFORMANCE_STATELESS switch is gone since one process serves everything.
  • A single shared ConformanceServerFixture (in its own file) exposes ServerUrl (/) and StatelessServerUrl (/stateless), and both ServerConformanceTests and CachingConformanceTests consume it via [Collection(nameof(ConformanceServerCollection))]. This centralizes the per-framework port-binding logic and removes the now-redundant StatelessMrtrConformanceServerFixture and StatelessConformanceServer helper, so no test restarts a server on a fixed port.
  • Also drops a stray space in the commented-out [InlineData("http-custom-headers")] line in ClientConformanceTests to follow the usual convention for code meant to be uncommented later.

Follow-up cleanups to the conformance test infrastructure, squashed from three
working commits.

Serve both conformance lifecycles from one server. The conformance tests
previously ran two ConformanceServer instances: a stateful one
(ConformanceServerFixture, served at the default endpoint) and a stateless one
that each test spun up on its own fixed port (a StatelessMrtrConformanceServerFixture
for MRTR, plus a StatelessConformanceServer helper that the SEP-2243 and caching
tests started per-test on hand-picked port ranges). Differentiating the stateful
server per target framework while pinning the stateless servers to fixed ports
was inconsistent and prone to TCP TIME_WAIT conflicts on Windows.

Borrow TestSseServer's HandleStatelessMcp trick to host both lifecycles on a
single Kestrel port: the ConformanceServer now maps the stateful MCP server at
"/" and a separate stateless MCP server (its own ServiceCollection/
ApplicationBuilder so the isolated HttpServerTransportOptions.Stateless value
does not collide) at "/stateless". The --stateless/MCP_CONFORMANCE_STATELESS
switch is gone since one process serves everything.

A single shared ConformanceServerFixture (in its own file) now exposes ServerUrl
("/") and StatelessServerUrl ("/stateless"), and both ServerConformanceTests and
CachingConformanceTests consume it via [Collection(nameof(ConformanceServerCollection))].
This centralizes the per-framework port-binding logic, removes the now-redundant
StatelessMrtrConformanceServerFixture and StatelessConformanceServer helper, and
means no test restarts a server on a fixed port.

Also drop the stray space in the commented-out [InlineData("http-custom-headers")]
line in ClientConformanceTests so it follows the usual convention for code meant
to be uncommented later.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73 halter73 requested a review from tarekgh June 30, 2026 15:32
services.AddSingleton(app.Services.GetRequiredService<DiagnosticListener>());
services.AddRoutingCore();

ConfigureConformanceMcpServer(services, subscriptions, stateless: true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: the stateless server here reuses the same subscriptions dictionary as the stateful server on the main host. It's harmless today: the subscribe/unsubscribe handlers throw on the null stateless SessionId before touching the dictionary, and stateful session IDs are unique GUIDs, so the key spaces never collide. Still, these are two independent servers in separate DI containers, so sharing one mutable dictionary is a bit of a smell with no real upside. Could hand the stateless server its own new() dictionary to keep the two fully isolated.

@tarekgh tarekgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, nice cleanup. Hosting both lifecycles on one Kestrel port (stateful and stateless) gets rid of the fixed-port juggling that was causing the Windows  TIME_WAIT  flakiness, and consolidating onto a single shared  ConformanceServerFixture  means no test restarts a server on a fixed port anymore and the redundant stateless fixtures/helpers go away. I left one non-blocking nit if you want to consider it.

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.

3 participants