<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shilpa Mareddy</title>
    <description>The latest articles on DEV Community by Shilpa Mareddy (@shilpamareddy06-ops).</description>
    <link>https://dev.to/shilpamareddy06-ops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172398%2F5aa88a0f-afce-4e11-916e-11df853e0998.png</url>
      <title>DEV Community: Shilpa Mareddy</title>
      <link>https://dev.to/shilpamareddy06-ops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shilpamareddy06-ops"/>
    <language>en</language>
    <item>
      <title>Why in-memory MCP sessions fail behind a load balancer (NestJS demo)</title>
      <dc:creator>Shilpa Mareddy</dc:creator>
      <pubDate>Fri, 09 Oct 2026 04:27:46 +0000</pubDate>
      <link>https://dev.to/shilpamareddy06-ops/why-in-memory-mcp-sessions-fail-behind-a-load-balancer-nestjs-demo-3fj0</link>
      <guid>https://dev.to/shilpamareddy06-ops/why-in-memory-mcp-sessions-fail-behind-a-load-balancer-nestjs-demo-3fj0</guid>
      <description>&lt;p&gt;&lt;em&gt;By Shilpa Mareddy&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two copies of an MCP (Model Context Protocol) server (a server that AI applications call tools on) sit behind a round-robin load balancer (it sends each request to the next server in turn). The client's first request lands on copy A, the second on copy B, and B answers &lt;code&gt;400 Bad Request: Server not initialized&lt;/code&gt;. No tool was ever called.&lt;/p&gt;

&lt;p&gt;In the demo below, that failure comes from an MCP session held in one process's memory. A session is state the server keeps for one client connection.&lt;/p&gt;

&lt;p&gt;The 2026-07-28 revision of the MCP spec removes protocol-level sessions from its HTTP transport.&lt;/p&gt;

&lt;p&gt;This article reproduces the failure, then serves a small basket API (create a basket, add items, read it) from NestJS with no session. It then checks, with two real processes, that either one can answer these requests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version, in plain words.&lt;/strong&gt; Picture a coat check with two desks. You hand your coat to desk A and get a ticket. If you walk to desk B, they cannot honor your ticket, because only desk A has the record. That is roughly the failure above: the session lived in one server's memory, and the second server had no matching session. The fix is a claim number that any desk can look up in one shared back room. In the demo, the "back room" is a shared database, and the "claim number" is a basket ID the client sends with every call. In this demo, either copy of the server can then serve any of its three tools. The spec removes the protocol's own ticket (the session ID); the basket ID is a new one that the tool design creates, and it is a lookup key, not proof of who you are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will learn&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the 2026-07-28 spec removed from MCP over HTTP, and what replaced it.&lt;/li&gt;
&lt;li&gt;How one NestJS route serves MCP with no session.&lt;/li&gt;
&lt;li&gt;How to test "any instance can answer any request" with two real processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you need:&lt;/strong&gt; Node 22.13 or newer (the minimum the repo needs; tested only on 22.22.2 and 24.21.0), npm, and a working knowledge of HTTP and load balancers. You do not need to know MCP. Code: &lt;a href="https://github.com/shilpamareddy06-ops/ai-backend-patterns/tree/main/01-stateless-mcp-nestjs" rel="noopener noreferrer"&gt;shilpamareddy06-ops/ai-backend-patterns&lt;/a&gt; (folder &lt;code&gt;01-stateless-mcp-nestjs&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versions used:&lt;/strong&gt; MCP spec 2026-07-28; &lt;code&gt;@modelcontextprotocol/server&lt;/code&gt; 2.2.0, &lt;code&gt;@modelcontextprotocol/client&lt;/code&gt; 2.2.0, &lt;code&gt;@modelcontextprotocol/node&lt;/code&gt; 2.1.0; NestJS 12.1.1 (12.1.2 came out on 2026-09-30 and was not tested); zod 4.6.5; TypeScript 7.0.2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The failure: an in-memory session pins a client to one instance
&lt;/h2&gt;

&lt;p&gt;Under the 2025 protocol, a client began with a handshake, the setup exchange before the first tool call: &lt;code&gt;initialize&lt;/code&gt;, then &lt;code&gt;notifications/initialized&lt;/code&gt;. A server could assign a session at &lt;code&gt;initialize&lt;/code&gt; by returning an &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header, and a client that received one had to send it on every later request. The repo's baseline server does this. Key excerpt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/baseline/sessionful-main.ts&lt;/code&gt; (lines 22-23)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodeStreamableHTTPServerTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;sessionIdGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transport creates the session id and keeps it in this process's memory. The basket data sits in a shared store, so the only thing tying a client to one instance is the session.&lt;/p&gt;

&lt;p&gt;Two baseline processes run behind a small round-robin proxy written for the tests. It has no sticky routing (also called session affinity: load balancer configuration that sends every request from one client to the same instance).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;test/round-robin-proxy.ts&lt;/code&gt; (lines 15-24)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Plain round-robin reverse proxy: request N goes to backend N mod count. No affinity, no cookies,
 * no header inspection for routing. It records every exchange so tests can assert on the wire.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startRoundRobinProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backendPorts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Exchange&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;backendPorts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default v2 client connects through the proxy. Output captured from &lt;code&gt;test/baseline.test.ts&lt;/code&gt;, as recorded in the repo README:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;README.md&lt;/code&gt; (lines 93-95)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 0 POST initialize -&amp;gt; 200 req=- res=0fddeeb6-e410-4729-b61c-dd701a31548d
 1 POST notifications/initialized -&amp;gt; 400 req=0fddeeb6-e410-4729-b61c-dd701a31548d res=-
CLIENT ERROR: SdkHttpError: Error POSTing to endpoint: {"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Server not initialized"},"id":null}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns: backend index, HTTP method, JSON-RPC method (JSON-RPC is the message format MCP uses: a method name, parameters, an id), status, session id sent, session id returned. Instance 0 created the session at &lt;code&gt;initialize&lt;/code&gt;. The next request carried that id to instance 1, which answered HTTP 400, code -32000, "Bad Request: Server not initialized". The message comes from the SDK transport. The client never reaches a tool call. A control test shows the baseline working against one instance directly, so the proxy routing is what breaks it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fid3fthbizngvr8atzryc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fid3fthbizngvr8atzryc.png" alt="Before: the session ID lives in instance A’s memory, so the next request, sent to instance B with that ID, fails with HTTP 400. After: no session exists; create_basket, add_item and get_basket go to alternating instances and succeed because the basket lives in a shared store." width="800" height="717"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Illustrative, simplified diagram (the real test makes four calls and starts with &lt;code&gt;server/discover&lt;/code&gt;, which is not drawn). One baseline design, one proxy: the data was shared, the session was not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is one design, not all sessionful servers.&lt;/strong&gt; The baseline keeps one transport per process, so it holds exactly one session. B's error shows that B has no initialized transport, not that B checked the session id. A server with a session map would fail differently, for example with an unknown-session error. Session maps, sticky routing and shared session stores were not tested, and this article makes no claim about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 2026-07-28 spec changed
&lt;/h2&gt;

&lt;p&gt;The spec was released on 2026-07-28. The idea: every request stands alone and carries what the server needs, so any instance can answer it. In plain terms, the server stops remembering who you are between calls, and the list below shows how. Readers who only want the idea can skim the codes and proposal numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No sessions.&lt;/strong&gt; The changelog removes protocol-level sessions and the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header from the Streamable HTTP transport (MCP's transport over HTTP). List results no longer vary per connection. This comes from SEP-2567 (a SEP is a numbered design proposal for the spec).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No handshake.&lt;/strong&gt; &lt;code&gt;initialize&lt;/code&gt; and &lt;code&gt;notifications/initialized&lt;/code&gt; are gone (SEP-2575). The spec: "There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently." The version and client capabilities travel in each request's &lt;code&gt;_meta&lt;/code&gt; field, a place in the parameters for protocol metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One endpoint.&lt;/strong&gt; A single POST endpoint; every JSON-RPC message is its own POST. The GET endpoint is removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routable headers.&lt;/strong&gt; Every POST that carries a request has the headers &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; and &lt;code&gt;Mcp-Method&lt;/code&gt;. Calls to &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;resources/read&lt;/code&gt; and &lt;code&gt;prompts/get&lt;/code&gt; also have &lt;code&gt;Mcp-Name&lt;/code&gt;. The stated purpose is that intermediaries can route without parsing the body. Servers must check them against the body and answer HTTP 400 with &lt;code&gt;-32020&lt;/code&gt; on a mismatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;State that must survive between calls does not disappear. SEP-2567 says it should use explicit handles: IDs the server creates and the client passes back as ordinary tool arguments. Its example is &lt;code&gt;create_basket()&lt;/code&gt; returning a &lt;code&gt;basket_id&lt;/code&gt;, then &lt;code&gt;add_item(basket_id, ...)&lt;/code&gt;. It calls this a tool-design pattern with no wire format, "not a protocol change". The demo follows that example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serve MCP from one NestJS route, with no session
&lt;/h2&gt;

&lt;p&gt;In plain words: one route accepts every MCP request and builds a fresh, empty server for each one. Nothing is remembered inside the server, and anything that must be remembered goes to the database. This is the whole controller (&lt;code&gt;src/mcp/mcp.controller.ts&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/mcp/mcp.controller.ts&lt;/code&gt; (lines 1-31)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;All&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Res&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hostHeaderValidation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originValidation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toNodeHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BASKET_STORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BasketStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../basket/basket.store.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildBasketServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./basket-server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;McpController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;toNodeHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;validateHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;hostHeaderValidation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;validateOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;originValidation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BASKET_STORE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// DNS-rebinding protection. Defaults are localhost only; behind a load balancer set ALLOWED_HOSTS.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validateHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hostHeaderValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedHosts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validateOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;originValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedOrigins&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// 'reject' = 2026-07-28 only. Drop it to also serve 2025-era clients through the SDK's stateless fallback.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHandler&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;buildBasketServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toNodeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// One route for every method: the SDK decides what is valid (POST only, no GET stream, no DELETE session).&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;All&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Res&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Guards answer 403 themselves and return false.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validateOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createMcpHandler&lt;/code&gt; takes a factory. Per the SDK docs it builds a fresh server for each request, and there is no &lt;code&gt;Mcp-Session-Id&lt;/code&gt;. &lt;code&gt;toNodeHandler&lt;/code&gt; from &lt;code&gt;@modelcontextprotocol/node&lt;/code&gt; mounts it on Nest's request and response objects. &lt;code&gt;legacy: 'reject'&lt;/code&gt; accepts only 2026-07-28 traffic (more below). &lt;code&gt;src/main.ts&lt;/code&gt; turns Nest's body parser off so the SDK reads the raw body; Nest's default parser was not tested.&lt;/p&gt;

&lt;p&gt;The Host and Origin checks are DNS-rebinding protection (a web page in your browser tricking it into reaching a server on your own machine), not authentication. Defaults accept localhost only; behind a real load balancer set &lt;code&gt;HOST&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; and &lt;code&gt;ALLOWED_ORIGINS&lt;/code&gt; (see the README), or other hosts get HTTP 403.&lt;/p&gt;

&lt;p&gt;The factory is &lt;code&gt;buildBasketServer&lt;/code&gt; (excerpt; &lt;code&gt;get_basket&lt;/code&gt;, lines 52-59, follows the same pattern):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/mcp/basket-server.ts&lt;/code&gt; (lines 19-50)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Builds a fresh McpServer. createMcpHandler calls this once per HTTP request,
 * so nothing here may hold state: everything lives in the injected store,
 * and the client carries the basket_id from call to call.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildBasketServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BasketStore&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stateless-basket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_basket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create an empty basket. Returns a basket_id to pass to the other tools.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;basket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add qty of an item to a basket.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;basket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;basket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;basket&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;unknownBasket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basket_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in the factory holds state. The client carries &lt;code&gt;basket_id&lt;/code&gt; from call to call, and the state lives behind the &lt;code&gt;BasketStore&lt;/code&gt; interface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;src/basket/basket.store.ts&lt;/code&gt; (lines 10-15)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;BasketStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Adds qty to the item (creating it if needed). Resolves undefined if the basket does not exist. */&lt;/span&gt;
  &lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Basket&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spec removes the session, not the need for storage. In its discussion of gateways, SEP-2567 says that with handles "any replica can serve it from shared storage". Here that storage is one SQLite file both processes open, which suits a demo, not production. (The spec removed sessions; the SDK is more nuanced, as a later section explains.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A MongoDB store: an untested design sketch (not built or run).&lt;/strong&gt; Each basket would be one document holding &lt;code&gt;ownerId&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt;, and an &lt;code&gt;expiresAt&lt;/code&gt; date. A TTL index (a MongoDB index that deletes documents after a date) on &lt;code&gt;expiresAt&lt;/code&gt; lets MongoDB delete abandoned baskets on its own. MongoDB's docs say the TTL background task runs every 60 seconds and that expired documents can linger beyond that, depending on server load, so reads should also treat a basket past &lt;code&gt;expiresAt&lt;/code&gt; as missing. &lt;code&gt;expiresAt&lt;/code&gt; would be pushed forward on every write, so a basket in active use does not expire. For quantity changes the sketch uses an atomic update (an &lt;code&gt;$inc&lt;/code&gt; on the item's quantity, with items stored as a map keyed by item name) rather than read-modify-write, so two replicas adding items at once cannot lose an update, which is the same property the 20-concurrent-calls test checks here for SQLite.&lt;/p&gt;

&lt;p&gt;To tie the handle to a user, never trust the handle alone. Every call would look up the basket by &lt;code&gt;basketId&lt;/code&gt; and the authenticated user's id together, and return the same "not found" result whether the basket does not exist or belongs to someone else, so a leaked or guessed handle reveals nothing. A handle only identifies state; it is not a credential, so authorization still has to happen on each call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests check: two real processes behind one plain proxy
&lt;/h2&gt;

&lt;p&gt;In plain words: the test starts two copies of the server, sends requests to them in turn, and checks that both copies see the same basket. The stateless test starts two separate OS processes running the compiled Nest app, behind the same round-robin proxy, and uses the official v2 client. That client defaults to the 2025 handshake, which this server answers with HTTP 400 and &lt;code&gt;-32022 "Unsupported protocol version: 2025-11-25"&lt;/code&gt;. So each test client pins the new protocol:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;test/stateless.test.ts&lt;/code&gt; (lines 34-43)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** A fresh client per test, so no test depends on another one having run. */&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stateless-test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="c1"&gt;// The v2 client defaults to the 2025 handshake; this pins the 2026-07-28 stateless protocol.&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;versionNegotiation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wire-level test checks every exchange the proxy recorded:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;test/stateless.test.ts&lt;/code&gt; (lines 77-100)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;never uses a session: no initialize, no Mcp-Session-Id in either direction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// includes the connect-time exchange(s)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createBasket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exchanges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;request carried a session id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-session-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseHeaders&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response carried a session id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpcMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;initialize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpcMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications/initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-protocol-version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// The first request is server/discover, the rest are self-describing calls.&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;rpcMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server/discover&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Routing headers are present on tool calls (Mcp-Method / Mcp-Name).&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exchanges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpcMethod&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expected a tools/call exchange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_basket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the tests check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No request or response carries &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, and &lt;code&gt;initialize&lt;/code&gt; is never sent.&lt;/li&gt;
&lt;li&gt;Every request carries &lt;code&gt;MCP-Protocol-Version&lt;/code&gt; set to &lt;code&gt;2026-07-28&lt;/code&gt;, and the first request is &lt;code&gt;server/discover&lt;/code&gt; (servers must implement it; clients may skip it).&lt;/li&gt;
&lt;li&gt;Four consecutive calls (create, add, add, read) land on alternating instances and see the same basket.&lt;/li&gt;
&lt;li&gt;20 concurrent &lt;code&gt;add_item&lt;/code&gt; calls (quantity 1 each) sent through the round-robin proxy sum to exactly 20. This exercises the SQLite store, not the protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm test&lt;/code&gt; runs 11 tests (7 stateless, 2 baseline, 2 for Host/Origin configuration). In the build run all passed on Node v22.22.2, Linux. When an independent check pointed the two instances at different database files, 3 of the 11 tests failed (the cross-instance, input-validation and concurrency tests), because the second instance did not know the basket. I have not run that check myself. It shows that the tests detect unshared state.&lt;/p&gt;

&lt;p&gt;By hand (not covered by &lt;code&gt;npm test&lt;/code&gt;), the repo notes also record that a request with no per-request metadata gets HTTP 400, with &lt;code&gt;-32022&lt;/code&gt; when the version header is missing as well and &lt;code&gt;-32602&lt;/code&gt; when only &lt;code&gt;_meta&lt;/code&gt; is missing. That is SDK behavior as observed; the spec's validation rules suggest &lt;code&gt;-32020&lt;/code&gt; for a missing required header, so check the spec before relying on it.&lt;/p&gt;

&lt;p&gt;I also ran it myself on a MacBook (macOS) with Node v24.21.0: I ran &lt;code&gt;npm ci&lt;/code&gt; and then &lt;code&gt;npm test&lt;/code&gt;. All 11 tests passed (3 suites, 0 failures) in about 3.2 seconds. The "Bad Request: Server not initialized" line printed in the output is the baseline demo failing on purpose, not a test failure. I then cloned the public repository fresh from GitHub into an empty folder and ran &lt;code&gt;npm ci&lt;/code&gt; (128 packages, 0 vulnerabilities) and &lt;code&gt;npm test&lt;/code&gt; again, with the same result: 11 of 11 passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  "No sessions" is true of the spec, not of every SDK code path
&lt;/h2&gt;

&lt;p&gt;The v2 TypeScript SDK still contains a sessionful path, and the baseline uses it. The SDK docs describe multi-node use with a shared event store (for resuming dropped streams) or per-node sessions that must be routed by session id. Also, &lt;code&gt;createMcpHandler&lt;/code&gt; defaults to &lt;code&gt;legacy: 'stateless'&lt;/code&gt;, which serves both 2025-era and 2026-07-28 clients. The demo sets &lt;code&gt;legacy: 'reject'&lt;/code&gt;, which refuses 2025-era traffic; the default fallback was not exercised. The SDK migration guide describes this default; the demo does not use it.&lt;/p&gt;

&lt;p&gt;SEP-2567 describes a "clean break" with no deprecation window, while the SDK ships a compatibility shim. The SEP's own text is stale in places, so use the spec pages for normative claims.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Related changes, not covered here.&lt;/strong&gt; Other features were deprecated or reclassified in this revision (HTTP+SSE was deprecated earlier, in 2025), with different removal rules, not one shared window. Roots, Sampling, Logging and Dynamic Client Registration have an earliest removal in the first revision released on or after 2027-07-28; actual removal is a Core Maintainer decision that may come later. The old HTTP+SSE transport has its own rule: earliest removal is three months after SEP-2596 reaches Final. SEP-2596 is marked Final, but its page gives no date, so no calendar date is given here. Sessions were removed outright. See the deprecated features registry in Sources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this demo does not prove
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real load balancer, or other sessionful designs.&lt;/strong&gt; The proxy is a 65-line Node script written for the tests. No nginx or Kubernetes was run; Host and Origin configuration was tested with simulated headers only. Session maps, sticky routing and shared session stores were not tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header-based routing.&lt;/strong&gt; The proxy ignores the new routing headers. The tests check that they are sent, not that a load balancer can use them; the &lt;code&gt;-32020&lt;/code&gt; rejection is not tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi round-trip requests.&lt;/strong&gt; The spec's replacement for a server asking the client something mid-call: the server returns an input-required result and the client retries with the answer. Servers must use it for &lt;code&gt;roots/list&lt;/code&gt;, &lt;code&gt;sampling/createMessage&lt;/code&gt; and &lt;code&gt;elicitation/create&lt;/code&gt;, a breaking change. Not implemented here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth.&lt;/strong&gt; There is none: anyone holding a &lt;code&gt;basket_id&lt;/code&gt; can use it, and handles never expire. SEP-2567's non-normative guidance is to validate the handle together with the auth context on every call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming.&lt;/strong&gt; SSE (server-sent events, streaming over one HTTP response) is untested. The spec says a broken response stream loses the in-flight request, with no resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other features and clients.&lt;/strong&gt; Resources, prompts, tasks, &lt;code&gt;subscriptions/listen&lt;/code&gt; and the 2025-client fallback. Only the official v2 TypeScript client was used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production storage.&lt;/strong&gt; &lt;code&gt;SqliteBasketStore&lt;/code&gt; uses &lt;code&gt;node:sqlite&lt;/code&gt;, which is experimental in Node 22 and prints an &lt;code&gt;ExperimentalWarning&lt;/code&gt; at startup. One SQLite file only works for processes on one machine. The store is demo-only; a real deployment would use a shared database such as MongoDB or Redis. Nothing caps baskets or items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance and environments.&lt;/strong&gt; No benchmarks. The full build run was Linux with Node v22.22.2; there was also one &lt;code&gt;npm test&lt;/code&gt; run on macOS with Node v24.21.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you should not do this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sticky routing already meets your needs.&lt;/strong&gt; If affinity works and its costs are acceptable, moving state into a store is extra work. Sticky routing was not tested here, so this article cannot compare them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your clients still speak the 2025 protocol.&lt;/strong&gt; The spec's compatibility table says a legacy client against a modern-only server fails. A dual-era server can serve both on one endpoint (the SDK default); not tested here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your tools need to ask the client something mid-call.&lt;/strong&gt; Server-initiated requests must move to the multi round-trip mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You run one instance.&lt;/strong&gt; The baseline's control test worked against a single instance. The failure here needs requests to reach different processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways: keep MCP server state out of memory
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If a server keeps per-client state in its own memory, it can break when a load balancer sends requests to different copies, as the baseline here did. Sticky routing is another fix this article did not test.&lt;/li&gt;
&lt;li&gt;Keep that state in a shared store and give the client an ID to send back with each call, as the basket ID does here.&lt;/li&gt;
&lt;li&gt;Treat the ID as a lookup key, not proof of identity. If your server has authentication, check who is calling on every request. The demo has none, so anyone holding a basket ID can use it.&lt;/li&gt;
&lt;li&gt;Test with two real server processes behind a plain round-robin proxy, not with one process.&lt;/li&gt;
&lt;li&gt;If the server accepts only 2026-07-28 traffic, as this demo's does, make the v2 TypeScript client pin that version (or use its &lt;code&gt;'auto'&lt;/code&gt; mode), because the client defaults to the 2025 handshake.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Spec and proposals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;MCP spec 2026-07-28 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http" rel="noopener noreferrer"&gt;Streamable HTTP transport&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning" rel="noopener noreferrer"&gt;Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/discover" rel="noopener noreferrer"&gt;server/discover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr" rel="noopener noreferrer"&gt;Multi round-trip requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/deprecated" rel="noopener noreferrer"&gt;Deprecated features registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/seps/2567-sessionless-mcp" rel="noopener noreferrer"&gt;SEP-2567: Sessionless MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/seps/2575-stateless-mcp" rel="noopener noreferrer"&gt;SEP-2575: Make MCP Stateless&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/seps/2596-spec-feature-lifecycle-and-deprecation" rel="noopener noreferrer"&gt;SEP-2596: Feature lifecycle and deprecation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/transports" rel="noopener noreferrer"&gt;MCP spec 2025-11-25, transports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SDK and release notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ts.sdk.modelcontextprotocol.io/v2/migration/support-2026-07-28.html" rel="noopener noreferrer"&gt;TypeScript SDK v2: supporting protocol revision 2026-07-28&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ts.sdk.modelcontextprotocol.io/v2/serving/http.html" rel="noopener noreferrer"&gt;TypeScript SDK v2: serving over HTTP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ts.sdk.modelcontextprotocol.io/v2/serving/sessions-state-scaling.html" rel="noopener noreferrer"&gt;TypeScript SDK v2: sessions, state and scaling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;MCP blog: 2026-07-28 release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/sdk-betas-2026-07-28/" rel="noopener noreferrer"&gt;MCP blog: SDK betas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mongodb.com/docs/manual/core/index-ttl/" rel="noopener noreferrer"&gt;MongoDB manual: TTL indexes&lt;/a&gt; (checked 2026-10-01: 60-second background task, deletion not immediate).&lt;/li&gt;
&lt;li&gt;npm registry, checked 2026-09-28: &lt;code&gt;@modelcontextprotocol/server&lt;/code&gt; 2.2.0, &lt;code&gt;client&lt;/code&gt; 2.2.0, &lt;code&gt;node&lt;/code&gt; 2.1.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/shilpamareddy06-ops/ai-backend-patterns/tree/main/01-stateless-mcp-nestjs" rel="noopener noreferrer"&gt;shilpamareddy06-ops/ai-backend-patterns&lt;/a&gt;, folder &lt;code&gt;01-stateless-mcp-nestjs&lt;/code&gt; (&lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;NOTES.md&lt;/code&gt;, &lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;test/&lt;/code&gt;).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance. I ran the code and tests myself and take responsibility for this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>nestjs</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
