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"
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
| Feature | stdio Transport | SSE Transport |
|---|---|---|
| Connection type | Local process pipes | HTTP/HTTPS over network |
| Client count | One client per process | Multiple concurrent clients |
| Deployment | Client spawns server | Server runs independently |
| State sharing | None (isolated) | Shared across clients |
| Setup complexity | Minimal | Requires HTTP server |
| Best for | Local dev, Claude Desktop | Production, web apps |
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
Create the project
npx mcp-framework create sse-server
cd sse-server
npm install express cors
npm install -D @types/express @types/cors
Project structure
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`);
});
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;
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
Build and start the server
npm run build
node dist/index.js
The server starts and listens on port 3001.
Test with the MCP Inspector
npx @modelcontextprotocol/inspector --transport sse http://localhost:3001/sse
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
});
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);
});