Build an SSE Transport MCP Server

Build an MCP server using Server-Sent Events (SSE) transport for HTTP-based real-time communication with AI clients.


title: "Build an SSE Transport MCP Server" description: "Build an MCP server using Server-Sent Events (SSE) transport for HTTP-based real-time communication with AI clients." order: 10 level: "advanced" duration: "35 min" keywords:

  • MCP SSE
  • MCP Server-Sent Events
  • MCP HTTP transport
  • MCP SSE server
  • MCP remote server date: "2026-04-01"

Quick Summary

Build an MCP server that uses Server-Sent Events (SSE) transport instead of stdio. This enables remote access over HTTP, letting multiple clients connect to a single server instance. You will set up an Express-based SSE server with both mcp-framework and the official SDK.

Why SSE Transport?

The default stdio transport is great for local development where each client spawns its own server process. But for production deployments, you often need:

  • Remote access -- Connect from any machine, not just locally
  • Shared state -- Multiple clients share one server instance
  • Web integration -- Embed MCP in web applications
  • Scalability -- Deploy behind load balancers
Featurestdio TransportSSE Transport
Connection typeLocal process pipesHTTP/HTTPS over network
Client countOne client per processMultiple concurrent clients
DeploymentClient spawns serverServer runs independently
State sharingNone (isolated)Shared across clients
Setup complexityMinimalRequires HTTP server
Best forLocal dev, Claude DesktopProduction, web apps
Server-Sent Events (SSE)

A web standard for pushing updates from server to client over HTTP. Unlike WebSockets, SSE is unidirectional (server to client) and uses standard HTTP. MCP uses SSE for server-to-client messages and POST requests for client-to-server messages.

Project Setup

1

Create the project

npx mcp-framework create sse-server
cd sse-server
npm install express cors
npm install -D @types/express @types/cors
2

Project structure

sse-server
src
tools
EchoTool.ts
TimeTool.ts
index.ts
sse-transport.ts
package.json
tsconfig.json

Building with the Official SDK

The official SDK has built-in SSE transport support. Create src/index.ts:

import express from "express";
import cors from "cors";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";

const app = express();
app.use(cors());
app.use(express.json());

// Create the MCP server
const server = new McpServer({
  name: "sse-server",
  version: "1.0.0",
});

// Register tools
server.tool(
  "echo",
  "Echo back the provided message with metadata",
  { message: z.string() },
  async ({ message }) => ({
    content: [{
      type: "text" as const,
      text: JSON.stringify({
        echo: message,
        timestamp: new Date().toISOString(),
        transport: "sse",
      }, null, 2),
    }],
  })
);

server.tool(
  "server_time",
  "Get the current server time in various formats",
  {
    timezone: z.string().optional().describe("IANA timezone (default: UTC)"),
  },
  async ({ timezone }) => {
    const tz = timezone || "UTC";
    const now = new Date();
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify({
          iso: now.toISOString(),
          local: now.toLocaleString("en-US", { timeZone: tz }),
          unix: Math.floor(now.getTime() / 1000),
          timezone: tz,
        }, null, 2),
      }],
    };
  }
);

// SSE endpoint -- clients connect here
let transport: SSEServerTransport | null = null;

app.get("/sse", async (req, res) => {
  console.log("New SSE connection");
  transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});

// Message endpoint -- clients send messages here
app.post("/messages", async (req, res) => {
  if (!transport) {
    res.status(503).json({ error: "No active SSE connection" });
    return;
  }
  await transport.handlePostMessage(req, res);
});

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok", transport: "sse" });
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`MCP SSE server running on http://localhost:${PORT}`);
  console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
  console.log(`Messages endpoint: http://localhost:${PORT}/messages`);
});
Two Endpoints

SSE transport requires two endpoints: a GET endpoint for the SSE stream (server-to-client) and a POST endpoint for client-to-server messages. The client connects to the SSE endpoint first, then sends tool calls to the messages endpoint.

Building with mcp-framework

mcp-framework also supports SSE transport. Update src/index.ts:

import { MCPServer } from "mcp-framework";

const server = new MCPServer({
  transport: {
    type: "sse",
    options: {
      port: 3001,
      endpoint: "/sse",
      messageEndpoint: "/messages",
    },
  },
});

server.start();

With mcp-framework, the tools are auto-discovered from src/tools/. Create a simple tool in src/tools/EchoTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";

class EchoTool extends MCPTool<typeof inputSchema> {
  name = "echo";
  description = "Echo back a message with server metadata";

  schema = {
    message: {
      type: z.string().min(1),
      description: "Message to echo",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    return JSON.stringify({
      echo: input.message,
      timestamp: new Date().toISOString(),
      transport: "sse",
    }, null, 2);
  }
}

const inputSchema = z.object({
  message: z.string().min(1),
});

export default EchoTool;
Keep SSE Connections Alive

SSE connections can be dropped by proxies and load balancers. Send periodic heartbeat comments (lines starting with :) to keep the connection alive. Most MCP SDK implementations handle this automatically.

Testing the SSE Server

1

Build and start the server

npm run build
node dist/index.js

The server starts and listens on port 3001.

2

Test with the MCP Inspector

npx @modelcontextprotocol/inspector --transport sse http://localhost:3001/sse
3

Test with curl

Open two terminals. In the first, connect to SSE:

curl -N http://localhost:3001/sse

In the second, send a message (using the session ID from the SSE output):

curl -X POST http://localhost:3001/messages \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Adding Authentication

For production SSE servers, add authentication middleware:

function authMiddleware(req: express.Request, res: express.Response, next: express.NextFunction) {
  const token = req.headers.authorization?.replace("Bearer ", "");

  if (!token || token !== process.env.MCP_AUTH_TOKEN) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }

  next();
}

app.get("/sse", authMiddleware, async (req, res) => {
  // ... SSE connection handler
});

app.post("/messages", authMiddleware, async (req, res) => {
  // ... message handler
});
Always Secure Remote Servers

SSE servers are accessible over the network. Always add authentication, use HTTPS in production, and implement rate limiting. An unauthenticated MCP server is a security risk.

Connecting from Claude Desktop

For SSE servers, the Claude Desktop config differs:

{
  "mcpServers": {
    "remote-server": {
      "url": "http://localhost:3001/sse",
      "transport": "sse"
    }
  }
}

Multi-Client Architecture

One major advantage of SSE is supporting multiple clients:

const sessions = new Map<string, SSEServerTransport>();

app.get("/sse", async (req, res) => {
  const sessionId = randomUUID();
  const transport = new SSEServerTransport(`/messages/${sessionId}`, res);
  sessions.set(sessionId, transport);

  res.on("close", () => {
    sessions.delete(sessionId);
    console.log(`Session ${sessionId} disconnected`);
  });

  await server.connect(transport);
});

app.post("/messages/:sessionId", async (req, res) => {
  const transport = sessions.get(req.params.sessionId);
  if (!transport) {
    res.status(404).json({ error: "Session not found" });
    return;
  }
  await transport.handlePostMessage(req, res);
});

Frequently Asked Questions