feat(worker): run both MCP servers stateless

Set Stateless = true on the internal (/mcp on the SignalR port) and external
MCP HTTP transports. Two effects:

- No Mcp-Session-Id, so a worker restart can no longer 404 a session that
  outlives it -- ConPTY tiles in the UI process and externally registered
  claude sessions keep working across a restart.
- A 2026-07-28 client is no longer refused back to the initialize handshake.
  A stateful server rejects that revision on purpose (see the SDK's
  StreamableHttpHandler.s_sessionSupportingProtocolVersions), so the new
  per-request protocol path was unreachable regardless of SDK version.

Nothing here used the stateful-only features (sampling, elicitation, resource
subscriptions, unsolicited notifications). In-tool progress notifications ride
the POST's own response stream and are unaffected -- which matters, since ~20
tools use them to hold off the client's 300s idle abort. All three consumers
(TaskRunner, PlanningSessionManager, the installer's RegisterMcpStep) already
register type: "http", so dropping the legacy SSE endpoint breaks nothing.

Verified against the running worker across two restarts: tools/list returns
57 tools over a bare POST with no initialize and no session id, and a full
2026-07-28 tools/call round-trip returns real data.
This commit is contained in:
mika kuns
2026-08-26 11:05:31 +02:00
parent f80af122e9
commit 69fcceb2ed
2 changed files with 9 additions and 2 deletions
+8 -2
View File
@@ -201,7 +201,13 @@ builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddScoped<FindingsStoreLocator>();
builder.Services.AddScoped<TaskRunFindingsMcpTools>();
builder.Services.AddMcpServer()
.WithHttpTransport()
// Stateless: no Mcp-Session-Id, so a worker restart doesn't 404 the sessions that
// outlive it (ConPTY tiles in the UI process, externally registered claude sessions),
// and a 2026-07-28 client isn't refused back to the initialize handshake. Nothing here
// needs the stateful-only features (sampling, elicitation, resource subscriptions,
// unsolicited notifications); in-tool progress works in both modes. Implies no legacy
// SSE endpoint — setting EnableLegacySse alongside this throws at startup.
.WithHttpTransport(o => o.Stateless = true)
.WithTools<PlanningMcpService>()
.WithTools<TaskRunMcpService>()
.WithTools<TaskRunFindingsMcpTools>();
@@ -342,7 +348,7 @@ if (cfg.ExternalMcpPort > 0)
externalBuilder.Services.AddScoped<FindingsStoreLocator>();
externalBuilder.Services.AddScoped<FindingsMcpTools>();
externalBuilder.Services.AddMcpServer()
.WithHttpTransport()
.WithHttpTransport(o => o.Stateless = true)
.WithRequestFilters(f => f.AddCallToolFilter(ExternalMcpExceptionFilter.Wrap))
.WithTools<ExternalMcpService>()
.WithTools<BatchMcpTools>()