Build a Streamable HTTP MCP Server with TypeScript – Boost Your AI Edge

This guide walks through creating a stateless Streamable HTTP MCP Server using the TypeScript SDK, explaining the protocol’s shift from HTTP + SSE to Streamable HTTP, detailing setup, code structure, endpoint handling, session management, and deployment, and highlighting performance and stability advantages for AI applications.

Ubiquitous Tech
Ubiquitous Tech
Ubiquitous Tech
Build a Streamable HTTP MCP Server with TypeScript – Boost Your AI Edge

With AI projects accelerating toward 2025, many enterprises need efficient ways to integrate AI capabilities into daily workflows. The Model Context Protocol (MCP) has become a key skill for developers, and mastering its newer Streamable HTTP mode offers a more stable and performant alternative to the older HTTP + SSE approach.

The previous two lessons covered the Stdio command mode and the HTTP + SSE mode, both part of the first‑generation MCP protocol released on 2024‑11‑05. SSE requires the server to maintain a long‑lived connection for the entire request lifecycle, which can become a reliability bottleneck under high concurrency.

On 2025‑03‑26, MCP introduced the second‑generation protocol that replaces HTTP + SSE with Streamable HTTP. In this mode, the server can choose to operate statelessly or statefully, reducing resource demands for many scenarios. The Streamable HTTP transport combines several features:

Uses ordinary HTTP POST/GET requests as the base transport.

Optionally upgrades the response to an SSE stream for true streaming when needed.

Supports a decentralized, connection‑free design, enabling stateless operation.

Allows the same /message endpoint to both send requests and receive SSE streams.

Eliminates the dedicated /sse endpoint, consolidating all traffic under a unified protocol layer.

These characteristics give developers better stability, lower latency, and simpler client implementations compared with the traditional HTTP + SSE pattern.

Development Workflow

1. Environment preparation : Install Node.js (preferably via NVM) and ensure the TypeScript‑SDK is available. mkdir typescript-streamablehttp-demo-mcpserver 2. Bootstrap code : Copy the code from the previous HTTP + SSE tutorial as a starting point.

3. Core implementation : Replace the SSE transport with StreamableHTTPServerTransport. The essential code creates the transport, connects it to a Server instance, and handles the request.

// Stateless creation
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);

4. Dependency imports :

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express, { Request, Response } from 'express';
import { CallToolRequestSchema, ListToolsRequestSchema, Tool, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";

5. Tool definition (example: a weather‑lookup tool):

const GET_TIANQI: Tool = {
  name: "get_city_tianqi",
  description: "Return current weather for a given city",
  inputSchema: {
    type: "object",
    properties: { city: { type: "string", description: "City name" } },
    required: ["city"]
  }
};
const SUPPORT_TOOLS = [GET_TIANQI] as const;

6. Server creation helper :

function getServer() {
  const server = new Server({ name: "tianqi-mcp-server", version: "0.0.1" }, {
    capabilities: { tools: {} }
  });
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: SUPPORT_TOOLS }));
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    try {
      switch (request.params.name) {
        case "get_city_tianqi": {
          const { city } = request.params.arguments as { city: string };
          return await handleGetTianqiData(city);
        }
        default:
          return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true };
      }
    } catch (error) {
      return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
    }
  });
  return server;
}

7. Express endpoint for /mcp handling both initialization and regular calls. The handler creates a transport when a session ID is absent and the request matches isInitializeRequest, otherwise it reuses an existing transport from the transports map.

app.post('/mcp', async (req: any, res: any) => {
  console.log('Received MCP request:', req.body);
  try {
    const sessionId = req.headers['mcp-session-id'] as string | undefined;
    let transport: StreamableHTTPServerTransport;
    if (sessionId && transports[sessionId]) {
      transport = transports[sessionId];
    } else if (!sessionId && isInitializeRequest(req.body)) {
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => randomUUID(),
        enableJsonResponse: true,
        onsessioninitialized: (sid) => {
          console.log(`Session initialized with ID: ${sid}`);
          transports[sid] = transport;
        }
      });
      transport.onclose = () => {
        const sid = transport.sessionId;
        if (sid && transports[sid]) {
          console.log(`Transport closed for session ${sid}, removing`);
          delete transports[sid];
        }
      };
      const server = getServer();
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
      return;
    } else {
      res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null });
      return;
    }
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('Error handling MCP request:', error);
    if (!res.headersSent) {
      res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null });
    }
  }
});

8. Additional HTTP verbs ( GET and DELETE) return 405 Method Not Allowed responses, ensuring the server only accepts POST requests for MCP interactions.

app.get('/mcp', (req, res) => {
  res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
});
app.delete('/mcp', (req, res) => {
  res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null }));
});

9. Server startup and graceful shutdown :

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Streamable HTTP Server listening on port ${PORT}`))
  .on('error', err => console.error('Server error', err));
process.on('SIGINT', async () => {
  console.log('Shutting down server...');
  for (const sessionId in transports) {
    try {
      console.log(`Closing transport for session ${sessionId}`);
      await transports[sessionId].close();
      delete transports[sessionId];
    } catch (error) {
      console.error(`Error closing transport for session ${sessionId}:`, error);
    }
  }
  console.log('Server shutdown complete');
  process.exit(0);
});

The final code bundle (shown in the article) can be built with npm run build, producing dist/index.js. Running node dist/index.js launches the stateless Streamable HTTP MCP Server.

Why Choose Streamable HTTP?

The article lists three concrete advantages:

Better stability and higher performance under high concurrency compared with HTTP + SSE.

Shorter, more predictable response times.

Simpler client implementation with less code and lower maintenance cost.

These benefits make Streamable HTTP a compelling choice for remote MCP servers, especially when a stateless design can reduce resource requirements.

Additional Context

The TypeScript SDK follows the philosophy “simple to use but powerful”, allowing developers without deep TypeScript expertise to build complex MCP applications. The SDK currently provides official client support for TypeScript, Python, and (in the future) Java. Spring AI does not yet support this mode, but Alibaba’s Spring AI does.

Summary of MCP Integration Modes

Stdio – suitable for command‑line tools and local integration.

HTTP + SSE – used for remote server communication but requires a persistent connection.

Streamable HTTP – modern, bidirectional HTTP transport that can operate statelessly or statefully.

By following the steps above, developers can quickly prototype a Streamable HTTP MCP Server, test it with tools like the Cherry Studio client, and deploy it via node dist/index.js.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptAIMCPBackend DevelopmentStreamable HTTP
Ubiquitous Tech
Written by

Ubiquitous Tech

A ubiquitous public account for pirate enthusiasts, regularly sharing curated experiences, tech learning, and growth insights. Currently publishing articles on AI RAG customer service, AI MCP technology, and open-source design. Personal free Knowledge Planet: Awakening New World Programmer.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.