Building MCP Servers with Express.js
Complete guide to building MCP servers with Express.js, covering SSE transport integration, middleware composition, route organization, authentication layers, tool registration patterns, session management, and production Express-based MCP server architecture.
Building MCP Servers with Express.js
Overview
The MCP SDK ships with built-in stdio and SSE transports, but embedding an MCP server inside an Express.js application gives you the entire Express ecosystem — middleware, routing, error handling, and the ability to serve both MCP clients and regular HTTP clients from the same process. I have built MCP servers that sit alongside existing REST APIs, sharing database connections, authentication middleware, and deployment infrastructure. Express is the glue that makes this practical.
Prerequisites
- Node.js 16 or later
- Express.js 4.x (
npm install express) @modelcontextprotocol/sdkpackage installed- Basic understanding of Express middleware and routing
- Familiarity with Server-Sent Events (SSE)
- Understanding of MCP protocol concepts (tools, resources, prompts)
Express and MCP Integration Architecture
How SSE Transport Works with Express
The MCP SSE transport requires two HTTP endpoints: a GET endpoint for the SSE event stream and a POST endpoint for client messages. Express handles both naturally, but the SSE connection has special requirements — no response buffering, no compression, and long-lived connections that outlast typical request timeouts.
// basic-mcp-express.js
var express = require("express");
var McpServer = require("@modelcontextprotocol/sdk/server/mcp.js").McpServer;
var SSEServerTransport = require("@modelcontextprotocol/sdk/server/sse.js").SSEServerTransport;
var app = express();
app.use(express.json());
var mcpServer = new McpServer({
name: "express-mcp",
version: "1.0.0"
});
// Register a simple tool
mcpServer.tool("greet", "Greet a user by name", {
name: { type: "string", description: "Name to greet" }
}, function(params) {
return {
content: [{ type: "text", text: "Hello, " + params.name + "! Welcome to the Express MCP server." }]
};
});
// SSE connection endpoint
var transports = {};
app.get("/mcp/sse", function(req, res) {
// SSE headers
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
var transport = new SSEServerTransport("/mcp/messages", res);
var sessionId = transport.sessionId;
transports[sessionId] = transport;
console.log("New MCP connection: " + sessionId);
transport.onclose = function() {
delete transports[sessionId];
console.log("MCP connection closed: " + sessionId);
};
mcpServer.connect(transport);
});
// Message handling endpoint
app.post("/mcp/messages", function(req, res) {
var sessionId = req.query.sessionId;
var transport = transports[sessionId];
if (!transport) {
res.status(400).json({ error: "Unknown session: " + sessionId });
return;
}
transport.handlePostMessage(req, res);
});
app.listen(3100, function() {
console.log("Express MCP server running on port 3100");
});
Middleware Integration
Applying Express Middleware to MCP Routes
The power of Express comes from middleware. You can wrap MCP endpoints with the same authentication, logging, rate limiting, and CORS middleware you use for REST APIs.
// middleware/auth.js
var jwt = require("jsonwebtoken");
function mcpAuth(options) {
var secret = options.secret;
var allowAnonymous = options.allowAnonymous || false;
return function(req, res, next) {
var token = req.get("Authorization");
if (!token && allowAnonymous) {
req.mcpUser = { role: "anonymous", permissions: ["read"] };
return next();
}
if (!token || !token.startsWith("Bearer ")) {
res.status(401).json({ error: "Missing or invalid Authorization header" });
return;
}
try {
var decoded = jwt.verify(token.replace("Bearer ", ""), secret);
req.mcpUser = {
id: decoded.sub,
role: decoded.role || "user",
permissions: decoded.permissions || ["read"]
};
next();
} catch (err) {
res.status(401).json({ error: "Invalid token: " + err.message });
}
};
}
module.exports = mcpAuth;
// middleware/rate-limit.js
function mcpRateLimit(options) {
var windowMs = options.windowMs || 60000;
var maxRequests = options.maxRequests || 100;
var clients = {};
// Clean up expired entries periodically
setInterval(function() {
var now = Date.now();
Object.keys(clients).forEach(function(key) {
if (now - clients[key].windowStart > windowMs) {
delete clients[key];
}
});
}, windowMs);
return function(req, res, next) {
var clientKey = req.ip + ":" + (req.mcpUser ? req.mcpUser.id : "anon");
var now = Date.now();
if (!clients[clientKey] || now - clients[clientKey].windowStart > windowMs) {
clients[clientKey] = { count: 0, windowStart: now };
}
clients[clientKey].count++;
res.setHeader("X-RateLimit-Limit", maxRequests);
res.setHeader("X-RateLimit-Remaining", Math.max(0, maxRequests - clients[clientKey].count));
if (clients[clientKey].count > maxRequests) {
res.status(429).json({
error: "Rate limit exceeded",
retryAfter: Math.ceil((clients[clientKey].windowStart + windowMs - now) / 1000)
});
return;
}
next();
};
}
module.exports = mcpRateLimit;
// middleware/request-logger.js
function mcpRequestLogger(logger) {
return function(req, res, next) {
var start = Date.now();
res.on("finish", function() {
var duration = Date.now() - start;
var logData = {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: duration + "ms",
ip: req.ip,
userAgent: req.get("user-agent")
};
if (req.mcpUser) {
logData.userId = req.mcpUser.id;
logData.role = req.mcpUser.role;
}
if (res.statusCode >= 400) {
logger.warn("MCP request failed", logData);
} else {
logger.info("MCP request", logData);
}
});
next();
};
}
module.exports = mcpRequestLogger;
Composing Middleware Stack
// routes/mcp.js
var express = require("express");
var mcpAuth = require("../middleware/auth");
var mcpRateLimit = require("../middleware/rate-limit");
var mcpRequestLogger = require("../middleware/request-logger");
var logger = require("../logger");
function createMcpRouter(mcpServer, options) {
var router = express.Router();
var transports = {};
// Apply middleware stack
router.use(mcpRequestLogger(logger));
router.use(mcpAuth({
secret: options.jwtSecret,
allowAnonymous: options.allowAnonymous || false
}));
router.use(mcpRateLimit({
windowMs: 60000,
maxRequests: options.rateLimit || 100
}));
// CORS for browser-based MCP clients
router.use(function(req, res, next) {
var allowedOrigins = options.corsOrigins || [];
var origin = req.get("Origin");
if (origin && allowedOrigins.indexOf(origin) > -1) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
}
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
});
// SSE endpoint
router.get("/sse", function(req, res) {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Disable Nginx buffering
var transport = new (require("@modelcontextprotocol/sdk/server/sse.js").SSEServerTransport)(
options.messagesPath || "/mcp/messages",
res
);
var sessionId = transport.sessionId;
transports[sessionId] = {
transport: transport,
user: req.mcpUser,
connectedAt: new Date(),
ip: req.ip
};
logger.info("MCP SSE connection opened", {
sessionId: sessionId,
userId: req.mcpUser ? req.mcpUser.id : "anonymous",
ip: req.ip
});
transport.onclose = function() {
delete transports[sessionId];
logger.info("MCP SSE connection closed", { sessionId: sessionId });
};
mcpServer.connect(transport);
});
// Message endpoint
router.post("/messages", express.json(), function(req, res) {
var sessionId = req.query.sessionId;
var session = transports[sessionId];
if (!session) {
res.status(400).json({ error: "Invalid session" });
return;
}
session.transport.handlePostMessage(req, res);
});
// Session management endpoint
router.get("/sessions", function(req, res) {
if (!req.mcpUser || req.mcpUser.role !== "admin") {
res.status(403).json({ error: "Admin access required" });
return;
}
var sessions = Object.keys(transports).map(function(id) {
var s = transports[id];
return {
sessionId: id,
userId: s.user ? s.user.id : "anonymous",
connectedAt: s.connectedAt,
ip: s.ip
};
});
res.json({ sessions: sessions, total: sessions.length });
});
return router;
}
module.exports = createMcpRouter;
Route Organization
Mounting MCP Alongside Existing Routes
// app.js
var express = require("express");
var McpServer = require("@modelcontextprotocol/sdk/server/mcp.js").McpServer;
var createMcpRouter = require("./routes/mcp");
var apiRouter = require("./routes/api");
var app = express();
// Regular API routes
app.use("/api", apiRouter);
// Health check
app.get("/health", function(req, res) {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// Create and configure MCP server
var mcpServer = new McpServer({
name: "my-app-mcp",
version: "2.0.0"
});
// Register tools (see next section)
registerTools(mcpServer);
// Mount MCP router at /mcp
app.use("/mcp", createMcpRouter(mcpServer, {
jwtSecret: process.env.JWT_SECRET,
corsOrigins: ["https://app.example.com"],
rateLimit: 200,
messagesPath: "/mcp/messages",
allowAnonymous: false
}));
// Error handler
app.use(function(err, req, res, next) {
console.error("Server error:", err.message);
res.status(500).json({ error: "Internal server error" });
});
app.listen(process.env.PORT || 3100, function() {
console.log("Server started on port " + (process.env.PORT || 3100));
});
Modular Tool Registration
// tools/index.js
var registerDatabaseTools = require("./database");
var registerFileTools = require("./files");
var registerApiTools = require("./api-client");
function registerTools(mcpServer, dependencies) {
registerDatabaseTools(mcpServer, dependencies.db);
registerFileTools(mcpServer, dependencies.fileRoot);
registerApiTools(mcpServer, dependencies.httpClient);
}
module.exports = registerTools;
// tools/database.js
function registerDatabaseTools(mcpServer, pool) {
mcpServer.tool(
"query",
"Execute a read-only SQL query",
{
sql: { type: "string", description: "SQL SELECT query to execute" },
params: { type: "array", description: "Query parameters", items: { type: "string" } }
},
function(args) {
var sql = args.sql.trim();
// Safety: only allow SELECT queries
if (!sql.toUpperCase().startsWith("SELECT")) {
return {
content: [{ type: "text", text: "Error: Only SELECT queries are allowed" }],
isError: true
};
}
return pool.query(sql, args.params || []).then(function(result) {
return {
content: [{
type: "text",
text: JSON.stringify({
columns: result.fields.map(function(f) { return f.name; }),
rows: result.rows,
rowCount: result.rowCount
}, null, 2)
}]
};
}).catch(function(err) {
return {
content: [{ type: "text", text: "Query error: " + err.message }],
isError: true
};
});
}
);
mcpServer.tool(
"list_tables",
"List all tables in the database",
{},
function() {
var sql = "SELECT table_name, table_type FROM information_schema.tables " +
"WHERE table_schema = 'public' ORDER BY table_name";
return pool.query(sql).then(function(result) {
var tables = result.rows.map(function(r) {
return r.table_name + " (" + r.table_type + ")";
});
return {
content: [{ type: "text", text: "Tables:\n" + tables.join("\n") }]
};
});
}
);
mcpServer.tool(
"describe_table",
"Show column details for a table",
{
table: { type: "string", description: "Table name" }
},
function(args) {
var sql = "SELECT column_name, data_type, is_nullable, column_default " +
"FROM information_schema.columns " +
"WHERE table_schema = 'public' AND table_name = $1 " +
"ORDER BY ordinal_position";
return pool.query(sql, [args.table]).then(function(result) {
if (result.rows.length === 0) {
return {
content: [{ type: "text", text: "Table '" + args.table + "' not found" }],
isError: true
};
}
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}]
};
});
}
);
}
module.exports = registerDatabaseTools;
// tools/files.js
var fs = require("fs");
var path = require("path");
function registerFileTools(mcpServer, rootDir) {
mcpServer.tool(
"read_file",
"Read a file from the project directory",
{
filePath: { type: "string", description: "Relative path to the file" }
},
function(args) {
var fullPath = path.resolve(rootDir, args.filePath);
// Security: prevent path traversal
if (!fullPath.startsWith(path.resolve(rootDir))) {
return {
content: [{ type: "text", text: "Error: Path traversal not allowed" }],
isError: true
};
}
try {
var content = fs.readFileSync(fullPath, "utf8");
var stats = fs.statSync(fullPath);
return {
content: [{
type: "text",
text: "File: " + args.filePath + " (" + stats.size + " bytes)\n\n" + content
}]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error reading file: " + err.message }],
isError: true
};
}
}
);
mcpServer.tool(
"list_files",
"List files in a directory",
{
dirPath: { type: "string", description: "Relative directory path" }
},
function(args) {
var fullPath = path.resolve(rootDir, args.dirPath || ".");
if (!fullPath.startsWith(path.resolve(rootDir))) {
return {
content: [{ type: "text", text: "Error: Path traversal not allowed" }],
isError: true
};
}
try {
var entries = fs.readdirSync(fullPath, { withFileTypes: true });
var listing = entries.map(function(entry) {
var type = entry.isDirectory() ? "[DIR]" : "[FILE]";
var size = "";
if (entry.isFile()) {
var stats = fs.statSync(path.join(fullPath, entry.name));
size = " (" + stats.size + " bytes)";
}
return type + " " + entry.name + size;
});
return {
content: [{ type: "text", text: listing.join("\n") }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error: " + err.message }],
isError: true
};
}
}
);
}
module.exports = registerFileTools;
Session Management
Per-Session State
Each SSE connection creates a separate MCP session. When tools need to track state across invocations within the same session (conversation context, temporary data, user preferences), you need a session store.
// session-store.js
function SessionStore() {
this.sessions = new Map();
}
SessionStore.prototype.create = function(sessionId, userData) {
var session = {
id: sessionId,
user: userData,
data: {},
createdAt: new Date(),
lastActivity: new Date()
};
this.sessions.set(sessionId, session);
return session;
};
SessionStore.prototype.get = function(sessionId) {
var session = this.sessions.get(sessionId);
if (session) {
session.lastActivity = new Date();
}
return session || null;
};
SessionStore.prototype.set = function(sessionId, key, value) {
var session = this.sessions.get(sessionId);
if (session) {
session.data[key] = value;
session.lastActivity = new Date();
}
};
SessionStore.prototype.getData = function(sessionId, key) {
var session = this.sessions.get(sessionId);
if (session && session.data.hasOwnProperty(key)) {
return session.data[key];
}
return undefined;
};
SessionStore.prototype.destroy = function(sessionId) {
this.sessions.delete(sessionId);
};
SessionStore.prototype.cleanup = function(maxAgeMs) {
var now = Date.now();
var removed = 0;
var self = this;
this.sessions.forEach(function(session, id) {
if (now - session.lastActivity.getTime() > maxAgeMs) {
self.sessions.delete(id);
removed++;
}
});
return removed;
};
module.exports = SessionStore;
Session-Aware Tools
// tools/session-tools.js
function registerSessionTools(mcpServer, sessionStore) {
mcpServer.tool(
"set_preference",
"Set a user preference for this session",
{
key: { type: "string", description: "Preference key" },
value: { type: "string", description: "Preference value" }
},
function(args, context) {
var sessionId = context.sessionId;
sessionStore.set(sessionId, "pref:" + args.key, args.value);
return {
content: [{ type: "text", text: "Preference set: " + args.key + " = " + args.value }]
};
}
);
mcpServer.tool(
"get_preferences",
"Get all preferences for this session",
{},
function(args, context) {
var session = sessionStore.get(context.sessionId);
if (!session) {
return {
content: [{ type: "text", text: "No session found" }],
isError: true
};
}
var prefs = {};
Object.keys(session.data).forEach(function(key) {
if (key.startsWith("pref:")) {
prefs[key.replace("pref:", "")] = session.data[key];
}
});
return {
content: [{
type: "text",
text: Object.keys(prefs).length > 0
? JSON.stringify(prefs, null, 2)
: "No preferences set"
}]
};
}
);
mcpServer.tool(
"remember",
"Store a note in the current session",
{
note: { type: "string", description: "Note to remember" }
},
function(args, context) {
var sessionId = context.sessionId;
var notes = sessionStore.getData(sessionId, "notes") || [];
notes.push({
text: args.note,
timestamp: new Date().toISOString()
});
sessionStore.set(sessionId, "notes", notes);
return {
content: [{ type: "text", text: "Remembered. Total notes: " + notes.length }]
};
}
);
}
module.exports = registerSessionTools;
Error Handling Patterns
Express Error Middleware for MCP
// middleware/error-handler.js
var logger = require("../logger");
function mcpErrorHandler(err, req, res, next) {
var statusCode = err.statusCode || 500;
var errorId = Date.now().toString(36);
logger.error("MCP request error", {
errorId: errorId,
path: req.path,
method: req.method,
statusCode: statusCode,
message: err.message,
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
userId: req.mcpUser ? req.mcpUser.id : undefined
});
// Don't leak internal errors to clients
if (statusCode === 500) {
res.status(500).json({
error: "Internal server error",
errorId: errorId,
message: "An unexpected error occurred. Reference ID: " + errorId
});
} else {
res.status(statusCode).json({
error: err.message,
errorId: errorId
});
}
}
// Async error wrapper for route handlers
function asyncHandler(fn) {
return function(req, res, next) {
var result = fn(req, res, next);
if (result && typeof result.catch === "function") {
result.catch(next);
}
};
}
module.exports = {
mcpErrorHandler: mcpErrorHandler,
asyncHandler: asyncHandler
};
Tool-Level Error Boundaries
// tools/safe-tool.js
function safeTool(mcpServer, name, description, schema, handler) {
mcpServer.tool(name, description, schema, function(params, context) {
try {
var result = handler(params, context);
if (result && typeof result.then === "function") {
return result.catch(function(err) {
console.error("Tool '" + name + "' failed:", err.message);
return {
content: [{
type: "text",
text: "Tool error: " + err.message + "\n\nPlease try again or rephrase your request."
}],
isError: true
};
});
}
return result;
} catch (err) {
console.error("Tool '" + name + "' threw:", err.message);
return {
content: [{
type: "text",
text: "Tool error: " + err.message
}],
isError: true
};
}
});
}
module.exports = safeTool;
Complete Working Example
A production Express.js application serving both REST API and MCP endpoints with full middleware stack.
// server.js - Complete Express MCP Server
var express = require("express");
var http = require("http");
var McpServer = require("@modelcontextprotocol/sdk/server/mcp.js").McpServer;
var SSEServerTransport = require("@modelcontextprotocol/sdk/server/sse.js").SSEServerTransport;
var Pool = require("pg").Pool;
// Configuration
var config = {
port: process.env.PORT || 3100,
jwtSecret: process.env.JWT_SECRET || "dev-secret-change-in-prod",
databaseUrl: process.env.DATABASE_URL,
corsOrigins: (process.env.CORS_ORIGINS || "http://localhost:3000").split(","),
rateLimit: parseInt(process.env.RATE_LIMIT, 10) || 100
};
// Database pool
var pool = config.databaseUrl ? new Pool({ connectionString: config.databaseUrl }) : null;
// Express app
var app = express();
app.use(express.json());
// Create MCP server
var mcpServer = new McpServer({
name: "express-fullstack-mcp",
version: "1.0.0"
});
// Session tracking
var sessions = {};
// ---- Tool Registration ----
mcpServer.tool("echo", "Echo a message back", {
message: { type: "string", description: "Message to echo" }
}, function(params) {
return {
content: [{ type: "text", text: "Echo: " + params.message }]
};
});
mcpServer.tool("server_info", "Get server information", {}, function() {
return {
content: [{
type: "text",
text: JSON.stringify({
name: "express-fullstack-mcp",
version: "1.0.0",
uptime: Math.floor(process.uptime()) + "s",
activeSessions: Object.keys(sessions).length,
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB",
nodeVersion: process.version,
database: pool ? "connected" : "not configured"
}, null, 2)
}]
};
});
if (pool) {
mcpServer.tool("query", "Run a read-only SQL query", {
sql: { type: "string", description: "SQL SELECT query" }
}, function(params) {
var sql = params.sql.trim();
if (!sql.toUpperCase().startsWith("SELECT")) {
return {
content: [{ type: "text", text: "Only SELECT queries allowed" }],
isError: true
};
}
return pool.query(sql).then(function(result) {
return {
content: [{
type: "text",
text: JSON.stringify({
columns: result.fields.map(function(f) { return f.name; }),
rows: result.rows.slice(0, 100),
rowCount: result.rowCount,
truncated: result.rowCount > 100
}, null, 2)
}]
};
}).catch(function(err) {
return {
content: [{ type: "text", text: "SQL Error: " + err.message }],
isError: true
};
});
});
}
mcpServer.tool("http_get", "Fetch a URL and return the response", {
url: { type: "string", description: "URL to fetch" }
}, function(params) {
var mod = params.url.startsWith("https") ? require("https") : require("http");
return new Promise(function(resolve) {
var req = mod.get(params.url, function(res) {
var body = "";
res.on("data", function(chunk) { body += chunk; });
res.on("end", function() {
resolve({
content: [{
type: "text",
text: JSON.stringify({
statusCode: res.statusCode,
headers: res.headers,
bodyLength: body.length,
body: body.substring(0, 5000)
}, null, 2)
}]
});
});
});
req.on("error", function(err) {
resolve({
content: [{ type: "text", text: "Fetch error: " + err.message }],
isError: true
});
});
req.setTimeout(10000, function() {
req.destroy();
resolve({
content: [{ type: "text", text: "Request timed out after 10 seconds" }],
isError: true
});
});
});
});
// ---- Middleware ----
// Request logging
app.use(function(req, res, next) {
var start = Date.now();
res.on("finish", function() {
if (req.path !== "/health") {
console.log(req.method + " " + req.path + " " + res.statusCode + " " + (Date.now() - start) + "ms");
}
});
next();
});
// CORS
app.use(function(req, res, next) {
var origin = req.get("Origin");
if (origin && config.corsOrigins.indexOf(origin) > -1) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
});
// ---- Routes ----
// Health check
app.get("/health", function(req, res) {
var checks = { server: "ok" };
if (pool) {
pool.query("SELECT 1").then(function() {
checks.database = "ok";
res.json({ status: "healthy", checks: checks });
}).catch(function(err) {
checks.database = "error: " + err.message;
res.status(503).json({ status: "degraded", checks: checks });
});
} else {
res.json({ status: "healthy", checks: checks });
}
});
// REST API routes
app.get("/api/status", function(req, res) {
res.json({
server: "express-fullstack-mcp",
version: "1.0.0",
mcpSessions: Object.keys(sessions).length
});
});
// MCP SSE endpoint
app.get("/mcp/sse", function(req, res) {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
var transport = new SSEServerTransport("/mcp/messages", res);
var sessionId = transport.sessionId;
sessions[sessionId] = {
transport: transport,
connectedAt: new Date(),
ip: req.ip,
toolCalls: 0
};
console.log("MCP session opened: " + sessionId + " from " + req.ip +
" (total: " + Object.keys(sessions).length + ")");
transport.onclose = function() {
var session = sessions[sessionId];
var duration = session ? Math.round((Date.now() - session.connectedAt.getTime()) / 1000) : 0;
delete sessions[sessionId];
console.log("MCP session closed: " + sessionId +
" (duration: " + duration + "s, tools: " + (session ? session.toolCalls : 0) + ")");
};
mcpServer.connect(transport);
});
// MCP message endpoint
app.post("/mcp/messages", function(req, res) {
var sessionId = req.query.sessionId;
var session = sessions[sessionId];
if (!session) {
res.status(400).json({ error: "Invalid session ID" });
return;
}
session.toolCalls++;
session.transport.handlePostMessage(req, res);
});
// MCP session list (admin)
app.get("/mcp/sessions", function(req, res) {
var list = Object.keys(sessions).map(function(id) {
var s = sessions[id];
return {
sessionId: id,
connectedAt: s.connectedAt,
duration: Math.round((Date.now() - s.connectedAt.getTime()) / 1000) + "s",
ip: s.ip,
toolCalls: s.toolCalls
};
});
res.json({ sessions: list, total: list.length });
});
// Error handler
app.use(function(err, req, res, next) {
console.error("Error:", err.message);
res.status(500).json({ error: "Internal server error" });
});
// ---- Start Server ----
var server = http.createServer(app);
server.keepAliveTimeout = 65000;
server.headersTimeout = 66000;
server.listen(config.port, function() {
console.log("Express MCP server running on port " + config.port);
console.log(" REST API: http://localhost:" + config.port + "/api/status");
console.log(" MCP SSE: http://localhost:" + config.port + "/mcp/sse");
console.log(" Health: http://localhost:" + config.port + "/health");
console.log(" Database: " + (pool ? "connected" : "not configured"));
});
// Graceful shutdown
process.on("SIGTERM", function() {
console.log("SIGTERM received. Draining connections...");
server.close(function() {
if (pool) pool.end();
console.log("Server shut down cleanly");
process.exit(0);
});
setTimeout(function() {
console.error("Forced shutdown after 30s timeout");
process.exit(1);
}, 30000);
});
# Install dependencies
npm install express @modelcontextprotocol/sdk pg
# Run without database
node server.js
# Run with PostgreSQL
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb node server.js
# Test health
curl http://localhost:3100/health
# Test REST API
curl http://localhost:3100/api/status
# View MCP sessions
curl http://localhost:3100/mcp/sessions
Output:
Express MCP server running on port 3100
REST API: http://localhost:3100/api/status
MCP SSE: http://localhost:3100/mcp/sse
Health: http://localhost:3100/health
Database: connected
Common Issues and Troubleshooting
SSE Connection Drops Immediately
Error: SSE connection closed after 0 bytes
Express compression middleware buffers the response, which prevents SSE events from flushing immediately.
// Fix: Disable compression for SSE routes
var compression = require("compression");
app.use(compression({
filter: function(req, res) {
if (req.path === "/mcp/sse") return false;
return compression.filter(req, res);
}
}));
Request Body Too Large for Tool Messages
Error: request entity too large (PayloadTooLargeError)
Express default body limit is 100KB. MCP messages with large tool responses exceed this.
// Fix: Increase body limit for MCP message endpoint
app.post("/mcp/messages", express.json({ limit: "5mb" }), function(req, res) {
// handle message
});
Multiple MCP Server Instances Sharing Transport
Error: Transport already connected to a server
Each SSE connection creates one transport, and each transport connects to one MCP server instance. If you create a new McpServer per request, tool registrations are lost.
// Wrong: new server per request
app.get("/mcp/sse", function(req, res) {
var server = new McpServer({ name: "mcp" }); // Tools not registered!
// ...
});
// Right: single server, multiple transports
var mcpServer = new McpServer({ name: "mcp" });
registerTools(mcpServer);
app.get("/mcp/sse", function(req, res) {
var transport = new SSEServerTransport("/mcp/messages", res);
mcpServer.connect(transport); // Reuses the same server
});
Session ID Missing from POST Requests
Error: Invalid session ID (sessionId is undefined)
The SSE transport sends the session ID as a query parameter on the POST URL. If your proxy strips query strings, the session lookup fails.
# Fix: Preserve query strings in proxy
location /mcp/messages {
proxy_pass http://backend$request_uri; # $request_uri includes query string
}
Best Practices
- Mount MCP routes under a prefix — use
/mcp/sseand/mcp/messagesinstead of/sseand/messages. This avoids conflicts with existing routes and makes proxy configuration clearer. - Share middleware between REST and MCP routes — authentication, logging, and CORS middleware should be written once and applied to both MCP and REST routers. Consistency reduces bugs.
- Disable response buffering for SSE — set
X-Accel-Buffering: nofor Nginx, disable Express compression for SSE routes, and setCache-Control: no-cache. Buffering breaks real-time SSE event delivery. - Track sessions in a map, not an array — use an object or Map keyed by session ID for O(1) lookups. An array requires scanning every session on every message.
- Implement graceful shutdown — close the HTTP server, drain active SSE connections, and close database pools in order. Express's
server.close()stops accepting new connections while existing ones finish. - Set keepAliveTimeout higher than your proxy — if Nginx has a 60-second keepalive, set Express to 65 seconds. If Express drops the connection first, Nginx sends requests to a dead connection.
- Validate tool inputs at the Express layer — do not rely solely on MCP schema validation. Add Express middleware that validates authentication and authorization before the tool handler runs.
- Log session lifecycle events — connection opened, closed, duration, tool call count. This data reveals usage patterns and helps debug connectivity issues.