From a17d402eb4ab6d9fb2d48d82b7e23f4e8a7e3c21 Mon Sep 17 00:00:00 2001 From: lyz Date: Sat, 13 Sep 2025 10:35:23 +0800 Subject: [PATCH 1/4] refactor(server): refactor server implementation to simplify code and improve maintainability - Remove complex request handling logic, switch to Express framework - Remove unnecessary cleanup functions and server factory - Simplify SSE and Streamable server implementations - Add CORS support - Optimize error handling and response format --- package.json | 4 + src/server.ts | 5 - src/services/sse.ts | 117 ++++------------- src/services/stdio.ts | 7 +- src/services/streamable.ts | 254 +++++++------------------------------ 5 files changed, 77 insertions(+), 310 deletions(-) diff --git a/package.json b/package.json index a3c767b..b219e76 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "keywords": ["mcp", "mermaid", "data-visualization", "chart", "diagram"], "dependencies": { "@modelcontextprotocol/sdk": "^1.11.4", + "cors": "^2.8.5", + "express": "^5.1.0", "mermaid-isomorphic": "^3.0.4", "playwright": "^1.52.0", "zod": "^3.25.16", @@ -28,6 +30,8 @@ "devDependencies": { "@biomejs/biome": "1.9.4", "@modelcontextprotocol/inspector": "^0.12.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "@types/node": "^22.15.21", "husky": "^9.1.7", "lint-staged": "^15.5.2", diff --git a/src/server.ts b/src/server.ts index 2f33ff7..b153618 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,9 +12,7 @@ import { } from "./services"; import { schema, tool } from "./tools"; import { renderMermaid } from "./utils"; -import { createServerCleanup } from "./utils/cleanupHelpers"; import { Logger } from "./utils/logger"; -import { shutdownManager } from "./utils/shutdownManager"; /** * Creates and configures an MCP server for mermaid generation. @@ -36,9 +34,6 @@ export function createServer(): Server { server.onerror = (error) => Logger.error("MCP Error", error); - // Register server cleanup with shutdown manager - shutdownManager.registerCleanup(createServerCleanup(server)); - return server; } diff --git a/src/services/sse.ts b/src/services/sse.ts index 147046a..27df0e2 100644 --- a/src/services/sse.ts +++ b/src/services/sse.ts @@ -1,110 +1,45 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { type RequestHandlers, createBaseHttpServer } from "../utils"; -import { Logger } from "../utils/logger"; +import type { Request, Response } from "express"; +import express from "express"; export const startSSEMcpServer = async ( server: Server, endpoint = "/sse", port = 3033, ): Promise => { - const activeTransports: Record = {}; + const app = express(); + app.use(express.json()); - // Define the request handler for SSE-specific logic - const handleRequest: RequestHandlers["handleRequest"] = async ( - req: IncomingMessage, - res: ServerResponse, - ) => { - if (!req.url) { - res.writeHead(400).end("No URL"); - return; - } - - const reqUrl = new URL(req.url, "http://localhost"); + const transports: Record = {}; - // Handle GET requests to the SSE endpoint - if (req.method === "GET" && reqUrl.pathname === endpoint) { + app.get(endpoint, async (req: Request, res: Response) => { + try { const transport = new SSEServerTransport("/messages", res); - - activeTransports[transport.sessionId] = transport; - - let closed = false; - - res.on("close", async () => { - closed = true; - - try { - await server.close(); - } catch (error) { - Logger.error("Error closing server", error); - } - - delete activeTransports[transport.sessionId]; - }); - - try { - await server.connect(transport); - - await transport.send({ - jsonrpc: "2.0", - method: "sse/connection", - params: { message: "SSE Connection established" }, - }); - } catch (error) { - if (!closed) { - Logger.error("Error connecting to server", error); - res.writeHead(500).end("Error connecting to server"); - } - } - - return; + transports[transport.sessionId] = transport; + transport.onclose = () => delete transports[transport.sessionId]; + await server.connect(transport); + } catch (error) { + if (!res.headersSent) + res.status(500).send("Error establishing SSE stream"); } + }); - // Handle POST requests to the messages endpoint - if (req.method === "POST" && req.url?.startsWith("/messages")) { - const sessionId = new URL( - req.url, - "https://example.com", - ).searchParams.get("sessionId"); - - if (!sessionId) { - res.writeHead(400).end("No sessionId"); - return; - } - - const activeTransport: SSEServerTransport | undefined = - activeTransports[sessionId]; - - if (!activeTransport) { - res.writeHead(400).end("No active transport"); - return; - } - - await activeTransport.handlePostMessage(req, res); - return; - } + app.post("/messages", async (req: Request, res: Response) => { + const sessionId = req.query.sessionId as string; + if (!sessionId) return res.status(400).send("Missing sessionId parameter"); - // If we reach here, no handler matched - res.writeHead(404).end("Not found"); - }; + const transport = transports[sessionId]; + if (!transport) return res.status(404).send("Session not found"); - // Custom cleanup for SSE server - const cleanup = () => { - // Close all active transports - for (const transport of Object.values(activeTransports)) { - try { - transport.close(); - } catch (error) { - Logger.error("Error closing SSE transport", error); - } + try { + await transport.handlePostMessage(req, res, req.body); + } catch (error) { + if (!res.headersSent) res.status(500).send("Error handling request"); } - }; + }); - // Create the HTTP server using our factory - createBaseHttpServer(port, endpoint, { - handleRequest, - cleanup, - serverType: "SSE Server", + app.listen(port, () => { + console.log(`SSE Server listening on http://localhost:${port}${endpoint}`); }); }; diff --git a/src/services/stdio.ts b/src/services/stdio.ts index 048063b..069b7ea 100644 --- a/src/services/stdio.ts +++ b/src/services/stdio.ts @@ -1,13 +1,8 @@ import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createTransportCleanup } from "../utils/cleanupHelpers"; -import { shutdownManager } from "../utils/shutdownManager"; export async function startStdioMcpServer(server: Server): Promise { const transport = new StdioServerTransport(); - - // Register transport cleanup - shutdownManager.registerCleanup(createTransportCleanup(transport)); - await server.connect(transport); + console.log("STDIO MCP Server started"); } diff --git a/src/services/streamable.ts b/src/services/streamable.ts index 42ab0b0..42122ce 100644 --- a/src/services/streamable.ts +++ b/src/services/streamable.ts @@ -1,222 +1,60 @@ -import { randomUUID } from "node:crypto"; -import type { IncomingMessage, ServerResponse } from "node:http"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { - type EventStore, - StreamableHTTPServerTransport, -} from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; -import { - InMemoryEventStore, - type RequestHandlers, - createBaseHttpServer, - getBody, -} from "../utils"; -import { Logger } from "../utils/logger"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; +import express from "express"; export const startHTTPStreamableServer = async ( createServer: () => Server, endpoint = "/mcp", port = 1122, - eventStore: EventStore = new InMemoryEventStore(), ): Promise => { - const activeTransports: Record< - string, - { - server: Server; - transport: StreamableHTTPServerTransport; - } - > = {}; - - // Define the request handler for streamable-specific logic - const handleRequest: RequestHandlers["handleRequest"] = async ( - req: IncomingMessage, - res: ServerResponse, - ) => { - if (!req.url) { - res.writeHead(400).end("No URL"); - return; - } - - const reqUrl = new URL(req.url, "http://localhost"); - - // Handle POST requests to endpoint - if (req.method === "POST" && reqUrl.pathname === endpoint) { - try { - const sessionId = Array.isArray(req.headers["mcp-session-id"]) - ? req.headers["mcp-session-id"][0] - : req.headers["mcp-session-id"]; - let transport: StreamableHTTPServerTransport; - - let server: Server; - - const body = await getBody(req); - - /** - * diagram: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#sequence-diagram. - */ - // 1. If the sessionId is provided and the server is already created, use the existing transport and server. - if (sessionId && activeTransports[sessionId]) { - transport = activeTransports[sessionId].transport; - server = activeTransports[sessionId].server; - - // 2. If the sessionId is not provided and the request is an initialize request, create a new transport for the session. - } else if (!sessionId && isInitializeRequest(body)) { - transport = new StreamableHTTPServerTransport({ - // use the event store to store the events to replay on reconnect. - // more details: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#resumability-and-redelivery. - eventStore: eventStore || new InMemoryEventStore(), - onsessioninitialized: (_sessionId: string) => { - // add only when the id Sesison id is generated. - activeTransports[_sessionId] = { - server, - transport, - }; - }, - sessionIdGenerator: randomUUID, - }); - - // Handle the server close event. - transport.onclose = async () => { - const sid = transport.sessionId; - if (sid && activeTransports[sid]) { - try { - await server?.close(); - } catch (error) { - Logger.error("Error closing server", error); - } - - // delete used transport and server to avoid memory leak. - delete activeTransports[sid]; - } - }; - - // Create the server - try { - server = createServer(); - } catch (error) { - if (error instanceof Response) { - res.writeHead(error.status).end(error.statusText); - return; - } - res.writeHead(500).end("Error creating server"); - return; - } - - server.connect(transport); - - await transport.handleRequest(req, res, body); - return; - } else { - // Error if the server is not created but the request is not an initialize request. - res.setHeader("Content-Type", "application/json"); - res.writeHead(400).end( - JSON.stringify({ - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: null, - jsonrpc: "2.0", - }), - ); - - return; - } - - // Handle the request if the server is already created. - await transport.handleRequest(req, res, body); - } catch (error) { - Logger.error("Error handling request", error); - res.setHeader("Content-Type", "application/json"); - res.writeHead(500).end( - JSON.stringify({ - error: { code: -32603, message: "Internal Server Error" }, - id: null, - jsonrpc: "2.0", - }), - ); - } - return; - } - - // Handle GET requests to endpoint - if (req.method === "GET" && reqUrl.pathname === endpoint) { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - const activeTransport: - | { - server: Server; - transport: StreamableHTTPServerTransport; - } - | undefined = sessionId ? activeTransports[sessionId] : undefined; - - if (!sessionId) { - res.writeHead(400).end("No sessionId"); - return; - } - - if (!activeTransport) { - res.writeHead(400).end("No active transport"); - return; - } - - const lastEventId = req.headers["last-event-id"] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); - } - - await activeTransport.transport.handleRequest(req, res); - return; - } - - // Handle DELETE requests to endpoint - if (req.method === "DELETE" && reqUrl.pathname === endpoint) { - console.log("received delete request"); - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId) { - res.writeHead(400).end("Invalid or missing sessionId"); - return; - } - - console.log("received delete request for session", sessionId); - - const transport = activeTransports[sessionId]?.transport; - if (!transport) { - res.writeHead(400).end("No active transport"); - return; - } - - try { - await transport.handleRequest(req, res); - } catch (error) { - console.error("Error handling delete request:", error); - res.writeHead(500).end("Error handling delete request"); + const app = express(); + app.use(express.json()); + app.use(cors({ origin: "*", exposedHeaders: ["Mcp-Session-Id"] })); + + app.post(endpoint, async (req: Request, res: Response) => { + try { + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + res.on("close", () => { + transport.close(); + server.close(); + }); + } catch (error) { + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); } - - return; } + }); - // If we reach here, no handler matched - res.writeHead(404).end("Not found"); - }; + app.get(endpoint, (req: Request, res: Response) => { + res.status(405).json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed" }, + id: null, + }); + }); - // Custom cleanup for streamable server - const cleanup = () => { - for (const { server, transport } of Object.values(activeTransports)) { - try { - transport.close(); - } catch (error) { - Logger.error("Error closing streamable transport", error); - } - } - }; + app.delete(endpoint, (req: Request, res: Response) => { + res.status(405).json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed" }, + id: null, + }); + }); - // Create the HTTP server using our factory - createBaseHttpServer(port, endpoint, { - handleRequest, - cleanup, - serverType: "HTTP Streamable Server", + app.listen(port, () => { + console.log( + `Streamable HTTP Server listening on http://localhost:${port}${endpoint}`, + ); }); }; From 5deb64cc290288e2324f45d06a244174216b40e0 Mon Sep 17 00:00:00 2001 From: lyz Date: Sat, 13 Sep 2025 10:35:34 +0800 Subject: [PATCH 2/4] refactor(utils): remove unused utility classes - Remove unused utility classes: InMemoryEventStore, cleanupHelpers, getBody, httpServer, and shutdownManager - Keep utility classes that are still in use: zodToJsonSchema, renderMermaid, and logger --- src/utils/InMemoryEventStore.ts | 92 -------------------- src/utils/cleanupHelpers.ts | 47 ---------- src/utils/getBody.ts | 16 ---- src/utils/httpServer.ts | 150 -------------------------------- src/utils/index.ts | 9 -- src/utils/shutdownManager.ts | 59 ------------- 6 files changed, 373 deletions(-) delete mode 100644 src/utils/InMemoryEventStore.ts delete mode 100644 src/utils/cleanupHelpers.ts delete mode 100644 src/utils/getBody.ts delete mode 100644 src/utils/httpServer.ts delete mode 100644 src/utils/shutdownManager.ts diff --git a/src/utils/InMemoryEventStore.ts b/src/utils/InMemoryEventStore.ts deleted file mode 100644 index e1a0b59..0000000 --- a/src/utils/InMemoryEventStore.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * This is a copy of the InMemoryEventStore from the typescript-sdk. - * reference: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/examples/shared/inMemoryEventStore.ts - */ - -import type { EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; - -/** - * Simple in-memory implementation of the EventStore interface for resumability. - * This is primarily intended for examples and testing, not for production use. - * where a persistent storage solution would be more appropriate. - * see more details: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#resumability-and-redelivery. - */ -export class InMemoryEventStore implements EventStore { - private events: Map = - new Map(); - - /** - * Generates a unique event ID for a given stream ID - */ - private generateEventId(streamId: string): string { - return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; - } - - /** - * Extracts the stream ID from an event ID - */ - private getStreamIdFromEventId(eventId: string): string { - const parts = eventId.split("_"); - return parts.length > 0 ? parts[0] : ""; - } - - /** - * Stores an event with a generated event ID - * Implements EventStore.storeEvent - */ - async storeEvent(streamId: string, message: JSONRPCMessage): Promise { - const eventId = this.generateEventId(streamId); - this.events.set(eventId, { streamId, message }); - return eventId; - } - - /** - * Replays events that occurred after a specific event ID - * Implements EventStore.replayEventsAfter - */ - async replayEventsAfter( - lastEventId: string, - { - send, - }: { send: (eventId: string, message: JSONRPCMessage) => Promise }, - ): Promise { - if (!lastEventId || !this.events.has(lastEventId)) { - return ""; - } - - // Extract the stream ID from the event ID - const streamId = this.getStreamIdFromEventId(lastEventId); - if (!streamId) { - return ""; - } - - let foundLastEvent = false; - - // Sort events by eventId for chronological ordering - const sortedEvents = [...this.events.entries()].sort((a, b) => - a[0].localeCompare(b[0]), - ); - - for (const [ - eventId, - { streamId: eventStreamId, message }, - ] of sortedEvents) { - // Only include events from the same stream - if (eventStreamId !== streamId) { - continue; - } - - // Start sending events after we find the lastEventId - if (eventId === lastEventId) { - foundLastEvent = true; - continue; - } - - if (foundLastEvent) { - await send(eventId, message); - } - } - return streamId; - } -} diff --git a/src/utils/cleanupHelpers.ts b/src/utils/cleanupHelpers.ts deleted file mode 100644 index b97df17..0000000 --- a/src/utils/cleanupHelpers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { Logger } from "./logger"; - -/** - * Create a server cleanup handler - */ -export function createServerCleanup(server: Server): () => void { - return () => { - try { - server.close(); - } catch (error) { - Logger.error("Error closing server", error); - } - }; -} - -/** - * Create a transport cleanup handler - */ -export function createTransportCleanup(transport: { - close: () => void; -}): () => void { - return () => { - try { - transport.close(); - } catch (error) { - Logger.error("Error closing transport", error); - } - }; -} - -/** - * Create a combined cleanup handler for server and transport - */ -export function createCombinedCleanup( - server: Server, - transport: { close: () => void }, -): () => void { - return () => { - try { - transport.close(); - server.close(); - } catch (error) { - Logger.error("Error during combined cleanup", error); - } - }; -} diff --git a/src/utils/getBody.ts b/src/utils/getBody.ts deleted file mode 100644 index 939b7a7..0000000 --- a/src/utils/getBody.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IncomingMessage } from "node:http"; - -export function getBody(request: IncomingMessage) { - return new Promise((resolve) => { - const bodyParts: Buffer[] = []; - let body: string; - request - .on("data", (chunk) => { - bodyParts.push(chunk); - }) - .on("end", () => { - body = Buffer.concat(bodyParts).toString(); - resolve(JSON.parse(body)); - }); - }); -} diff --git a/src/utils/httpServer.ts b/src/utils/httpServer.ts deleted file mode 100644 index 9065237..0000000 --- a/src/utils/httpServer.ts +++ /dev/null @@ -1,150 +0,0 @@ -import http from "node:http"; -import type { IncomingMessage, ServerResponse } from "node:http"; -import { Logger } from "./logger"; -import { shutdownManager } from "./shutdownManager"; - -/** - * Interface for request handlers that will be passed to the server factory - */ -export interface RequestHandlers { - /** - * Main handler for HTTP requests - */ - handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise; - - /** - * Custom cleanup function to be called when the server is shutting down - */ - cleanup?: () => void; - - /** - * Server type name for logging purposes - */ - serverType: string; -} - -/** - * Handles CORS headers for incoming requests - */ -function handleCORS(req: IncomingMessage, res: ServerResponse): void { - if (req.headers.origin) { - try { - const origin = new URL(req.headers.origin as string); - res.setHeader("Access-Control-Allow-Origin", origin.origin); - res.setHeader("Access-Control-Allow-Credentials", "true"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "*"); - } catch (error) { - Logger.error("Error parsing origin", error); - } - } -} - -/** - * Handles common endpoints like health check and ping - * @returns true if the request was handled, false otherwise - */ -function handleCommonEndpoints( - req: IncomingMessage, - res: ServerResponse, -): boolean { - if (!req.url) { - res.writeHead(400).end("No URL"); - return true; - } - - if (req.method === "GET" && req.url === "/health") { - res.writeHead(200, { "Content-Type": "text/plain" }).end("OK"); - return true; - } - - if (req.method === "GET" && req.url === "/ping") { - res.writeHead(200).end("pong"); - return true; - } - - return false; -} - -/** - * Sets up cleanup handlers for the HTTP server - */ -function setupCleanupHandlers( - httpServer: http.Server, - customCleanup?: () => void, -): void { - // Register HTTP server cleanup - shutdownManager.registerCleanup(() => { - return new Promise((resolve) => { - httpServer.close(() => { - Logger.cleanup("HTTP server closed"); - resolve(); - }); - }); - }); - - // Register custom cleanup if provided - if (customCleanup) { - shutdownManager.registerCleanup(customCleanup); - } - - // Setup signal handlers only once - shutdownManager.setupSignalHandlers(); - - httpServer.once("close", () => { - process.removeAllListeners("SIGINT"); - process.removeAllListeners("SIGTERM"); - }); -} - -/** - * Logs server startup information with formatted URLs - */ -function logServerStartup( - serverType: string, - port: number, - endpoint: string, -): void { - Logger.serverStartup(serverType, port, endpoint); -} - -/** - * Creates a base HTTP server with common functionality - */ -export function createBaseHttpServer( - port: number, - endpoint: string, - handlers: RequestHandlers, -): http.Server { - const httpServer = http.createServer(async (req, res) => { - // Handle CORS for all requests - handleCORS(req, res); - - // Handle OPTIONS requests - if (req.method === "OPTIONS") { - res.writeHead(204).end(); - return; - } - - // Handle common endpoints like health and ping - if (handleCommonEndpoints(req, res)) return; - - // Pass remaining requests to the specific handler - try { - await handlers.handleRequest(req, res); - } catch (error) { - Logger.error(`Error in ${handlers.serverType} request handler`, error); - res.writeHead(500).end("Internal Server Error"); - } - }); - - // Set up cleanup handlers - setupCleanupHandlers(httpServer, handlers.cleanup); - - // Start listening and log server info - httpServer.listen(port, () => { - logServerStartup(handlers.serverType, port, endpoint); - }); - - return httpServer; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 8354ef7..4a06492 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,12 +1,3 @@ export { zodToJsonSchema } from "./schema"; -export { InMemoryEventStore } from "./InMemoryEventStore"; -export { getBody } from "./getBody"; -export { createBaseHttpServer, type RequestHandlers } from "./httpServer"; export { renderMermaid } from "./render"; -export { shutdownManager, ShutdownManager } from "./shutdownManager"; export { Logger } from "./logger"; -export { - createServerCleanup, - createTransportCleanup, - createCombinedCleanup, -} from "./cleanupHelpers"; diff --git a/src/utils/shutdownManager.ts b/src/utils/shutdownManager.ts deleted file mode 100644 index 3102fe6..0000000 --- a/src/utils/shutdownManager.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Logger } from "./logger"; - -/** - * Centralized shutdown manager for handling cleanup and graceful shutdown - */ -export class ShutdownManager { - private cleanupHandlers: Array<() => void | Promise> = []; - private isShuttingDown = false; - - /** - * Register a cleanup handler - */ - registerCleanup(handler: () => void | Promise): void { - this.cleanupHandlers.push(handler); - } - - async cleanup(): Promise { - if (this.isShuttingDown) { - Logger.warn("Shutdown already in progress, forcing exit..."); - process.exit(1); - } - - this.isShuttingDown = true; - Logger.info("🔄 Shutting down gracefully..."); - - // Execute all cleanup handlers with timeout - const cleanupPromises = this.cleanupHandlers.map(async (handler) => { - try { - await Promise.race([ - handler(), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Cleanup timeout")), 3000), - ), - ]); - } catch (error) { - Logger.error("Error during cleanup", error); - } - }); - - await Promise.all(cleanupPromises); - Logger.success("Cleanup completed"); - process.exit(0); - } - - /** - * Setup signal handlers for graceful shutdown - */ - setupSignalHandlers(): void { - // Remove any existing listeners to avoid duplicates - process.removeAllListeners("SIGINT"); - process.removeAllListeners("SIGTERM"); - - process.once("SIGINT", this.cleanup.bind(this)); - process.once("SIGTERM", this.cleanup.bind(this)); - } -} - -// Global shutdown manager instance -export const shutdownManager = new ShutdownManager(); From d3ffcd81b67bcf0598b290f6baa31969923306c7 Mon Sep 17 00:00:00 2001 From: lyz Date: Sat, 13 Sep 2025 13:35:54 +0800 Subject: [PATCH 3/4] docs(README): Update the run guide and development instructions - Add detailed steps for local development and global installation - Add startup methods for different transport protocols - Update access point information - Add new npm scripts in package.json --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 ++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 40b65e6..0568fbc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,9 @@ Also, you can use it on aliyun, modelscope, glama.ai, smithery.ai or others with ## 🚰 Run with SSE or Streamable transport -Install the package globally. +### Option 1: Global Installation + +Install the package globally: ```bash npm install -g mcp-mermaid @@ -69,10 +71,28 @@ mcp-mermaid -t sse mcp-mermaid -t streamable ``` +### Option 2: Local Development + +If you're working with the source code locally: + +```bash +# Clone and setup +git clone https://github.com/hustcc/mcp-mermaid.git +cd mcp-mermaid +npm install +npm run build + +# Run with npm scripts +npm run start:sse # SSE transport on port 3033 +npm run start:streamable # Streamable transport on port 1122 +``` + +### Access Points + Then you can access the server at: -- SSE transport: `http://localhost:3033/sse` -- Streamable transport: `http://localhost:3033/mcp` +- SSE transport: `http://localhost:3033/sse` +- Streamable transport: `http://localhost:1122/mcp` (local) or `http://localhost:3033/mcp` (global) ## 🎮 CLI Options @@ -90,7 +110,6 @@ Options: --help, -h Show this help message ``` - ## 🔨 Development Install dependencies: @@ -105,12 +124,36 @@ Build the server: npm run build ``` -Start the MCP server: +### Start the MCP server + +**Using MCP Inspector (for debugging):** ```bash npm run start ``` +**Using different transport protocols:** + +```bash +# SSE transport (Server-Sent Events) +npm run start:sse + +# Streamable HTTP transport +npm run start:streamable +``` + +**Direct node commands:** + +```bash +# SSE transport on port 3033 +node build/index.js --transport sse --port 3033 + +# Streamable HTTP transport on port 1122 +node build/index.js --transport streamable --port 1122 + +# STDIO transport (for MCP client integration) +node build/index.js --transport stdio +``` ## 📄 License diff --git a/package.json b/package.json index b219e76..ae1b076 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "prebuild": "rm -rf build/*", "build": "tsc && tsc-alias -p tsconfig.json", "start": "npx @modelcontextprotocol/inspector node build/index.js", + "start:sse": "node build/index.js --transport sse --port 3033", + "start:streamable": "node build/index.js --transport streamable --port 1122", "prepare": "husky && npm run build", "prepublishOnly": "npm run build", "postinstall": "playwright install --with-deps chromium" From 7b0ed3c18b95a24d87de68b0e9a2270a54ac89f6 Mon Sep 17 00:00:00 2001 From: hustcc Date: Sat, 13 Sep 2025 13:54:07 +0800 Subject: [PATCH 4/4] chore: bump version from 0.1.3 to 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae1b076..8fb62c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mcp-mermaid", "description": "❤️ Generate mermaid diagram and chart with AI MCP dynamically.", - "version": "0.1.3", + "version": "0.2.0", "main": "build/index.js", "type": "module", "scripts": {