Build1 publisher3 min readPublished
Stateless MCP pushes the server object inside the request handler
Skybridge v2 dropped the one-server-per-process pattern for a factory that builds an McpServer per request. The bill lands on module-scope work, which now runs per request unless you move it.
The Engineer · Build desk

What happened
- MCP's 2026-07-28 revision, shipped in July, made the protocol stateless, retiring the initialize handshake and the session ID along with it.
- Skybridge v2 replaced its one-McpServer-per-process pattern with a factory that spins up a fresh server for each request, because the server now has to speak two protocol versions.
- The entry point splits into server.ts, which holds the whole app definition, and index.ts, which only runs it, so tests can import the app without starting it.
- A new @skybridge/test package runs a real model conversation against the app in process and reports which tools the model reached for.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Anything expensive left in the handler becomes a per-request allocation, and Alpic says neither the compiler nor a manual test catches it, so the mistake shows up as resource growth under load rather than as a failing build.
- cost Server construction moves from process start to request time, so the cost is paid on every user call by whoever runs the app, not once by whoever deploys it.
- decision Any framework serving both revisions has to pick where the negotiated version lives, and Skybridge's choice pushes a per-request lifecycle down onto every app built on it.
- capability Tool-choice failures, where the model answers from its own knowledge instead of calling your tool, become assertable in CI rather than something you probe by hand in a chat window.
Statelessness took away the place a server used to keep what it negotiated with the client: the initialize handshake and the session ID both went away in the 2026-07-28 revision [1]. The SDK stamps an McpServer instance with the negotiated revision, and your tools, resources, prompts and views get registered on that same instance [6]. One object holds both the protocol version and the registry. Per-request negotiation therefore means a per-request object.
The attribution in Alpic's post is narrower than that, though. It gives the trigger as the server needing to speak two different versions of the protocol, alongside a change in how the new revision serializes results [5][3]. Statelessness is what makes the revision a per-request property rather than a per-connection one. A framework that only ever speaks one revision may still get away with a single server for the process, which is what the official SDK did before [4].
Registration is the only work that belongs in the handler body, because that body runs on every request, and anything that used to sit once at module scope moves to `setup` [9]. Alpic's own wrong example is a `pg.Pool` constructed inside the handler, one pool per request [11]. A pool created per request is still a pool. It is just nobody else's.
`setup` runs once, at `run()` or on the first request, never at module import, and its result arrives as the handler's second argument [12]. That is the good part of the design. The correct pattern is also the shorter one to type, so the API shape does the teaching that a comment in the docs would not. The eval matchers show the same instinct: they are typed against your own registry, so a tool name that no longer exists fails at the type level [17].
The guardrail is a console warning, once, when a handler takes longer than 50ms [13]. Divide a second by 50ms and you get 20 [19]. A handler sitting at that threshold spends a full second of processing on registration every 20 requests, before a single tool does any work. Registration should be assembling closures, so 50ms is a generous ceiling, and a handler that hits it is almost certainly doing I/O that belongs one level up.
What the source supports is one framework's migration, described by the vendor that ships it. It does not show that the official SDK now requires a factory, or that every MCP app must adopt one; it shows that a framework built on that SDK found per-request instantiation necessary to serve both revisions at once [5]. If your server pins a revision, the forcing function in this post does not apply to you.
On a v1 migration, the statements to audit are the ones that used to sit at module scope above the server definition. In v1 they lived in the same file as the definition, and the tempting move is to wrap the lot in the handler [14]. Each of those lines now needs a decision: once in `setup`, or once per request [12]. Neither the compiler nor a manual call in devtools will make that decision for you, and per Alpic, neither will catch it if you get it wrong [10].
What to watch
- Whether the official MCP SDK documents a per-request server factory as the supported pattern, or keeps one-per-process for servers that pin a single revision.
- Whether the 50ms handler warning hardens into a limit once apps are published in the stores.
- Whether the eval matchers extend past single tool calls to multi-turn sequences; the published excerpt only shows one assertion.