10 min read

How to Build an MCP Server in Node.js

The 2026-07-28 MCP specification removes protocol-level sessions, shifting security and state validation responsibilities to server developers. This guide covers building a working Node.js MCP server with the current SDK, plus critical changes to implement before the stateless spec finalizes.

Featured image for "How to Build an MCP Server in Node.js"

The MCP TypeScript SDK just split into six packages, and if you start a new server on the v1 import paths today, you’ll rewrite them before July 28. The 2026-07-28 specification release candidate removes protocol-level sessions entirely, which sounds like a simplification — until you realize it shifts every security invariant that sessions used to guarantee into your application code. Building an MCP server in Node.js has never been easier to get running, and it has never required more careful thinking about what happens once it’s live.

Here’s the practical reality: the server code itself is the cheap part. The protocol gives you a clean abstraction for exposing tools, resources, and prompts. But authentication, state validation, and header integrity checks are now your responsibility, not the protocol’s. This guide walks through building a working server with the current SDK, then maps exactly what changes when the stateless spec lands — so you’re not caught mid-migration.

The SDK Landscape: v1 Production vs. v2 Alpha

You have two SDK lines to choose from right now, and the choice isn’t subtle. The production v1.x line ships as @modelcontextprotocol/sdk, and it’s what every tutorial and most production servers use today. The v2.0.0-alpha.2, published April 1, 2026, reorganizes the monolithic package into six scoped modules: @modelcontextprotocol/server, @modelcontextprotocol/client, @modelcontextprotocol/node, @modelcontextprotocol/express, @modelcontextprotocol/hono, and @modelcontextprotocol/fastify.

The v2 alpha carries an explicit warning: expect breaking changes until it stabilizes. That’s not marketing hedging — the milestone tracker shows a five-PR ship plan with the stateless protocol, subscription routing, and multi-round-trip requests still in progress. If you’re building a server you need to ship next month, use v1. If you’re prototyping against the July spec, install the alpha and accept that import paths and APIs will shift under you.

Node.js 18 or higher is required, with Node 20+ recommended for new projects. The SDK is ESM-only, so set "type": "module" in your package.json or use .mjs extensions.

Building a Minimal Server: Tools, Resources, and Transports

A minimal MCP server follows one consistent pattern: instantiate McpServer with a name and version, register your capabilities, and connect a transport. MCP servers expose three primitives: tools (callable functions), resources (read-only data accessible via URI), and prompts (reusable prompt templates). Tools are where you’ll spend most of your time.

Here’s a working server with one tool:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

server.registerTool(
  "search_listings",
  {
    description: "Search marketplace listings by keyword",
    inputSchema: z.object({
      query: z.string(),
      category: z.enum(["services", "rentals", "products"]).optional(),
    }),
  },
  async ({ query, category }) => ({
    content: [{ type: "text", text: JSON.stringify(await search(query, category)) }],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

Tool input schemas use Zod. The SDK validates inputs against the schema before invoking your handler — bad inputs never reach your code. Handlers must return a CallToolResult with a content array directly, not wrapped in a Promise. That’s a common footgun: the SDK handles the async wrapping, so return the result object, not an awaited one.

For transport, you have three options. Stdio runs as a local subprocess with zero network surface — perfect for personal tools and Claude Desktop configs. Streamable HTTP is the current standard for remote servers and the stateless-friendly path. Legacy SSE is deprecated in the 2026-07-28 spec and should only be used if you’re maintaining an existing deployment.

The Stateless Transition: What Actually Changes for Your Code

The 2026-07-28 specification, release candidate locked May 21 and final July 28, makes the protocol stateless by removing the initialize/initialized handshake and the Mcp-Session-Id header. Any server instance can now handle any request. On paper, this is a win: no sticky sessions, no shared session stores, no deep packet inspection at the gateway.

In practice, what I call the Stateless Backchannel pattern means the complexity doesn’t disappear — it moves. Protocol version, client info, and client capabilities now travel in _meta on every request instead of being exchanged once. A new server/discover method replaces the handshake for capability negotiation. Your server must implement it, or clients can’t negotiate with you at all.

The Streamable HTTP transport now requires Mcp-Method and Mcp-Name headers for routing. This lets load balancers route on headers without parsing the JSON-RPC body, which is genuinely useful. But servers must reject requests where these headers disagree with the body — that’s not optional, it’s a security invariant. A mismatch between the routing header and the actual method in the body is exactly the ambiguity that request smuggling attacks exploit. You have to write that validation yourself.

If your server needs state across calls, you can’t rely on a session anymore. You mint an explicit handle — a basket_id, a browser_id — from a tool, and the model passes it back as an ordinary argument. The state becomes a visible tool input rather than something hidden in transport metadata. That’s better for debugging. It’s also now entirely your responsibility to validate.

The SSE Contradiction: Deprecated Transport, Required Backchannel

Here’s the tension that catches most developers: SSE is deprecated as a transport, yet required for core interactive capabilities. The deployment guides label SSE as deprecated and recommend Streamable HTTP as the current standard. The spec promotes a stateless JSON-over-HTTP core. Sounds clean.

Then you try to use elicitation or sampling from a tool handler. Server-to-client requests via ctx.mcpReq.elicitInput() or ctx.mcpReq.requestSampling() require an SSE response stream and throw a clear error when enableJsonResponse: true disables SSE. You can’t have interactive tool flows without the backchannel. The JSON-only mode that makes stateless deployment simple breaks the moment your tool needs to ask the user a clarifying question.

The NodeStreamableHTTPServerTransport does support an opt-in keepAliveInterval option that writes periodic SSE keepalive comments to prevent proxies and load balancers from dropping idle connections during long-running requests. If you’re running behind AWS ALB or Cloudflare (which have default idle timeouts of 60 seconds), set this to something like 30000 milliseconds.

The practical takeaway: if your tools are purely request-response — call a function, return a result — you can use enableJsonResponse: true and deploy on any serverless platform. If any tool needs to elicit input or request sampling from the model, you need the SSE stream, which means a persistent connection, which means you’re back to caring about connection lifecycle management.

Security After Sessions: Your New Responsibilities

The stateless spec eliminates session hijacking via Mcp-Session-Id — that’s a real improvement. But Akamai’s analysis warns that predictable client-provided tracking IDs and the new _meta object introduce risks of workflow hijacking, cross-tenant data access, and secret leakage via HTTP headers like x-mcp-header. The protocol used to enforce these invariants. Now you do.

Three things you must handle in your server code that the protocol no longer handles for you:

  1. Header-body integrity checks. Validate that Mcp-Method and Mcp-Name headers agree with the JSON-RPC body on every call. The DEV Community readiness checklist calls this out explicitly — it’s a per-request cost that didn’t exist in the session-based model.

  2. State handle validation. If you mint state handles (task IDs, browser sessions, shopping carts), you must verify their integrity on every use. An attacker who can guess or tamper with a tracking ID can hijack another user’s workflow or access cross-tenant data. Use HMAC-signed handles or opaque server-side tokens, not sequential integers.

  3. _meta hygiene. The _meta object carries client info and capabilities on every request. Don’t log it verbatim, don’t reflect it into responses, and don’t trust any field in it without validation. If developers accidentally map sensitive inputs like API keys into _meta, those secrets become visible to every proxy and logging system along the request path.

This is why building a production-grade MCP server carries hidden costs that most teams underestimate. The protocol code is cheap. The security invariants that moved from protocol to application code are not.

Authentication: EMA Changes the Game for Enterprise

For any MCP server exposed beyond localhost, OAuth 2.1 with Resource Indicators (RFC 8707) is the 2026 mandatory auth layer. But the bigger news is Enterprise-Managed Authorization (EMA), promoted to stable on June 18, 2026. EMA enables zero-touch SSO for MCP servers via enterprise identity providers such as Okta using Cross App Access.

The developer experience: sign in through your company’s IdP once, and every EMA-enabled MCP server in your catalog inherits that trust without separate OAuth dances per vendor. IT defines which servers appear in the catalog; users pick from an allowlist rather than discovering community servers on the open internet. Initial client support covers Claude, Claude Code, and Visual Studio Code.

If you’re building an internal MCP server for an organization that already runs Okta or a similar IdP, EMA is the path to production adoption. Without it, you’re asking every engineer to paste personal API keys into local agent configs — which is exactly the pattern security teams reject during review. As we’ve covered in MCP security risks for engineering teams, most deployments still lack mandatory authentication, and EMA is the spec-backed fix.

Deployment and Hosting Costs

Where your server runs determines your auth shape, your cost profile, and your operational burden. A stdio server distributed as an npm package has zero hosting cost and zero ops surface. A remote HTTP server needs TLS, auth, monitoring, and a process that stays running.

Deployment ModelMonthly CostBest ForKey Constraint
Stdio (npm package)$0Personal tools, local devNo network, no sharing
Self-hosted VPS$40–$120/monthInternal team serversYou manage everything
Cloudflare MCP Gateway$0.50/million requestsHigh-scale, zero-opsBuilt-in auth, audit, DDoS protection

The Cloudflare MCP Gateway is compelling if you’re already in the Cloudflare ecosystem — built-in auth, audit logging, and DDoS protection at a usage-based price that’s hard to beat for variable-traffic workloads. Self-hosting on a VPS gives you full control but you own the TLS certificates, the auth layer, the monitoring, and the on-call rotation.

For stateless servers using enableJsonResponse: true, serverless platforms like Vercel or Cloudflare Workers work well. For servers that need the SSE backchannel for elicitation or sampling, you need a persistent process — Railway, Render, or a VPS with keepalive configured.

Migration Planning: The Tasks Extension and v1-to-v2

The 2026-07-28 spec moves the Tasks feature from core protocol to an extension, requiring migration for servers that used the experimental Tasks API in the 2025-11-25 spec due to wire format changes. If you shipped against the experimental Tasks API, this isn’t optional — the wire format changed, and your existing clients will break on July 28.

The v1-to-v2 SDK migration is separate but overlapping. Import paths change from @modelcontextprotocol/sdk/server/mcp.js to @modelcontextprotocol/server. The API surface is being reorganized to align with the stateless spec. The migration guide in the v2 package covers the mechanical changes, but the conceptual shift — from relying on protocol-guaranteed session security to implementing your own state validation — is where the real work lives.

Treat the 2026-07-28 spec and v2 SDK alpha as a hard migration boundary requiring immediate prototyping, not a routine upgrade. Teams that defer will face broken elicitation flows, security gaps in state handling, and irreconcilable v1/v2 divergence. The ten-week validation window between the release candidate (May 21) and the final spec (July 28) is for exactly this: get your server running against the RC, find the breaks, fix them before the final ships.

The Decision Framework

Here’s how to think about building an MCP server in Node.js right now:

If you need a local tool for personal use or your team’s dev workflows: Use v1 SDK, stdio transport, zero hosting. Ship it this week. You’ll need to migrate imports when you move to v2, but the core pattern — McpServer, registerTool, connect(transport) — stays the same.

If you need a remote server with request-response tools only: Use v1 SDK, Streamable HTTP transport, enableJsonResponse: true. Deploy on any serverless platform. Plan your v2 migration around the header validation and _meta handling requirements.

If you need interactive tool flows (elicitation, sampling): Use v1 SDK, Streamable HTTP transport with SSE backchannel. You need a persistent process and keepAliveInterval configured. Start prototyping against the v2 alpha now, because the ctx.mcpReq API surface is where the biggest changes land.

If you’re building for enterprise deployment: Add EMA from day one. The zero-touch OAuth flow is stable and supported in the three clients that matter most. Don’t build a custom auth layer when the spec now provides one. And as we’ve detailed in how MCP changes SaaS development workflows, budget for auth, audit, and safety infrastructure — they consume the majority of production deployment budgets.

The server code is the easy part. The protocol gives you a clean, well-documented abstraction for exposing capabilities. The hard part is everything the protocol used to do for you that it now expects you to do yourself. Start there.