@aashari/boilerplate-mcp-server
Version:
TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture
373 lines (372 loc) • 15.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = startServer;
const node_crypto_1 = require("node:crypto");
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const logger_util_js_1 = require("./utils/logger.util.js");
const config_util_js_1 = require("./utils/config.util.js");
const constants_util_js_1 = require("./utils/constants.util.js");
const index_js_1 = require("./cli/index.js");
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
// Import tools, resources, and prompts
const ipaddress_tool_js_1 = __importDefault(require("./tools/ipaddress.tool.js"));
const ipaddress_link_tool_js_1 = __importDefault(require("./tools/ipaddress-link.tool.js"));
const ipaddress_resource_js_1 = __importDefault(require("./resources/ipaddress.resource.js"));
const analysis_prompt_js_1 = __importDefault(require("./prompts/analysis.prompt.js"));
const logger = logger_util_js_1.Logger.forContext('index.ts');
let serverInstance = null;
let transportInstance = null;
let httpServerInstance = null;
const httpSessions = new Map();
function createMcpServer() {
const server = new mcp_js_1.McpServer({
name: constants_util_js_1.PACKAGE_NAME,
version: constants_util_js_1.VERSION,
});
ipaddress_tool_js_1.default.registerTools(server);
ipaddress_link_tool_js_1.default.registerTools(server);
ipaddress_resource_js_1.default.registerResources(server);
analysis_prompt_js_1.default.registerPrompts(server);
return server;
}
function getSessionId(req) {
const rawSessionId = req.headers['mcp-session-id'];
if (Array.isArray(rawSessionId)) {
return rawSessionId[0] ?? null;
}
if (typeof rawSessionId === 'string' && rawSessionId.length > 0) {
return rawSessionId;
}
return null;
}
/**
* Start the MCP server with the specified transport mode
*/
async function startServer(mode = 'stdio') {
logger.info(`Starting MCP server in ${mode} mode with ${constants_util_js_1.PACKAGE_NAME} v${constants_util_js_1.VERSION}`);
const serverLogger = logger_util_js_1.Logger.forContext('index.ts', 'startServer');
// Load configuration
serverLogger.info('Starting MCP server initialization...');
config_util_js_1.config.load();
if (config_util_js_1.config.getBoolean('DEBUG')) {
serverLogger.debug('Debug mode enabled');
}
if (mode === 'stdio') {
serverLogger.info(`Initializing Boilerplate MCP server v${constants_util_js_1.VERSION}`);
serverLogger.info('Registering MCP tools, resources, and prompts...');
serverInstance = createMcpServer();
serverLogger.debug('All tools, resources, and prompts registered');
serverLogger.info('Using STDIO transport');
transportInstance = new stdio_js_1.StdioServerTransport();
try {
await serverInstance.connect(transportInstance);
serverLogger.info('MCP server started successfully on STDIO transport');
setupGracefulShutdown();
return serverInstance;
}
catch (err) {
serverLogger.error('Failed to start server on STDIO transport', err);
process.exit(1);
}
}
else {
// HTTP transport with Express
serverLogger.info('Using Streamable HTTP transport');
const app = (0, express_1.default)();
// DNS rebinding protection - validate Origin header
// See: https://modelcontextprotocol.io/docs/concepts/transports
app.use((req, res, next) => {
const origin = req.headers.origin;
// Allow requests without Origin (direct API calls, curl, etc.)
if (!origin) {
return next();
}
// Validate Origin matches expected localhost patterns
const allowedOrigins = [
'http://localhost',
'http://127.0.0.1',
'http://[::1]',
'https://localhost',
'https://127.0.0.1',
'https://[::1]',
];
const isAllowed = allowedOrigins.some((allowed) => origin === allowed || origin.startsWith(`${allowed}:`));
if (!isAllowed) {
serverLogger.warn(`Rejected request with invalid origin: ${origin}`);
res.status(403).json({
error: 'Forbidden',
message: 'Invalid origin for MCP server',
});
return;
}
next();
});
app.use((0, cors_1.default)({ origin: true }));
app.use(express_1.default.json({ limit: '1mb' }));
const mcpEndpoint = '/mcp';
serverLogger.debug(`MCP endpoint: ${mcpEndpoint}`);
// Handle MCP POST requests (initialize + normal RPC calls)
app.post(mcpEndpoint, async (req, res) => {
const sessionId = getSessionId(req);
try {
if (sessionId) {
const session = httpSessions.get(sessionId);
if (!session) {
res.status(404).json({
jsonrpc: '2.0',
error: {
code: -32001,
message: 'Session not found for provided Mcp-Session-Id',
},
id: null,
});
return;
}
session.lastActivity = Date.now();
await session.transport.handleRequest(req, res, req.body);
return;
}
if (!(0, types_js_1.isInitializeRequest)(req.body)) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Missing Mcp-Session-Id header',
},
id: null,
});
return;
}
serverLogger.info('Creating new HTTP MCP session for initialize request');
let initializedSessionId = null;
const sessionServer = createMcpServer();
const sessionTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
onsessioninitialized: (newSessionId) => {
initializedSessionId = newSessionId;
httpSessions.set(newSessionId, {
server: sessionServer,
transport: sessionTransport,
lastActivity: Date.now(),
});
serverLogger.info(`Initialized HTTP MCP session ${newSessionId}`);
},
onsessionclosed: (closedSessionId) => {
const session = httpSessions.get(closedSessionId);
if (!session) {
return;
}
httpSessions.delete(closedSessionId);
void session.server.close().catch((err) => {
serverLogger.error(`Error closing server for session ${closedSessionId}`, err);
});
serverLogger.info(`Closed HTTP MCP session ${closedSessionId}`);
},
});
await sessionServer.connect(sessionTransport);
try {
await sessionTransport.handleRequest(req, res, req.body);
}
catch (err) {
if (initializedSessionId) {
httpSessions.delete(initializedSessionId);
}
await sessionTransport.close();
await sessionServer.close();
throw err;
}
}
catch (err) {
serverLogger.error('Error in HTTP MCP handler', err);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
// Handle optional GET requests for streamable HTTP SSE
app.get(mcpEndpoint, async (req, res) => {
const sessionId = getSessionId(req);
if (!sessionId) {
res.status(400).send('Missing Mcp-Session-Id header');
return;
}
const session = httpSessions.get(sessionId);
if (!session) {
res.status(404).send('Session not found');
return;
}
try {
await session.transport.handleRequest(req, res);
}
catch (err) {
serverLogger.error('Error in HTTP MCP GET handler', err);
if (!res.headersSent) {
res.status(500).send('Internal Server Error');
}
}
});
// Handle session termination requests
app.delete(mcpEndpoint, async (req, res) => {
const sessionId = getSessionId(req);
if (!sessionId) {
res.status(400).send('Missing Mcp-Session-Id header');
return;
}
const session = httpSessions.get(sessionId);
if (!session) {
res.status(404).send('Session not found');
return;
}
try {
await session.transport.handleRequest(req, res);
}
catch (err) {
serverLogger.error('Error in HTTP MCP DELETE handler', err);
if (!res.headersSent) {
res.status(500).send('Internal Server Error');
}
}
});
// Health check endpoint
app.get('/', (_req, res) => {
res.send(`Boilerplate MCP Server v${constants_util_js_1.VERSION} is running`);
});
// Start HTTP server
const PORT = Number(process.env.PORT ?? 3000);
const HOST = '127.0.0.1'; // Explicit localhost binding for security
await new Promise((resolve) => {
httpServerInstance = app.listen(PORT, HOST, () => {
serverLogger.info(`HTTP transport listening on http://${HOST}:${PORT}${mcpEndpoint}`);
serverLogger.info('Server bound to localhost only for security');
resolve();
});
});
// Reap idle sessions every 5 minutes (TTL: 30 minutes)
const SESSION_TTL_MS = 30 * 60 * 1000;
const REAP_INTERVAL_MS = 5 * 60 * 1000;
const reapInterval = setInterval(() => {
const now = Date.now();
for (const [sessionId, session] of httpSessions.entries()) {
if (now - session.lastActivity > SESSION_TTL_MS) {
serverLogger.info(`Reaping idle HTTP session ${sessionId}`);
httpSessions.delete(sessionId);
void session.transport
.close()
.catch((err) => serverLogger.debug(`Error closing transport for reaped session ${sessionId}`, err))
.then(() => session.server.close())
.catch((err) => serverLogger.debug(`Error closing server for reaped session ${sessionId}`, err));
}
}
}, REAP_INTERVAL_MS);
reapInterval.unref();
setupGracefulShutdown();
// HTTP mode uses per-session servers (managed in httpSessions map).
// Return a reference server for API compatibility; not connected to any transport.
return createMcpServer();
}
}
/**
* Main entry point
*/
async function main() {
const mainLogger = logger_util_js_1.Logger.forContext('index.ts', 'main');
// Load configuration
config_util_js_1.config.load();
// CLI mode - if any arguments are provided
if (process.argv.length > 2) {
mainLogger.info('CLI mode detected');
await (0, index_js_1.runCli)(process.argv.slice(2));
return;
}
// Server mode - determine transport
const transportMode = (process.env.TRANSPORT_MODE || 'stdio').toLowerCase();
let mode;
if (transportMode === 'stdio') {
mode = 'stdio';
}
else if (transportMode === 'http') {
mode = 'http';
}
else {
mainLogger.warn(`Unknown TRANSPORT_MODE "${transportMode}", defaulting to stdio`);
mode = 'stdio';
}
mainLogger.info(`Starting server with ${mode.toUpperCase()} transport`);
await startServer(mode);
}
// Run main if executed directly
if (require.main === module) {
main().catch((err) => {
logger.error('Unhandled error in main process', err);
process.exit(1);
});
}
/**
* Graceful shutdown handler
*/
function setupGracefulShutdown() {
const shutdownLogger = logger_util_js_1.Logger.forContext('index.ts', 'shutdown');
let shuttingDown = false;
const shutdown = async () => {
if (shuttingDown)
return;
shuttingDown = true;
try {
shutdownLogger.info('Shutting down gracefully...');
if (httpSessions.size > 0) {
shutdownLogger.info(`Closing ${httpSessions.size} active HTTP session(s)`);
}
for (const [sessionId, session] of httpSessions.entries()) {
try {
await session.transport.close();
}
catch (err) {
shutdownLogger.error(`Error closing transport for session ${sessionId}`, err);
}
try {
await session.server.close();
}
catch (err) {
shutdownLogger.error(`Error closing server for session ${sessionId}`, err);
}
}
httpSessions.clear();
if (httpServerInstance) {
await new Promise((resolve) => {
httpServerInstance.close(() => resolve());
});
}
if (transportInstance &&
'close' in transportInstance &&
typeof transportInstance.close === 'function') {
await transportInstance.close();
}
if (serverInstance && typeof serverInstance.close === 'function') {
await serverInstance.close();
}
process.exit(0);
}
catch (err) {
shutdownLogger.error('Error during shutdown', err);
process.exit(1);
}
};
['SIGINT', 'SIGTERM'].forEach((signal) => {
process.on(signal, shutdown);
});
}