#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import mermaid from 'mermaid';
import puppeteer from 'puppeteer';
import { JSDOM } from 'jsdom';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class SequenceDiagramServer {
constructor() {
this.server = new Server({
name: 'sequence-diagram-server',
version: '1.0.0',
}, {
capabilities: {
tools: {}
}
});
this.outputDir = path.join(__dirname, 'diagrams');
this.setupHandlers();
this.ensureOutputDirectory();
}
async ensureOutputDirectory() {
await fs.ensureDir(this.outputDir);
}
setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'generate_sequence_diagram',
description: 'Generate a sequence diagram image from Mermaid syntax or natural language description',
inputSchema: {
type: 'object',
properties: {
diagram: {
type: 'string',
description: 'Mermaid sequence diagram syntax or natural language description'
},
format: {
type: 'string',
enum: ['svg', 'png'],
default: 'svg',
description: 'Output image format'
},
filename: {
type: 'string',
description: 'Optional filename for the generated diagram (without extension)'
},
width: {
type: 'number',
default: 800,
description: 'Width of the generated image in pixels'
},
height: {
type: 'number',
default: 600,
description: 'Height of the generated image in pixels'
},
theme: {
type: 'string',
enum: ['default', 'dark', 'forest', 'neutral'],
default: 'default',
description: 'Mermaid theme to use'
}
},
required: ['diagram']
}
},
{
name: 'create_diagram_from_description',
description: 'Create a sequence diagram from a natural language description',
inputSchema: {
type: 'object',
properties: {
description: {
type: 'string',
description: 'Natural language description of the sequence (e.g., "User logs in, server validates, returns token")'
},
participants: {
type: 'array',
items: { type: 'string' },
description: 'List of participants/actors in the sequence'
},
format: {
type: 'string',
enum: ['svg', 'png'],
default: 'svg',
description: 'Output image format'
},
filename: {
type: 'string',
description: 'Optional filename for the generated diagram'
}
},
required: ['description']
}
},
{
name: 'validate_mermaid_syntax',
description: 'Validate Mermaid sequence diagram syntax',
inputSchema: {
type: 'object',
properties: {
diagram: {
type: 'string',
description: 'Mermaid sequence diagram syntax to validate'
}
},
required: ['diagram']
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'generate_sequence_diagram':
return await this.generateSequenceDiagram(args);
case 'create_diagram_from_description':
return await this.createDiagramFromDescription(args);
case 'validate_mermaid_syntax':
return await this.validateMermaidSyntax(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
]
};
}
});
}
async generateSequenceDiagram(args) {
const { diagram, format = 'svg', filename, width = 800, height = 600, theme = 'default' } = args;
// Ensure the diagram starts with sequenceDiagram
let mermaidCode = diagram.trim();
if (!mermaidCode.startsWith('sequenceDiagram')) {
mermaidCode = `sequenceDiagram\n${mermaidCode}`;
}
try {
const result = await this.renderDiagram(mermaidCode, format, theme, width, height);
// Save to file if filename provided
if (filename) {
const filePath = path.join(this.outputDir, `${filename}.${format}`);
if (format === 'svg') {
await fs.writeFile(filePath, result);
} else {
await fs.writeFile(filePath, result, 'base64');
}
const fileName = `${filename}.${format}`;
return {
content: [
{
type: 'text',
text: `Sequence diagram generated successfully!\nSaved to: ${filePath}\nFormat: ${format.toUpperCase()}\n\nHTTP Access:\n- View: http://localhost:8080/api/diagrams/${fileName}\n- Download: http://localhost:8080/api/diagrams/${fileName}/download\n- Direct: http://localhost:8080/diagrams/${fileName}`
}
]
};
}
return {
content: [
{
type: 'text',
text: `Sequence diagram generated successfully!\nFormat: ${format.toUpperCase()}\n\n${format === 'svg' ? result : 'Base64 PNG data generated (too large to display)'}`
}
]
};
} catch (error) {
throw new Error(`Failed to generate diagram: ${error.message}`);
}
}
async createDiagramFromDescription(args) {
const { description, participants = [], format = 'svg', filename } = args;
// Convert natural language to Mermaid syntax
const mermaidCode = this.descriptionToMermaid(description, participants);
return await this.generateSequenceDiagram({
diagram: mermaidCode,
format,
filename
});
}
descriptionToMermaid(description, participants) {
// Basic natural language to Mermaid conversion
// This is a simplified version - you can enhance this with NLP libraries
let mermaidCode = 'sequenceDiagram\n';
// Add participants if provided
participants.forEach(participant => {
mermaidCode += ` participant ${participant}\n`;
});
// Simple pattern matching for common sequence actions
const lines = description.split(/[.!?]/).filter(line => line.trim());
lines.forEach(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return;
// Look for patterns like "A sends B" or "A calls B"
const sendPattern = /(\w+)\s+(?:sends?|calls?|requests?)\s+(\w+)/i;
const responsePattern = /(\w+)\s+(?:responds?|returns?|replies?)\s+(?:to\s+)?(\w+)/i;
const sendMatch = trimmedLine.match(sendPattern);
const responseMatch = trimmedLine.match(responsePattern);
if (sendMatch) {
const [, from, to] = sendMatch;
mermaidCode += ` ${from}->>${to}: ${trimmedLine}\n`;
} else if (responseMatch) {
const [, from, to] = responseMatch;
mermaidCode += ` ${from}-->>${to}: ${trimmedLine}\n`;
} else {
// Default: treat as a note or generic message
mermaidCode += ` Note over ${participants[0] || 'A'}: ${trimmedLine}\n`;
}
});
return mermaidCode;
}
async validateMermaidSyntax(args) {
const { diagram } = args;
try {
// Try to parse with mermaid
const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="mermaid"></div></body></html>`);
global.window = dom.window;
global.document = dom.window.document;
mermaid.initialize({ startOnLoad: false });
// Attempt to parse the diagram
const parseResult = await mermaid.parse(diagram);
return {
content: [
{
type: 'text',
text: `✅ Mermaid syntax is valid!\n\nParsed successfully: ${JSON.stringify(parseResult, null, 2)}`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `❌ Mermaid syntax error:\n${error.message}`
}
]
};
}
}
async renderDiagram(mermaidCode, format, theme, width, height) {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
await page.setViewport({ width, height });
const html = `
<!DOCTYPE html>
<html>
<head>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({
startOnLoad: false,
theme: '${theme}',
securityLevel: 'loose'
});
window.renderDiagram = async function() {
const element = document.getElementById('mermaid');
try {
const { svg } = await mermaid.render('diagram', \`${mermaidCode}\`);
element.innerHTML = svg;
return true;
} catch (error) {
element.innerHTML = '<p>Error: ' + error.message + '</p>';
throw error;
}
};
</script>
</head>
<body>
<div id="mermaid"></div>
</body>
</html>
`;
await page.setContent(html);
await page.waitForFunction('window.renderDiagram');
await page.evaluate('window.renderDiagram()');
// Wait for the diagram to render
await page.waitForSelector('#mermaid svg', { timeout: 10000 });
if (format === 'svg') {
const svg = await page.$eval('#mermaid svg', el => el.outerHTML);
return svg;
} else {
const element = await page.$('#mermaid svg');
const screenshot = await element.screenshot({ type: 'png' });
return screenshot.toString('base64');
}
} finally {
await browser.close();
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Sequence Diagram MCP server running on stdio');
}
}
// Start both MCP server and HTTP server
async function startServers() {
try {
// Start MCP server
const server = new SequenceDiagramServer();
await server.run();
// Start HTTP server in a separate process or thread
// For now, we'll start it as a child process
const { spawn } = await import('child_process');
const httpServer = spawn('node', ['server.js'], {
stdio: 'inherit',
detached: true
});
console.error('HTTP server started on port 8080');
// Handle process termination
process.on('SIGINT', () => {
console.error('Shutting down servers...');
httpServer.kill();
process.exit(0);
});
} catch (error) {
console.error('Failed to start servers:', error);
process.exit(1);
}
}
startServers();