import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { createTextResult } from "./lib/utils.ts";
import { logger } from "./logger.ts";
import { getConfig } from "./config.ts";
const getServer = () => {
const config = getConfig();
const server = new McpServer({
name: config.SERVER_NAME,
version: config.SERVER_VERSION,
});
server.registerTool(
"echo",
{
title: "Echo",
description: "Echo back the provided message",
inputSchema: {
message: z.string().describe("The message to echo back"),
},
},
async (args) => {
const data = { echo: args.message };
return createTextResult(data);
},
);
return server;
};
const app = express();
app.use(express.json());
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const mcpHandler = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
try {
// Handle initialization requests (usually POST without session ID)
if (req.method === "POST" && !sessionId && isInitializeRequest(req.body)) {
logger.info("Initializing new MCP session");
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = transport;
logger.info("MCP session initialized", { sessionId });
},
});
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return;
}
// Handle existing session requests
if (sessionId && transports[sessionId]) {
const transport = transports[sessionId];
await transport.handleRequest(req, res, req.body);
return;
}
// Handle case where no session ID is provided for non-init requests
if (req.method === "POST" && !sessionId) {
logger.warn(
"POST request without session ID for non-initialization request",
);
res
.status(400)
.json({ error: "Session ID required for non-initialization requests" });
return;
}
// Handle unknown session
if (sessionId && !transports[sessionId]) {
logger.warn("Request for unknown session", { sessionId });
res.status(404).json({ error: "Session not found" });
return;
}
// For GET requests without session, return server info
if (req.method === "GET") {
const config = getConfig();
res.json({
name: config.SERVER_NAME,
version: config.SERVER_VERSION,
description: "TypeScript template for building MCP servers",
capabilities: ["tools"],
});
}
} catch (error) {
logger.error("Error handling MCP request", {
error: error instanceof Error ? error.message : error,
});
res.status(500).json({ error: "Internal server error" });
}
};
// Handle MCP requests on /mcp endpoint
app.post("/mcp", mcpHandler);
app.get("/mcp", mcpHandler);
async function main() {
const config = getConfig();
// Graceful shutdown handling
process.on("SIGTERM", () => {
logger.info("SIGTERM received, shutting down gracefully");
process.exit(0);
});
process.on("SIGINT", () => {
logger.info("SIGINT received, shutting down gracefully");
process.exit(0);
});
app.listen(config.PORT, () => {
logger.info(
`MCP TypeScript Template Server running on port ${config.PORT}`,
{
environment: config.NODE_ENV,
serverName: config.SERVER_NAME,
version: config.SERVER_VERSION,
},
);
});
}
main().catch((error) => {
logger.error("Server startup error", {
error: error instanceof Error ? error.message : error,
});
process.exit(1);
});