Skip to content

Add opt-in stateless HTTP transport mode#43

Merged
tmunzer-AIDE merged 15 commits into
mainfrom
feat/stateless-http-mode
Jun 16, 2026
Merged

Add opt-in stateless HTTP transport mode#43
tmunzer-AIDE merged 15 commits into
mainfrom
feat/stateless-http-mode

Conversation

@tmunzer-AIDE

Copy link
Copy Markdown
Collaborator

Summary

Adds an opt-in stateless HTTP transport (MISTMCP_STATELESS env / --stateless flag). With --transport http, the server is served via mcp_server.http_app(stateless_http=True) under uvicorn — a fresh transport per request, no server session id — so an already-connected MCP client survives a server restart without reconnecting. Default off ⇒ existing behavior is byte-for-byte unchanged.

Because the per-request transport starts already-initialized, on_initialize can't be relied on to set per-session state, so the port also:

  • Resolves write-tool visibility at build time in create_mcp_server() (_configure_write_visibility): hides write + write_delete by default and re-shows write only in the DANGER ZONE. Behavior-neutral in stateful mode (the elicitation middleware still re-resolves per session); required for stateless so the DELETE tool can't leak or auto-execute.
  • Sets the DANGER-ZONE elicitation bypass request-scoped in a new on_call_tool (gated on config.stateless), so auto-accept works without an initialize and doesn't leak into the session store.
  • Fails closed deterministically: config_elicitation_handler raises ElicitationUnavailableError (converted to a clean ToolError by the existing tool wrappers) when elicitation is needed in stateless HTTP, instead of relying on ctx.elicit()'s undefined stateless behavior.

Safety gate: startup is refused (non-zero exit) for exactly stateless AND http AND enable_write_tools AND NOT disable_elicitation — the one combo that would need in-band elicitation. Also adds a MISTMCP_DISABLE_ELICITATION env var (symmetric with MISTMCP_ENABLE_WRITE_TOOLS) so env-only deployments can reach the writes-in-stateless (DANGER ZONE) config.

Accepted trade-offs (documented in the README): no server→client push (GET route dropped), and destructive upgrade/utility actions fail closed in read-only stateless.

Design spec and implementation plan are included under docs/superpowers/.

Test Plan

  • uv run python -m pytest — 97 passed, coverage 61% (gate ≥30%)
  • uv run ruff check src tests / ruff format --check — clean on all changed files
  • New tests cover: the validation refusal matrix; env/CLI/start() threading + main() non-zero exit; build-time visibility matrix + idempotency (via real list_tools()); on_call_tool request-scoped state; the fail-closed guard; and the _run_stateless_http launch (asserts http_app(stateless_http=True) with no event_store + exact uvicorn args)
  • Default-off behavior verified byte-for-byte unchanged (non-stateless HTTP launch path identical; new branches no-op when stateless=False)
  • Manual smoke: uv run mistmcp --transport http --stateless (logs the stateless INFO line; restart-survival); --stateless --enable-write-tools without --disable-elicitation exits non-zero with the refusal message

🤖 Generated with Claude Code

tmunzer-AIDE and others added 15 commits June 15, 2026 17:26
Design for an opt-in stateless HTTP transport (MISTMCP_STATELESS /
--stateless) so an already-connected MCP client survives a server restart.
Port of the private-fork feature, adapted to this repo (no URL mode / webui /
Mongo). Centralizes write+write_delete build-time visibility and adds a
request-scoped elicitation bypass for the stateless DANGER-ZONE path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- [P2] Add §5.8 fail-closed guard in config_elicitation_handler: raise
  ElicitationUnavailableError when stateless+http and state is not set, instead
  of relying on ctx.elicit() behavior. All 3 call sites already convert it to
  ToolError. Matrix/trade-offs/tests updated.
- [P3] Soften "on_initialize does not fire" -> "cannot be relied on to set state
  for later tool calls" (SDK starts the per-request session initialized).
- [P3] Drop .env.example (absent in this repo); README env tables are canonical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Renumber: fail-closed guard 5.7, fatality 5.8 (match physical order); fix refs
- "three write-ish tools" -> "four tools in the write/mutation surface"
- Experimental matrix row labeled "write + ?experimental=true" (branch needs
  enable_write_tools)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7-task TDD plan (config+validation, elicitation guard, build-time visibility,
on_call_tool, start()+launch, env+main wiring, README). Task order keeps the
full suite green at every step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rtup gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s danger zone

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nfig

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tmunzer-AIDE tmunzer-AIDE merged commit 112ff1e into main Jun 16, 2026
4 checks passed
@tmunzer-AIDE tmunzer-AIDE deleted the feat/stateless-http-mode branch June 16, 2026 02:20
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