feat(eve): stateful MCP client sessions#331
Open
ckblockit wants to merge 8 commits into
Open
Conversation
Signed-off-by: Chung Eun Kim <ck@blockit.com>
Signed-off-by: Chung Eun Kim <ck@blockit.com>
Signed-off-by: Chung Eun Kim <ck@blockit.com>
Signed-off-by: Chung Eun Kim <ck@blockit.com>
Signed-off-by: Chung Eun Kim <ck@blockit.com>
…lots `connectionProvider.create` now reads persisted `Mcp-Session-Id` values from `session.state` and seeds `McpSessionSlots` into `ConnectionRegistryImpl` so stateful MCP connections resume their server-side session across eve step boundaries. `connectionProvider.commit` drains `registry.collectMcpSessionUpdates()` back into `session.state`, completing the durable round-trip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Chung Eun Kim <ck@blockit.com>
Add a "Stateful sessions" subsection to docs/connections/mcp.mdx documenting the session: "stateful" option, per-principal scoping, and automatic 404 re-initialize. Add a patch changeset for the eve package. Fix as-unknown-as double casts in connection.test.ts to satisfy guard:invariants (add required ModuleSourceRef fields instead). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Chung Eun Kim <ck@blockit.com>
A 404 during HTTP initialize while replaying a persisted session id means the session expired on the server, not a streamable-HTTP/SSE transport mismatch. Rethrow immediately so #withSessionRetry clears the slot and re-initializes over HTTP. The 404→SSE path is unchanged for connections that are not replaying a session id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Chung Eun Kim <ck@blockit.com>
|
@ckblockit is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
Author
|
@allenzhou101 could you review this when you have a chance? Thanks! (Requesting via comment — I'm contributing from a fork, so I can't assign you as a formal reviewer.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
session: "stateful"option todefineMcpClientConnection. When set, an MCP connection persists its server-assignedMcp-Session-Idacross eve step boundaries, so a stateful MCP server (one that returnsMcp-Session-Idoninitialize, per the MCP spec) sees one continuous session for the life of the eve session instead of a net-new session every step.The problem
eve rebuilds the connection registry on every step — it's a derived context key holding live client instances that can't survive a step boundary. So each step created a fresh
@ai-sdk/mcpclient, opened a new transport, and sent a newinitialize, getting a newMcp-Session-Idevery step. The previously negotiated session was never captured or persisted, so a stateful MCP server treated every step as a brand-new session.How it works
The work splits into cheap replay and a small capture:
Mcp-Session-Idas a request header.initialize): read theMcp-Session-Idoff the server's response.Both happen through a custom
fetchpassed to@ai-sdk/mcp's HTTP transport, driven by a per-step mutable "slot". Persistence mirrors the existingsandboxProvider:sessionflows from the public definition →ResolvedConnectionDefinition.connectionProvider.createseeds a slot fromsession.state, keyed bymcpSessionStateKey(connectionName, principalId).McpConnectionClientits slot; the client'sfetchwrapper injects + captures theMcp-Session-Id.404(expired/unknown session), the client clears the slot, closes, and re-initializes once.connectionProvider.commitdrains changed ids back intosession.state, which survives the step boundary.Scoping: per connection + authenticated principal, with an
"anonymous"fallback for unauthenticated callers.session.stateis itself per-eve-session, so there's no cross-session or cross-principal leakage.Stateless connections are unchanged — no slot, no
fetchwrapper, no retry.Deliberately deferred: HTTP
DELETEterminationThe MCP spec says a client SHOULD send
DELETEto terminate a session when done. eve has no clean per-eve-session "end" hook (commit/disposerun every step;HarnessSessionhas no terminal flag), and firingDELETEat any existing hook would kill the session we're deliberately persisting. SinceDELETEisSHOULD(notMUST) and correctness is preserved by server-side TTL + the mandatory404re-init path, it's left as a follow-up rather than adding session-lifecycle plumbing here.Testing
404 → re-init, the fetch wrapper's inject/capture/no-clobber, and theconnectionProvidercreate→commit round-trip (persist + re-seed across a simulated step boundary).404while replaying a persisted session re-initializes over HTTP rather than falling back to SSE (it fails without the guard).guard:invariants, anddocs:checkall pass.Changeset
patch— new feature, pre-1.0.🤖 Generated with Claude Code