Skip to main content
Glama

claude-mermaid

handlers.ts7.32 kB
import { execFile } from "child_process"; import { promisify } from "util"; import { writeFile, mkdir, copyFile, access } from "fs/promises"; import { join, dirname } from "path"; import { tmpdir } from "os"; import { ensureLiveServer, addLiveDiagram, hasActiveConnections } from "./live-server.js"; import { getDiagramFilePath, getPreviewDir, saveDiagramSource, loadDiagramSource, loadDiagramOptions, validateSavePath, } from "./file-utils.js"; import { mcpLogger } from "./logger.js"; const execFileAsync = promisify(execFile); interface RenderOptions { diagram: string; previewId: string; format: string; theme: string; background: string; width: number; height: number; scale: number; } function getOpenCommand(): string { return process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; } async function renderDiagram(options: RenderOptions, liveFilePath: string): Promise<void> { const { diagram, previewId, format, theme, background, width, height, scale } = options; mcpLogger.info(`Rendering diagram: ${previewId}`, { format, theme, width, height }); const tempDir = join(tmpdir(), "claude-mermaid"); await mkdir(tempDir, { recursive: true }); const inputFile = join(tempDir, `diagram-${previewId}.mmd`); const outputFile = join(tempDir, `diagram-${previewId}.${format}`); await writeFile(inputFile, diagram, "utf-8"); const args = [ "-y", "mmdc", "-i", inputFile, "-o", outputFile, "-t", theme, "-b", background, "-w", width.toString(), "-H", height.toString(), "-s", scale.toString(), ]; if (format === "pdf") { args.push("--pdfFit"); } mcpLogger.debug(`Executing mermaid-cli`, { args }); try { const { stdout, stderr } = await execFileAsync("npx", args); if (stderr) { mcpLogger.debug(`mermaid-cli stderr`, { stderr }); } await copyFile(outputFile, liveFilePath); mcpLogger.info(`Diagram rendered successfully: ${previewId}`); } catch (error) { mcpLogger.error(`Diagram rendering failed: ${previewId}`, { error: error instanceof Error ? error.message : String(error), }); throw error; } } async function setupLivePreview( previewId: string, liveFilePath: string ): Promise<{ serverUrl: string; hasConnections: boolean }> { const port = await ensureLiveServer(); const hasConnections = hasActiveConnections(previewId); await addLiveDiagram(previewId, liveFilePath); const serverUrl = `http://localhost:${port}/${previewId}`; if (!hasConnections) { mcpLogger.info(`Opening browser for new diagram: ${previewId}`, { serverUrl }); const openCommand = getOpenCommand(); await execFileAsync(openCommand, [serverUrl]); } else { mcpLogger.info(`Reusing existing browser tab for diagram: ${previewId}`); } return { serverUrl, hasConnections }; } function createLivePreviewResponse( liveFilePath: string, format: string, serverUrl: string, hasConnections: boolean ): any { const actionMessage = hasConnections ? `Mermaid diagram updated successfully.` : `Mermaid diagram rendered successfully and opened in browser.`; const liveMessage = hasConnections ? `\nDiagram updated. Browser will refresh automatically.` : `\nLive reload URL: ${serverUrl}\nThe diagram will auto-refresh when you update it.`; return { content: [ { type: "text", text: `${actionMessage}\nWorking file: ${liveFilePath} (${format.toUpperCase()})${liveMessage}`, }, ], }; } function createStaticRenderResponse(liveFilePath: string, format: string): any { return { content: [ { type: "text", text: `Mermaid diagram rendered successfully.\nWorking file: ${liveFilePath} (${format.toUpperCase()})\n\nNote: Live preview is only available for SVG format. Use mermaid_save to save this diagram to a permanent location.`, }, ], }; } export async function handleMermaidPreview(args: any) { const diagram = args.diagram as string; const previewId = args.preview_id as string; const format = (args.format as string) || "svg"; const theme = (args.theme as string) || "default"; const background = (args.background as string) || "white"; const width = (args.width as number) || 800; const height = (args.height as number) || 600; const scale = (args.scale as number) || 2; if (!diagram) { throw new Error("diagram parameter is required"); } if (!previewId) { throw new Error("preview_id parameter is required"); } const previewDir = getPreviewDir(previewId); await mkdir(previewDir, { recursive: true }); const liveFilePath = getDiagramFilePath(previewId, format); try { await saveDiagramSource(previewId, diagram, { theme, background, width, height, scale }); await renderDiagram( { diagram, previewId, format, theme, background, width, height, scale }, liveFilePath ); if (format === "svg") { const { serverUrl, hasConnections } = await setupLivePreview(previewId, liveFilePath); return createLivePreviewResponse(liveFilePath, format, serverUrl, hasConnections); } else { return createStaticRenderResponse(liveFilePath, format); } } catch (error) { return { content: [ { type: "text", text: `Error rendering Mermaid diagram: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } export async function handleMermaidSave(args: any) { const savePath = args.save_path as string; const previewId = args.preview_id as string; const format = (args.format as string) || "svg"; if (!savePath) { throw new Error("save_path parameter is required"); } if (!previewId) { throw new Error("preview_id parameter is required"); } // Validate save path to prevent path traversal attacks try { validateSavePath(savePath); } catch (error) { mcpLogger.error("Save path validation failed", { savePath, error: error instanceof Error ? error.message : String(error), }); return { content: [ { type: "text", text: `Invalid save path: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } try { const liveFilePath = getDiagramFilePath(previewId, format); try { await access(liveFilePath); } catch { const diagram = await loadDiagramSource(previewId); const options = await loadDiagramOptions(previewId); await renderDiagram( { diagram, previewId, format, ...options, }, liveFilePath ); } const saveDir = dirname(savePath); await mkdir(saveDir, { recursive: true }); await copyFile(liveFilePath, savePath); return { content: [ { type: "text", text: `Diagram saved to: ${savePath} (${format.toUpperCase()})`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error saving diagram: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/veelenga/claude-mermaid'

If you have feedback or need assistance with the MCP directory API, please join our Discord server