mermaid-live-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/index.js +1164 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../preview/src/server.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { writeFileSync, mkdirSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { PreviewServer } from \"@sketchdraw/preview\";\n\n// ── In-memory diagram store ──\ninterface StoredDiagram {\n id: string;\n syntax: string;\n svg: string;\n createdAt: Date;\n filePath?: string;\n previewUrl?: string;\n}\n\nconst diagrams = new Map<string, StoredDiagram>();\nlet nextId = 1;\n\nfunction generateId(): string {\n return `diagram_${nextId++}`;\n}\n\n// ── Live preview (per-diagram servers) ──\nconst previewServers = new Map<string, { server: PreviewServer; url: string }>();\n\nasync function ensurePreviewForDiagram(\n id: string\n): Promise<{ server: PreviewServer; url: string } | null> {\n const existing = previewServers.get(id);\n if (existing) return existing;\n\n const preview = new PreviewServer();\n preview.onSvgRendered = (diagId: string, svg: string) => {\n const stored = diagrams.get(diagId);\n if (stored) stored.svg = svg;\n };\n\n try {\n const url = await preview.start();\n const entry = { server: preview, url };\n previewServers.set(id, entry);\n preview.openBrowser();\n return entry;\n } catch {\n return null;\n }\n}\n\nasync function pushMermaidToPreview(\n id: string,\n syntax: string,\n title?: string\n): Promise<string | null> {\n const entry = await ensurePreviewForDiagram(id);\n if (!entry) return null;\n entry.server.updateMermaid(id, syntax, title);\n return entry.url;\n}\n\n// ── MCP Server ──\nconst server = new McpServer({\n name: \"mermaid-live-mcp\",\n version: \"0.1.0\",\n});\n\n// ── Tool: generate_mermaid ──\nserver.tool(\n \"generate_mermaid\",\n \"Generate a diagram from Mermaid.js syntax. Renders in live preview browser with SVG/PNG download options. Supports flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, Gantt charts, and more.\",\n {\n syntax: z\n .string()\n .describe(\"Mermaid diagram syntax (e.g. 'graph TD; A-->B;')\"),\n title: z\n .string()\n .optional()\n .describe(\"Optional title for the browser preview tab\"),\n },\n async ({ syntax, title }) => {\n const id = generateId();\n diagrams.set(id, {\n id,\n syntax,\n svg: \"\",\n createdAt: new Date(),\n });\n\n const preview = await pushMermaidToPreview(id, syntax, title);\n const storedDiagram = diagrams.get(id);\n if (storedDiagram && preview) storedDiagram.previewUrl = preview;\n\n const responseLines = [`Mermaid diagram sent to preview.`, `ID: ${id}`];\n if (preview) {\n responseLines.push(`Preview: ${preview}`);\n responseLines.push(\n `Use the download buttons in the browser to export as SVG or PNG.`\n );\n }\n\n return {\n content: [{ type: \"text\" as const, text: responseLines.join(\"\\n\") }],\n };\n }\n);\n\n// ── Tool: update_diagram ──\nserver.tool(\n \"update_diagram\",\n \"Replace a diagram's Mermaid syntax and re-render it in the preview.\",\n {\n diagram_id: z.string().describe(\"The ID of the diagram to update\"),\n syntax: z\n .string()\n .describe(\"New Mermaid diagram syntax to replace the existing one\"),\n title: z\n .string()\n .optional()\n .describe(\"Optional title for the browser preview tab\"),\n },\n async ({ diagram_id, syntax, title }) => {\n const stored = diagrams.get(diagram_id);\n if (!stored) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Diagram not found: ${diagram_id}. Use list_diagrams to see available diagrams.`,\n },\n ],\n isError: true,\n };\n }\n\n stored.syntax = syntax;\n stored.svg = \"\";\n\n const preview = await pushMermaidToPreview(diagram_id, syntax, title);\n\n const responseLines = [`Diagram ${diagram_id} updated successfully.`];\n if (preview) {\n responseLines.push(`Preview: ${preview}`);\n }\n\n return {\n content: [{ type: \"text\" as const, text: responseLines.join(\"\\n\") }],\n };\n }\n);\n\n// ── Tool: list_diagrams ──\nserver.tool(\n \"list_diagrams\",\n \"List all diagrams generated in this session.\",\n {},\n async () => {\n if (diagrams.size === 0) {\n return {\n content: [\n { type: \"text\" as const, text: \"No diagrams generated yet.\" },\n ],\n };\n }\n\n const lines = Array.from(diagrams.values()).map((d) => {\n const firstLine = d.syntax.split(\"\\n\")[0].trim();\n const parts = [\n `ID: ${d.id}`,\n `Created: ${d.createdAt.toISOString()}`,\n `SVG available: ${d.svg ? \"yes\" : \"no\"}`,\n `Syntax: ${firstLine}`,\n ];\n if (d.previewUrl) {\n parts.push(`Preview: ${d.previewUrl}`);\n }\n if (d.filePath) {\n parts.push(`File: ${d.filePath}`);\n }\n return parts.join(\" | \");\n });\n\n return {\n content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }],\n };\n }\n);\n\n// ── Tool: export_diagram ──\nserver.tool(\n \"export_diagram\",\n \"Write a diagram's SVG to disk. PNG export is available via the browser download buttons.\",\n {\n diagram_id: z.string().describe(\"The ID of the diagram to export\"),\n path: z.string().describe(\"File path to save the SVG file\"),\n },\n async ({ diagram_id, path: outputPath }) => {\n const stored = diagrams.get(diagram_id);\n if (!stored) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Diagram not found: ${diagram_id}. Use list_diagrams to see available diagrams.`,\n },\n ],\n isError: true,\n };\n }\n\n if (!stored.svg) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `SVG not yet available for ${diagram_id}. The browser may still be rendering — try again in a moment.`,\n },\n ],\n isError: true,\n };\n }\n\n const outPath = resolve(outputPath);\n mkdirSync(dirname(outPath), { recursive: true });\n writeFileSync(outPath, stored.svg, \"utf-8\");\n stored.filePath = outPath;\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Exported SVG to: ${outPath}`,\n },\n ],\n };\n }\n);\n\n// ── Start server ──\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n console.error(\"Failed to start MCP server:\", err);\n process.exit(1);\n});\n\n// ── Graceful shutdown ──\nasync function shutdownAllPreviews(): Promise<void> {\n const stops = Array.from(previewServers.values()).map(({ server }) =>\n server.stop()\n );\n await Promise.allSettled(stops);\n previewServers.clear();\n}\n\nprocess.on(\"SIGINT\", async () => {\n await shutdownAllPreviews();\n process.exit(0);\n});\nprocess.on(\"SIGTERM\", async () => {\n await shutdownAllPreviews();\n process.exit(0);\n});\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport { exec } from \"node:child_process\";\n\nconst HTML_PAGE = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Sketchdraw Preview</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>\n :root {\n --bg-base: #0f0f1a;\n --bg-surface: #161625;\n --bg-elevated: #1e1e32;\n --accent: #6c5ce7;\n --accent-hover: #7c6ef7;\n --accent-glow: rgba(108,92,231,0.35);\n --text-primary: #eaeaff;\n --text-secondary: #a0a0c0;\n --text-tertiary: #6a6a8a;\n --border: #2a2a44;\n --border-light: #33335a;\n --green: #4ade80;\n --red: #f87171;\n --radius-sm: 6px;\n --radius-md: 10px;\n --radius-lg: 14px;\n --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);\n --shadow-md: 0 4px 16px rgba(0,0,0,0.4);\n --shadow-lg: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);\n --transition-fast: 0.15s ease;\n --transition-normal: 0.25s ease;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n background: var(--bg-base);\n color: var(--text-primary);\n height: 100vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* ── Header ── */\n header {\n padding: 10px 20px;\n background: rgba(22,22,37,0.82);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border);\n display: flex;\n align-items: center;\n gap: 14px;\n z-index: 10;\n }\n .logo-icon {\n flex-shrink: 0;\n }\n .logo-icon svg { display: block; }\n .header-title { display: flex; flex-direction: column; gap: 1px; }\n .header-title h1 {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-primary);\n line-height: 1.2;\n }\n .header-subtitle {\n font-size: 11px;\n color: var(--text-tertiary);\n display: flex;\n align-items: center;\n gap: 6px;\n line-height: 1.2;\n }\n .status-dot {\n width: 6px; height: 6px;\n border-radius: 50%;\n background: var(--red);\n flex-shrink: 0;\n }\n .status-dot.connected {\n background: var(--green);\n animation: pulse-green 2s ease-in-out infinite;\n }\n @keyframes pulse-green {\n 0%,100% { box-shadow: 0 0 0 0 rgba(74,222,128,0.5); }\n 50% { box-shadow: 0 0 0 4px rgba(74,222,128,0); }\n }\n .status-dot.disconnected {\n background: var(--red);\n }\n\n /* ── Toolbar ── */\n .toolbar {\n display: none;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n }\n .toolbar.visible { display: flex; }\n .toolbar .divider {\n width: 1px; height: 20px;\n background: var(--border);\n margin: 0 6px;\n }\n .btn {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 5px 10px;\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n background: var(--bg-elevated);\n color: var(--text-secondary);\n font-family: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--transition-fast), transform var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);\n white-space: nowrap;\n line-height: 1;\n }\n .btn svg { flex-shrink: 0; }\n .btn:hover {\n background: var(--border);\n color: var(--text-primary);\n transform: translateY(-1px);\n }\n .btn:active { transform: translateY(0); }\n .btn:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n }\n .btn-primary {\n background: var(--accent);\n border-color: var(--accent);\n color: #fff;\n }\n .btn-primary:hover {\n background: var(--accent-hover);\n border-color: var(--accent-hover);\n box-shadow: 0 0 16px var(--accent-glow);\n color: #fff;\n }\n .zoom-display {\n font-size: 11px;\n color: var(--text-tertiary);\n min-width: 36px;\n text-align: center;\n font-variant-numeric: tabular-nums;\n user-select: none;\n }\n\n /* ── Main ── */\n main {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 20px;\n overflow: hidden;\n min-height: 0;\n position: relative;\n }\n\n /* ── Diagram container ── */\n #diagram-container {\n background: #ffffff;\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-lg);\n width: 100%;\n flex: 1;\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 0;\n transition: background var(--transition-normal);\n }\n #diagram-container.dark-canvas {\n background: #1a1a2e;\n }\n #diagram-container.dark-canvas #diagram-viewport svg {\n filter: invert(1) hue-rotate(180deg);\n }\n\n /* ── Viewport (zoom/pan) ── */\n #diagram-viewport {\n flex: 1;\n overflow: hidden;\n cursor: grab;\n display: flex;\n min-height: 0;\n }\n #diagram-viewport.grabbing { cursor: grabbing; }\n .diagram-transform {\n transform-origin: 0 0;\n transition: none;\n }\n .diagram-transform svg {\n display: block;\n }\n .diagram-entrance {\n animation: diagramIn 0.35s ease forwards;\n }\n @keyframes diagramIn {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n /* ── Empty state ── */\n .empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n flex: 1;\n gap: 12px;\n padding: 32px;\n }\n .empty-icon {\n animation: breathe 3s ease-in-out infinite;\n color: var(--text-tertiary);\n }\n @keyframes breathe {\n 0%,100% { transform: translateY(0); }\n 50% { transform: translateY(-8px); }\n }\n .empty-title {\n font-size: 15px;\n font-weight: 500;\n color: var(--text-secondary);\n }\n .empty-subtitle {\n font-size: 12px;\n color: var(--text-tertiary);\n }\n .dots-loading { display: flex; gap: 4px; margin-top: 4px; }\n .dots-loading span {\n width: 5px; height: 5px;\n border-radius: 50%;\n background: var(--text-tertiary);\n animation: dotBounce 1.4s ease-in-out infinite;\n }\n .dots-loading span:nth-child(2) { animation-delay: 0.16s; }\n .dots-loading span:nth-child(3) { animation-delay: 0.32s; }\n @keyframes dotBounce {\n 0%,80%,100% { transform: translateY(0); opacity: 0.4; }\n 40% { transform: translateY(-6px); opacity: 1; }\n }\n\n /* ── Loading state ── */\n .loading-overlay {\n position: absolute;\n inset: 0;\n background: rgba(255,255,255,0.85);\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n z-index: 5;\n opacity: 1;\n transition: opacity 0.3s ease;\n border-radius: var(--radius-lg);\n }\n .loading-overlay.hidden { opacity: 0; pointer-events: none; }\n #diagram-container.dark-canvas .loading-overlay {\n background: rgba(26,26,46,0.85);\n }\n .spinner {\n width: 28px; height: 28px;\n border: 3px solid var(--border);\n border-top-color: var(--accent);\n border-radius: 50%;\n animation: spin 0.7s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .loading-text {\n font-size: 12px;\n color: var(--text-tertiary);\n }\n\n /* ── Error panel ── */\n .error-panel {\n display: none;\n flex-direction: column;\n gap: 10px;\n padding: 20px;\n flex: 1;\n }\n .error-panel.visible { display: flex; }\n .error-header {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--red);\n font-size: 14px;\n font-weight: 600;\n }\n .error-message {\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 12px;\n color: var(--text-secondary);\n background: var(--bg-base);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n padding: 12px;\n overflow: auto;\n max-height: 200px;\n white-space: pre-wrap;\n word-break: break-word;\n }\n .error-dismiss {\n align-self: flex-start;\n }\n\n /* ── Info bar ── */\n .info-bar {\n display: none;\n padding: 6px 14px;\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--text-tertiary);\n border-top: 1px solid var(--border);\n gap: 16px;\n background: rgba(0,0,0,0.1);\n border-radius: 0 0 var(--radius-lg) var(--radius-lg);\n }\n .info-bar.visible { display: flex; }\n\n /* ── Shortcuts overlay ── */\n .shortcuts-overlay {\n display: none;\n position: fixed;\n inset: 0;\n z-index: 100;\n background: rgba(0,0,0,0.6);\n align-items: center;\n justify-content: center;\n backdrop-filter: blur(4px);\n }\n .shortcuts-overlay.visible { display: flex; }\n .shortcuts-panel {\n background: var(--bg-surface);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n padding: 24px;\n max-width: 360px;\n width: 90%;\n box-shadow: var(--shadow-lg);\n }\n .shortcuts-panel h3 {\n font-size: 14px;\n font-weight: 600;\n margin-bottom: 14px;\n color: var(--text-primary);\n }\n .shortcut-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 5px 0;\n font-size: 12px;\n }\n .shortcut-row span:first-child { color: var(--text-secondary); }\n .shortcut-keys {\n display: flex; gap: 3px;\n }\n kbd {\n background: var(--bg-elevated);\n border: 1px solid var(--border);\n border-radius: 4px;\n padding: 2px 6px;\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--text-primary);\n }\n .shortcuts-close {\n margin-top: 16px;\n width: 100%;\n }\n\n /* ── Responsive ── */\n @media (max-width: 640px) {\n header { flex-wrap: wrap; padding: 8px 12px; gap: 8px; }\n .toolbar { gap: 3px; }\n .btn .btn-label { display: none; }\n .btn { padding: 5px 7px; }\n .divider { display: none; }\n main { padding: 12px; }\n }\n @media (max-width: 400px) {\n .zoom-display { display: none; }\n }\n </style>\n <script src=\"https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js\"><\\/script>\n</head>\n<body>\n <header>\n <div class=\"logo-icon\">\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#6c5ce7\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z\"/>\n <path d=\"m15 5 4 4\"/>\n </svg>\n </div>\n <div class=\"header-title\">\n <h1 id=\"page-title\">Sketchdraw Preview</h1>\n <div class=\"header-subtitle\">\n <span class=\"status-dot disconnected\" id=\"status-dot\"></span>\n <span id=\"status-text\">Connecting</span>\n <span id=\"diagram-id-label\"></span>\n </div>\n </div>\n <div class=\"toolbar\" id=\"toolbar\">\n <button class=\"btn\" id=\"zoom-out\" title=\"Zoom out (-)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n <span class=\"zoom-display\" id=\"zoom-display\">100%</span>\n <button class=\"btn\" id=\"zoom-in\" title=\"Zoom in (+)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n <button class=\"btn\" id=\"zoom-fit\" title=\"Reset zoom (0)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 3h6v6\"/><path d=\"M9 21H3v-6\"/><path d=\"m21 3-7 7\"/><path d=\"m3 21 7-7\"/></svg>\n </button>\n <div class=\"divider\"></div>\n <button class=\"btn\" id=\"theme-toggle\" title=\"Toggle canvas theme (T)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" id=\"theme-icon-sun\"><circle cx=\"12\" cy=\"12\" r=\"5\"/><line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\"/><line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\"/><line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\"/><line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\"/><line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\"/><line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\"/><line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\"/><line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\"/></svg>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" id=\"theme-icon-moon\" style=\"display:none\"><path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\"/></svg>\n </button>\n <div class=\"divider\"></div>\n <button class=\"btn\" id=\"download-svg\" title=\"Download SVG (Cmd+S)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>\n <span class=\"btn-label\">SVG</span>\n </button>\n <button class=\"btn btn-primary\" id=\"download-png\" title=\"Download PNG (Cmd+Shift+S)\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>\n <span class=\"btn-label\">PNG</span>\n </button>\n </div>\n </header>\n\n <main>\n <div id=\"diagram-container\">\n <!-- Empty state -->\n <div class=\"empty-state\" id=\"empty-state\">\n <div class=\"empty-icon\">\n <svg width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/>\n <path d=\"M3 9h18\"/><path d=\"M9 21V9\"/>\n </svg>\n </div>\n <div class=\"empty-title\">Waiting for a diagram...</div>\n <div class=\"empty-subtitle\">Use generate_diagram to send a diagram here</div>\n <div class=\"dots-loading\"><span></span><span></span><span></span></div>\n </div>\n\n <!-- Diagram viewport -->\n <div id=\"diagram-viewport\" style=\"display:none;\">\n <div class=\"diagram-transform\" id=\"diagram-transform\"></div>\n </div>\n\n <!-- Loading overlay -->\n <div class=\"loading-overlay hidden\" id=\"loading-overlay\">\n <div class=\"spinner\"></div>\n <div class=\"loading-text\">Rendering diagram...</div>\n </div>\n\n <!-- Error panel -->\n <div class=\"error-panel\" id=\"error-panel\">\n <div class=\"error-header\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>\n Render Error\n </div>\n <div class=\"error-message\" id=\"error-message\"></div>\n <button class=\"btn error-dismiss\" id=\"error-dismiss\">Dismiss</button>\n </div>\n\n <!-- Info bar -->\n <div class=\"info-bar\" id=\"info-bar\">\n <span id=\"info-id\"></span>\n <span id=\"info-type\"></span>\n <span id=\"info-time\"></span>\n </div>\n </div>\n </main>\n\n <!-- Shortcuts overlay -->\n <div class=\"shortcuts-overlay\" id=\"shortcuts-overlay\">\n <div class=\"shortcuts-panel\">\n <h3>Keyboard Shortcuts</h3>\n <div class=\"shortcut-row\"><span>Download SVG</span><span class=\"shortcut-keys\"><kbd>Cmd</kbd><kbd>S</kbd></span></div>\n <div class=\"shortcut-row\"><span>Download PNG</span><span class=\"shortcut-keys\"><kbd>Cmd</kbd><kbd>Shift</kbd><kbd>S</kbd></span></div>\n <div class=\"shortcut-row\"><span>Zoom in</span><span class=\"shortcut-keys\"><kbd>+</kbd></span></div>\n <div class=\"shortcut-row\"><span>Zoom out</span><span class=\"shortcut-keys\"><kbd>-</kbd></span></div>\n <div class=\"shortcut-row\"><span>Reset zoom</span><span class=\"shortcut-keys\"><kbd>0</kbd></span></div>\n <div class=\"shortcut-row\"><span>Toggle theme</span><span class=\"shortcut-keys\"><kbd>T</kbd></span></div>\n <div class=\"shortcut-row\"><span>Show shortcuts</span><span class=\"shortcut-keys\"><kbd>?</kbd></span></div>\n <button class=\"btn shortcuts-close\" id=\"shortcuts-close\">Close</button>\n </div>\n </div>\n\n <script>\n mermaid.initialize({ startOnLoad: false, theme: 'default' });\n\n /* ── DOM refs ── */\n const diagramContainer = document.getElementById('diagram-container');\n const viewport = document.getElementById('diagram-viewport');\n const transformEl = document.getElementById('diagram-transform');\n const emptyState = document.getElementById('empty-state');\n const loadingOverlay = document.getElementById('loading-overlay');\n const errorPanel = document.getElementById('error-panel');\n const errorMessage = document.getElementById('error-message');\n const toolbar = document.getElementById('toolbar');\n const statusDot = document.getElementById('status-dot');\n const statusText = document.getElementById('status-text');\n const diagramIdLabel = document.getElementById('diagram-id-label');\n const zoomDisplay = document.getElementById('zoom-display');\n const infoBar = document.getElementById('info-bar');\n const infoId = document.getElementById('info-id');\n const infoType = document.getElementById('info-type');\n const infoTime = document.getElementById('info-time');\n const shortcutsOverlay = document.getElementById('shortcuts-overlay');\n const themeIconSun = document.getElementById('theme-icon-sun');\n const themeIconMoon = document.getElementById('theme-icon-moon');\n\n /* ── State ── */\n let ws;\n let currentDiagramId = null;\n let zoom = 1;\n let panX = 0, panY = 0;\n let svgBaseWidth = 0, svgBaseHeight = 0;\n let isPanning = false;\n let panStartX = 0, panStartY = 0;\n let darkCanvas = localStorage.getItem('sketchdraw-dark-canvas') === 'true';\n\n const ZOOM_MIN = 0.1;\n const ZOOM_MAX = 5;\n const ZOOM_STEP = 0.15;\n\n /* ── Init canvas theme ── */\n if (darkCanvas) {\n diagramContainer.classList.add('dark-canvas');\n themeIconSun.style.display = 'none';\n themeIconMoon.style.display = '';\n }\n\n /* ── Helpers ── */\n function applyTransform() {\n var svg = transformEl.querySelector('svg');\n if (svg) {\n svg.setAttribute('width', svgBaseWidth * zoom);\n svg.setAttribute('height', svgBaseHeight * zoom);\n }\n transformEl.style.transform = 'translate(' + panX + 'px,' + panY + 'px)';\n zoomDisplay.textContent = Math.round(zoom * 100) + '%';\n }\n\n function fitToViewport() {\n if (!svgBaseWidth || !svgBaseHeight) return;\n var rect = viewport.getBoundingClientRect();\n var pad = 32;\n var availW = rect.width - pad * 2;\n var availH = rect.height - pad * 2;\n if (availW <= 0 || availH <= 0) return;\n var scaleX = availW / svgBaseWidth;\n var scaleY = availH / svgBaseHeight;\n zoom = Math.min(scaleX, scaleY);\n var renderedW = svgBaseWidth * zoom;\n var renderedH = svgBaseHeight * zoom;\n panX = (rect.width - renderedW) / 2;\n panY = (rect.height - renderedH) / 2;\n applyTransform();\n }\n\n transformEl.addEventListener('animationend', function() {\n transformEl.classList.remove('diagram-entrance');\n });\n\n function setZoom(newZoom, centerX, centerY) {\n const clamped = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, newZoom));\n if (centerX !== undefined && centerY !== undefined) {\n const ratio = clamped / zoom;\n panX = centerX - ratio * (centerX - panX);\n panY = centerY - ratio * (centerY - panY);\n }\n zoom = clamped;\n applyTransform();\n }\n\n function resetZoom() {\n fitToViewport();\n }\n\n function showState(state) {\n emptyState.style.display = state === 'empty' ? 'flex' : 'none';\n viewport.style.display = state === 'diagram' ? 'flex' : 'none';\n errorPanel.classList.toggle('visible', state === 'error');\n if (state === 'diagram') {\n loadingOverlay.classList.remove('hidden');\n }\n if (state !== 'diagram') {\n infoBar.classList.remove('visible');\n }\n }\n\n function showToolbar() {\n toolbar.classList.add('visible');\n }\n\n function detectDiagramType(syntax) {\n const first = syntax.trim().split('\\\\n')[0].trim().toLowerCase();\n const types = ['graph','flowchart','sequenceDiagram','classDiagram','stateDiagram','erDiagram','journey','gantt','pie','quadrantChart','requirementDiagram','gitGraph','mindmap','timeline','sankey','xychart'];\n for (const t of types) {\n if (first.startsWith(t.toLowerCase())) return t;\n }\n return 'diagram';\n }\n\n function formatTime(d) {\n return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n }\n\n /* ── Canvas theme toggle ── */\n function toggleCanvasTheme() {\n darkCanvas = !darkCanvas;\n diagramContainer.classList.toggle('dark-canvas', darkCanvas);\n themeIconSun.style.display = darkCanvas ? 'none' : '';\n themeIconMoon.style.display = darkCanvas ? '' : 'none';\n localStorage.setItem('sketchdraw-dark-canvas', darkCanvas);\n }\n document.getElementById('theme-toggle').addEventListener('click', toggleCanvasTheme);\n\n /* ── Zoom buttons ── */\n document.getElementById('zoom-in').addEventListener('click', function() { setZoom(zoom + ZOOM_STEP); });\n document.getElementById('zoom-out').addEventListener('click', function() { setZoom(zoom - ZOOM_STEP); });\n document.getElementById('zoom-fit').addEventListener('click', resetZoom);\n\n /* ── Mouse wheel zoom ── */\n viewport.addEventListener('wheel', function(e) {\n e.preventDefault();\n var rect = viewport.getBoundingClientRect();\n var cx = e.clientX - rect.left;\n var cy = e.clientY - rect.top;\n var delta = -e.deltaY * 0.002;\n setZoom(zoom + delta, cx, cy);\n }, { passive: false });\n\n /* ── Pan with mouse ── */\n viewport.addEventListener('mousedown', function(e) {\n if (e.button !== 0) return;\n isPanning = true;\n panStartX = e.clientX - panX;\n panStartY = e.clientY - panY;\n viewport.classList.add('grabbing');\n e.preventDefault();\n });\n window.addEventListener('mousemove', function(e) {\n if (!isPanning) return;\n panX = e.clientX - panStartX;\n panY = e.clientY - panStartY;\n applyTransform();\n });\n window.addEventListener('mouseup', function() {\n if (!isPanning) return;\n isPanning = false;\n viewport.classList.remove('grabbing');\n });\n\n /* ── Touch pan ── */\n var touchId = null;\n viewport.addEventListener('touchstart', function(e) {\n if (e.touches.length === 1) {\n var t = e.touches[0];\n touchId = t.identifier;\n panStartX = t.clientX - panX;\n panStartY = t.clientY - panY;\n viewport.classList.add('grabbing');\n }\n }, { passive: true });\n viewport.addEventListener('touchmove', function(e) {\n for (var i = 0; i < e.changedTouches.length; i++) {\n var t = e.changedTouches[i];\n if (t.identifier === touchId) {\n panX = t.clientX - panStartX;\n panY = t.clientY - panStartY;\n applyTransform();\n break;\n }\n }\n }, { passive: true });\n viewport.addEventListener('touchend', function(e) {\n for (var i = 0; i < e.changedTouches.length; i++) {\n if (e.changedTouches[i].identifier === touchId) {\n touchId = null;\n viewport.classList.remove('grabbing');\n break;\n }\n }\n }, { passive: true });\n\n /* ── Error dismiss ── */\n document.getElementById('error-dismiss').addEventListener('click', function() {\n showState('empty');\n });\n\n /* ── Shortcuts overlay ── */\n document.getElementById('shortcuts-close').addEventListener('click', function() {\n shortcutsOverlay.classList.remove('visible');\n });\n shortcutsOverlay.addEventListener('click', function(e) {\n if (e.target === shortcutsOverlay) shortcutsOverlay.classList.remove('visible');\n });\n\n /* ── Download SVG ── */\n function downloadSvg() {\n var svg = transformEl.querySelector('svg');\n if (!svg) return;\n var clone = svg.cloneNode(true);\n clone.setAttribute('width', svgBaseWidth);\n clone.setAttribute('height', svgBaseHeight);\n var blob = new Blob([clone.outerHTML], { type: 'image/svg+xml' });\n var url = URL.createObjectURL(blob);\n var a = document.createElement('a');\n a.href = url;\n a.download = (currentDiagramId || 'diagram') + '.svg';\n a.click();\n URL.revokeObjectURL(url);\n }\n\n /* ── Download PNG ── */\n function downloadPng() {\n var svg = transformEl.querySelector('svg');\n if (!svg) return;\n var clone = svg.cloneNode(true);\n var w = svgBaseWidth;\n var h = svgBaseHeight;\n clone.setAttribute('width', w);\n clone.setAttribute('height', h);\n if (diagramContainer.classList.contains('dark-canvas')) {\n clone.style.filter = 'invert(1) hue-rotate(180deg)';\n }\n var svgData = new XMLSerializer().serializeToString(clone);\n var dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);\n var img = new Image();\n img.onload = function() {\n var scale = 2;\n var canvas = document.createElement('canvas');\n canvas.width = w * scale;\n canvas.height = h * scale;\n var ctx = canvas.getContext('2d');\n ctx.scale(scale, scale);\n ctx.drawImage(img, 0, 0, w, h);\n canvas.toBlob(function(blob) {\n if (!blob) return;\n var pngUrl = URL.createObjectURL(blob);\n var a = document.createElement('a');\n a.href = pngUrl;\n a.download = (currentDiagramId || 'diagram') + '.png';\n a.click();\n URL.revokeObjectURL(pngUrl);\n }, 'image/png');\n };\n img.src = dataUrl;\n }\n\n document.getElementById('download-svg').addEventListener('click', downloadSvg);\n document.getElementById('download-png').addEventListener('click', downloadPng);\n\n /* ── Keyboard shortcuts ── */\n document.addEventListener('keydown', function(e) {\n var mod = e.metaKey || e.ctrlKey;\n\n if (mod && e.shiftKey && e.key.toLowerCase() === 's') {\n e.preventDefault();\n downloadPng();\n return;\n }\n if (mod && e.key.toLowerCase() === 's') {\n e.preventDefault();\n downloadSvg();\n return;\n }\n\n if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;\n\n if (e.key === '+' || e.key === '=') { setZoom(zoom + ZOOM_STEP); e.preventDefault(); }\n else if (e.key === '-') { setZoom(zoom - ZOOM_STEP); e.preventDefault(); }\n else if (e.key === '0') { resetZoom(); e.preventDefault(); }\n else if (e.key.toLowerCase() === 't' && !mod) { toggleCanvasTheme(); e.preventDefault(); }\n else if (e.key === '?' || (e.shiftKey && e.key === '/')) {\n shortcutsOverlay.classList.toggle('visible');\n e.preventDefault();\n }\n else if (e.key === 'Escape') {\n shortcutsOverlay.classList.remove('visible');\n }\n });\n\n /* ── WebSocket connection ── */\n function connect() {\n ws = new WebSocket('ws://' + location.host + '/ws');\n\n ws.onopen = function() {\n statusDot.className = 'status-dot connected';\n statusText.textContent = 'Connected';\n };\n\n ws.onmessage = async function(event) {\n var data = JSON.parse(event.data);\n if (data.type === 'title') {\n document.title = data.title;\n document.getElementById('page-title').textContent = data.title;\n } else if (data.type === 'mermaid') {\n currentDiagramId = data.id;\n diagramIdLabel.textContent = data.id ? ' · ' + data.id : '';\n\n showState('diagram');\n\n try {\n var result = await mermaid.render('mermaid-output', data.syntax);\n transformEl.innerHTML = result.svg;\n\n var svg = transformEl.querySelector('svg');\n if (svg) {\n var vb = svg.viewBox.baseVal;\n if (vb && vb.width) {\n svgBaseWidth = vb.width;\n svgBaseHeight = vb.height;\n } else {\n svgBaseWidth = svg.width.baseVal.value || svg.getBoundingClientRect().width;\n svgBaseHeight = svg.height.baseVal.value || svg.getBoundingClientRect().height;\n }\n }\n\n fitToViewport();\n transformEl.classList.remove('diagram-entrance');\n void transformEl.offsetWidth;\n transformEl.classList.add('diagram-entrance');\n loadingOverlay.classList.add('hidden');\n showToolbar();\n\n /* Info bar */\n infoId.textContent = 'ID: ' + (data.id || '–');\n infoType.textContent = 'Type: ' + detectDiagramType(data.syntax);\n infoTime.textContent = 'Updated: ' + formatTime(new Date());\n infoBar.classList.add('visible');\n\n if (ws.readyState === 1) {\n ws.send(JSON.stringify({ type: 'svg-result', id: data.id, svg: result.svg }));\n }\n } catch (err) {\n showState('error');\n errorMessage.textContent = err.message || String(err);\n }\n }\n };\n\n ws.onclose = function() {\n statusDot.className = 'status-dot disconnected';\n statusText.textContent = 'Disconnected';\n setTimeout(connect, 2000);\n };\n\n ws.onerror = function() { ws.close(); };\n }\n\n connect();\n <\\/script>\n</body>\n</html>`;\n\nexport class PreviewServer {\n private httpServer: ReturnType<typeof createServer>;\n private wss: WebSocketServer;\n private clients: Set<WebSocket> = new Set();\n private currentContent:\n | { type: \"mermaid\"; syntax: string; id: string }\n | null = null;\n private port: number;\n private title: string = \"Sketchdraw Preview\";\n public onSvgRendered: ((id: string, svg: string) => void) | null = null;\n\n constructor(port = 0) {\n this.port = port;\n\n this.httpServer = createServer(\n (req: IncomingMessage, res: ServerResponse) => {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(HTML_PAGE);\n }\n );\n\n this.wss = new WebSocketServer({ server: this.httpServer, path: \"/ws\" });\n\n this.wss.on(\"connection\", (ws: WebSocket) => {\n this.clients.add(ws);\n ws.send(JSON.stringify({ type: \"title\", title: this.title }));\n if (this.currentContent) {\n ws.send(JSON.stringify(this.currentContent));\n }\n ws.on(\"message\", (raw: Buffer | string) => {\n try {\n const msg = JSON.parse(typeof raw === \"string\" ? raw : raw.toString());\n if (msg.type === \"svg-result\" && msg.id && msg.svg) {\n this.onSvgRendered?.(msg.id, msg.svg);\n }\n } catch {\n // ignore malformed messages\n }\n });\n ws.on(\"close\", () => {\n this.clients.delete(ws);\n });\n });\n }\n\n async start(): Promise<string> {\n return new Promise((resolve, reject) => {\n this.httpServer.listen(this.port, () => {\n const addr = this.httpServer.address();\n if (addr && typeof addr === \"object\") {\n this.port = addr.port;\n }\n const url = `http://localhost:${this.port}`;\n resolve(url);\n });\n this.httpServer.on(\"error\", reject);\n });\n }\n\n private broadcast(message: object): void {\n const data = JSON.stringify(message);\n for (const client of this.clients) {\n if (client.readyState === 1) {\n client.send(data);\n }\n }\n }\n\n setTitle(title: string): void {\n this.title = title;\n this.broadcast({ type: \"title\", title });\n }\n\n updateMermaid(id: string, syntax: string, title?: string): void {\n if (title) this.setTitle(title);\n this.currentContent = { type: \"mermaid\", syntax, id };\n this.broadcast(this.currentContent);\n }\n\n openBrowser(): void {\n const url = `http://localhost:${this.port}`;\n const platform = process.platform;\n const cmd =\n platform === \"darwin\"\n ? \"open\"\n : platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${cmd} ${url}`);\n }\n\n async stop(): Promise<void> {\n for (const client of this.clients) {\n client.close();\n }\n this.wss.close();\n return new Promise((resolve) => {\n this.httpServer.close(() => resolve());\n });\n }\n}\n"],"mappings":"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","names":["resolve","server"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mermaid-live-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for generating Mermaid diagrams with live browser preview",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mermaid-live-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "pnpm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"mermaid",
|
|
26
|
+
"diagram",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"claude",
|
|
29
|
+
"cursor",
|
|
30
|
+
"live-preview"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/iishyfishyy/mermaid-live-mcp.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/iishyfishyy/mermaid-live-mcp#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/iishyfishyy/mermaid-live-mcp/issues"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
43
|
+
"ws": "^8.18.0",
|
|
44
|
+
"zod": "^3.24.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@sketchdraw/preview": "workspace:*",
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"typescript": "^5.7.0"
|
|
51
|
+
}
|
|
52
|
+
}
|