Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -90,7 +110,6 @@ Options:
--help, -h Show this help message
```


## 🔨 Development

Install dependencies:
Expand All @@ -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

Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"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": {
"test": "vitest",
"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"
Expand All @@ -20,6 +22,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",
Expand All @@ -28,6 +32,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",
Expand Down
5 changes: 0 additions & 5 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

Expand Down
117 changes: 26 additions & 91 deletions src/services/sse.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
const activeTransports: Record<string, SSEServerTransport> = {};
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<string, SSEServerTransport> = {};

// 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}`);
});
};
7 changes: 1 addition & 6 deletions src/services/stdio.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const transport = new StdioServerTransport();

// Register transport cleanup
shutdownManager.registerCleanup(createTransportCleanup(transport));

await server.connect(transport);
console.log("STDIO MCP Server started");
}
Loading