main 81dc507991c6 cached
10 files
38.6 KB
10.0k tokens
13 symbols
1 requests
Download .txt
Repository: peng-shawn/mermaid-mcp-server
Branch: main
Commit: 81dc507991c6
Files: 10
Total size: 38.6 KB

Directory structure:
gitextract_pz7yyjpu/

├── .github/
│   └── workflows/
│       └── npm-publish.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── index.ts
├── package.json
├── scripts/
│   └── release.sh
├── smithery.yaml
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/npm-publish.yml
================================================
name: Publish to npm

on:
  push:
    tags:
      - 'v*' # Run workflow on version tags, e.g. v1.0.0

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Publish to npm
        run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 

================================================
FILE: .gitignore
================================================
node_modules
dist
.cursor/*

================================================
FILE: Dockerfile
================================================
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use node:lts-slim (Debian-based) instead of Alpine for better Chrome compatibility
FROM node:lts-slim

# Set working directory
WORKDIR /app

# Install Chromium with its dependencies
RUN apt-get update && apt-get install -y \
    chromium \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# Copy package files and install dependencies
# Use PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true to avoid downloading Chromium again
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
COPY package.json package-lock.json ./
RUN npm install

# Copy the rest of the application files
COPY . .

# Build the TypeScript code
RUN npm run build

# Command to run the MCP server
CMD [ "node", "dist/index.js" ]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Shawn Peng

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Mermaid MCP Server

A Model Context Protocol (MCP) server that converts Mermaid diagrams to PNG images or SVG files. This server allows AI assistants and other applications to generate visual diagrams from textual descriptions using the Mermaid markdown syntax.

## Features

- Converts Mermaid diagram code to PNG images or SVG files
- Supports multiple diagram themes (default, forest, dark, neutral)
- Customizable background colors
- Uses Puppeteer for high-quality headless browser rendering
- Implements the MCP protocol for seamless integration with AI assistants
- Flexible output options: return images/SVG directly or save to disk
- Error handling with detailed error messages

## How It Works

The server uses Puppeteer to launch a headless browser, render the Mermaid diagram to SVG, and optionally capture a screenshot of the rendered diagram. The process involves:

1. Launching a headless browser instance
2. Creating an HTML template with the Mermaid code
3. Loading the Mermaid.js library
4. Rendering the diagram to SVG
5. Either saving the SVG directly or taking a screenshot as PNG
6. Either returning the image/SVG directly or saving it to disk

## Build

```bash
npx tsc
```

## Usage

### Use with Claude desktop

```json
{
  "mcpServers": {
    "mermaid": {
      "command": "npx",
      "args": ["-y", "@peng-shawn/mermaid-mcp-server"]
    }
  }
}
```

### Use with Cursor and Cline

```bash
env CONTENT_IMAGE_SUPPORTED=false npx -y @peng-shawn/mermaid-mcp-server
```

You can find a list of mermaid diagrams under `./diagrams`, they are created using Cursor agent with prompt: "generate mermaid diagrams and save them in a separate diagrams folder explaining how renderMermaidPng work"

### Run with inspector

Run the server with inspector for testing and debugging:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

The server will start and listen on stdio for MCP protocol messages.

Learn more about inspector [here](https://modelcontextprotocol.io/docs/tools/inspector).

### Installing via Smithery

To install Mermaid Diagram Generator for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@peng-shawn/mermaid-mcp-server):

```bash
npx -y @smithery/cli install @peng-shawn/mermaid-mcp-server --client claude
```

### Docker and Smithery Environments

When running in Docker containers (including via Smithery), you may need to handle Chrome dependencies:

1. The server now attempts to use Puppeteer's bundled browser by default
2. If you encounter browser-related errors, you have two options:

   **Option 1: During Docker image build:**

   - Set `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true` when installing Puppeteer
   - Install Chrome/Chromium in your Docker container
   - Set `PUPPETEER_EXECUTABLE_PATH` at runtime to point to the Chrome installation

   **Option 2: Use Puppeteer's bundled Chrome:**

   - Ensure your Docker container has the necessary dependencies for Chrome
   - No need to set `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`
   - The code will use the bundled browser automatically

For Smithery users, the latest version should work without additional configuration.

## API

The server exposes a single tool:

- `generate`: Converts Mermaid diagram code to a PNG image or SVG file
  - Parameters:
    - `code`: The Mermaid diagram code to render
    - `theme`: (optional) Theme for the diagram. Options: "default", "forest", "dark", "neutral"
    - `backgroundColor`: (optional) Background color for the diagram, e.g. 'white', 'transparent', '#F0F0F0'
    - `outputFormat`: (optional) Output format for the diagram. Options: "png", "svg" (defaults to "png")
    - `name`: Name for the generated file (required when CONTENT_IMAGE_SUPPORTED=false)
    - `folder`: Absolute path to save the image/SVG to (required when CONTENT_IMAGE_SUPPORTED=false)

The behavior of the `generate` tool depends on the `CONTENT_IMAGE_SUPPORTED` environment variable:

- When `CONTENT_IMAGE_SUPPORTED=true` (default): The tool returns the image/SVG directly in the response
- When `CONTENT_IMAGE_SUPPORTED=false`: The tool saves the image/SVG to the specified folder and returns the file path

## Environment Variables

- `CONTENT_IMAGE_SUPPORTED`: Controls whether images are returned directly in the response or saved to disk
  - `true` (default): Images are returned directly in the response
  - `false`: Images are saved to disk, requiring `name` and `folder` parameters

## Examples

### Basic Usage

```javascript
// Generate a flowchart with default settings
{
  "code": "flowchart TD\n    A[Start] --> B{Is it?}\n    B -->|Yes| C[OK]\n    B -->|No| D[End]"
}
```

### With Theme and Background Color

```javascript
// Generate a sequence diagram with forest theme and light gray background
{
  "code": "sequenceDiagram\n    Alice->>John: Hello John, how are you?\n    John-->>Alice: Great!",
  "theme": "forest",
  "backgroundColor": "#F0F0F0"
}
```

### Saving to Disk (when CONTENT_IMAGE_SUPPORTED=false)

```javascript
// Generate a class diagram and save it to disk as PNG
{
  "code": "classDiagram\n    Class01 <|-- AveryLongClass\n    Class03 *-- Class04\n    Class05 o-- Class06",
  "theme": "dark",
  "name": "class_diagram",
  "folder": "/path/to/diagrams"
}
```

### Generating SVG Output

```javascript
// Generate a state diagram as SVG
{
  "code": "stateDiagram-v2\n    [*] --> Still\n    Still --> [*]\n    Still --> Moving\n    Moving --> Still\n    Moving --> Crash\n    Crash --> [*]",
  "outputFormat": "svg",
  "name": "state_diagram",
  "folder": "/path/to/diagrams"
}
```

## FAQ

### Doesn't Claude desktop already support mermaid via canvas?

Yes, but it doesn't support the `theme` and `backgroundColor` options. Plus, having a dedicated server makes it easier to create mermaid diagrams with different MCP clients.

### Why do I need to specify CONTENT_IMAGE_SUPPORTED=false when using with Cursor?

Cursor doesn't support inline images in responses yet.

## Publishing

This project uses GitHub Actions to automate the publishing process to npm.

### Method 1: Using the Release Script (Recommended)

1. Make sure all your changes are committed and pushed
2. Run the release script with either a specific version number or a semantic version increment:

   ```bash
   # Using a specific version number
   npm run release 0.1.4

   # Using semantic version increments
   npm run release patch  # Increments the patch version (e.g., 0.1.3 → 0.1.4)
   npm run release minor  # Increments the minor version (e.g., 0.1.3 → 0.2.0)
   npm run release major  # Increments the major version (e.g., 0.1.3 → 1.0.0)
   ```

3. The script will:
   - Validate the version format or semantic increment
   - Check if you're on the main branch
   - Detect and warn about version mismatches between files
   - Update all version references consistently (package.json, package-lock.json, and index.ts)
   - Create a single commit with all version changes
   - Create and push a git tag
   - The GitHub workflow will then automatically build and publish to npm

### Method 2: Manual Process

1. Update your code and commit the changes
2. Create and push a new tag with the version number:
   ```bash
   git tag v0.1.4  # Use the appropriate version number
   git push origin v0.1.4
   ```
3. The GitHub workflow will automatically:
   - Build the project
   - Publish to npm with the version from the tag

Note: You need to set up the `NPM_TOKEN` secret in your GitHub repository settings. To do this:

1. Generate an npm access token with publish permissions
2. Go to your GitHub repository → Settings → Secrets and variables → Actions
3. Create a new repository secret named `NPM_TOKEN` with your npm token as the value

## Badges

[![smithery badge](https://smithery.ai/badge/@peng-shawn/mermaid-mcp-server)](https://smithery.ai/server/@peng-shawn/mermaid-mcp-server)

<a href="https://glama.ai/mcp/servers/lzjlbitkzr">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/lzjlbitkzr/badge" alt="mermaid-mcp-server MCP server" />
</a>

## License

MIT


================================================
FILE: index.ts
================================================
#!/usr/bin/env node

import puppeteer from "puppeteer";
import path from "path";
import url from "url";
import fs from "fs";
import { resolve } from "import-meta-resolve";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from "@modelcontextprotocol/sdk/types.js";

/**
 * Mermaid MCP Server
 *
 * This server provides a tool to render Mermaid diagrams as PNG images or SVG files.
 *
 * Environment Variables:
 * - MERMAID_LOG_VERBOSITY: Controls the verbosity of logging (default: 2)
 *   0 = EMERGENCY - Only the most critical errors
 *   1 = CRITICAL - Critical errors that require immediate attention
 *   2 = ERROR - Error conditions (default)
 *   3 = WARNING - Warning conditions
 *   4 = INFO - Informational messages
 *   5 = DEBUG - Debug-level messages
 * - CONTENT_IMAGE_SUPPORTED: Controls whether images can be returned directly in the response (default: true)
 *   When set to 'false', the 'name' and 'folder' parameters become mandatory, and all images must be saved to disk.
 *
 * Example:
 *   MERMAID_LOG_VERBOSITY=2 node index.js  # Only show ERROR and more severe logs (default)
 *   MERMAID_LOG_VERBOSITY=4 node index.js  # Show INFO and more severe logs
 *   MERMAID_LOG_VERBOSITY=5 node index.js  # Show DEBUG and more severe logs
 *   CONTENT_IMAGE_SUPPORTED=false node index.js  # Require all images to be saved to disk
 *
 * Tool Parameters:
 * - code: The mermaid markdown to generate an image from (required)
 * - theme: Theme for the diagram (optional, one of: "default", "forest", "dark", "neutral")
 * - backgroundColor: Background color for the diagram (optional, e.g., "white", "transparent", "#F0F0F0")
 * - outputFormat: Output format for the diagram (optional, "png" or "svg", defaults to "png")
 * - name: Name for the generated file (required when saving to folder or when CONTENT_IMAGE_SUPPORTED=false)
 * - folder: Folder path to save the image to (optional, but required when CONTENT_IMAGE_SUPPORTED=false)
 *
 * File Saving Behavior:
 * - When 'folder' is specified, the image will be saved to disk instead of returned in the response
 * - The 'name' parameter is required when 'folder' is specified
 * - If a file with the same name already exists, a timestamp will be appended to the filename
 * - When CONTENT_IMAGE_SUPPORTED=false, all images must be saved to disk, and 'name' and 'folder' are required
 * - SVG files are saved as .svg text files, PNG files are saved as .png binary files
 */

// __dirname is not available in ESM modules by default
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

// Define log levels with numeric values for comparison
enum LogLevel {
  EMERGENCY = 0,
  CRITICAL = 1,
  ERROR = 2,
  WARNING = 3,
  INFO = 4,
  DEBUG = 5,
}

// Get verbosity level from environment variable, default to INFO (4)
const LOG_VERBOSITY = process.env.MERMAID_LOG_VERBOSITY
  ? parseInt(process.env.MERMAID_LOG_VERBOSITY, 10)
  : LogLevel.ERROR;

// Check if content images are supported (default: true)
const CONTENT_IMAGE_SUPPORTED = process.env.CONTENT_IMAGE_SUPPORTED !== "false";

// Convert LogLevel to MCP log level string
function getMcpLogLevel(
  level: LogLevel
): "error" | "info" | "debug" | "warning" | "critical" | "emergency" {
  switch (level) {
    case LogLevel.EMERGENCY:
      return "emergency";
    case LogLevel.CRITICAL:
      return "critical";
    case LogLevel.ERROR:
      return "error";
    case LogLevel.WARNING:
      return "warning";
    case LogLevel.DEBUG:
      return "debug";
    case LogLevel.INFO:
    default:
      return "info";
  }
}

function log(level: LogLevel, message: string) {
  // Only log if the current level is less than or equal to the verbosity setting
  if (level <= LOG_VERBOSITY) {
    // Get the appropriate MCP log level
    const mcpLevel = getMcpLogLevel(level);

    server.sendLoggingMessage({
      level: mcpLevel,
      data: message,
    });

    // Only console.error is consumed by MCP inspector
    console.error(`${LogLevel[level]} - ${message}`);
  }
}

// Define tools
const GENERATE_TOOL: Tool = {
  name: "generate",
  description: "Generate PNG image or SVG from mermaid markdown",
  inputSchema: {
    type: "object",
    properties: {
      code: {
        type: "string",
        description: "The mermaid markdown to generate an image from",
      },
      theme: {
        type: "string",
        enum: ["default", "forest", "dark", "neutral"],
        description: "Theme for the diagram (optional)",
      },
      backgroundColor: {
        type: "string",
        description:
          "Background color for the diagram, e.g. 'white', 'transparent', '#F0F0F0' (optional)",
      },
      outputFormat: {
        type: "string",
        enum: ["png", "svg"],
        description: "Output format for the diagram (optional, defaults to 'png')",
      },
      name: {
        type: "string",
        description: CONTENT_IMAGE_SUPPORTED
          ? "Name of the diagram (optional)"
          : "Name for the generated file (required)",
      },
      folder: {
        type: "string",
        description: CONTENT_IMAGE_SUPPORTED
          ? "Absolute path to save the image to (optional)"
          : "Absolute path to save the image to (required)",
      },
    },
    required: CONTENT_IMAGE_SUPPORTED ? ["code"] : ["code", "name", "folder"],
  },
};

// Server implementation
const server = new Server(
  {
    name: "mermaid-mcp-server",
    version: "0.2.0",
  },
  {
    capabilities: {
      tools: {},
      logging: {},
    },
  }
);

function isGenerateArgs(args: unknown): args is {
  code: string;
  theme?: "default" | "forest" | "dark" | "neutral";
  backgroundColor?: string;
  outputFormat?: "png" | "svg";
  name?: string;
  folder?: string;
} {
  return (
    typeof args === "object" &&
    args !== null &&
    "code" in args &&
    typeof (args as any).code === "string" &&
    (!(args as any).theme ||
      ["default", "forest", "dark", "neutral"].includes((args as any).theme)) &&
    (!(args as any).backgroundColor ||
      typeof (args as any).backgroundColor === "string") &&
    (!(args as any).outputFormat ||
      ["png", "svg"].includes((args as any).outputFormat)) &&
    (!(args as any).name || typeof (args as any).name === "string") &&
    (!(args as any).folder || typeof (args as any).folder === "string")
  );
}

async function renderMermaid(
  code: string,
  config: {
    theme?: "default" | "forest" | "dark" | "neutral";
    backgroundColor?: string;
    outputFormat?: "png" | "svg";
  } = {}
): Promise<{ data: string; svg?: string }> {
  log(LogLevel.INFO, "Launching Puppeteer");
  log(LogLevel.DEBUG, `Rendering with config: ${JSON.stringify(config)}`);

  // Resolve the path to the local mermaid.js file
  const distPath = path.dirname(
    url.fileURLToPath(resolve("mermaid", import.meta.url))
  );
  const mermaidPath = path.resolve(distPath, "mermaid.min.js");
  log(LogLevel.DEBUG, `Using Mermaid from: ${mermaidPath}`);

  const browser = await puppeteer.launch({
    headless: true,
    // Use the bundled browser instead of looking for Chrome on the system
    executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  // Declare page outside try block so it's accessible in catch and finally
  let page: puppeteer.Page | null = null;
  // Store console messages for error reporting
  const consoleMessages: string[] = [];

  try {
    page = await browser.newPage();
    log(LogLevel.DEBUG, "Browser page created");

    // Capture browser console messages for better error reporting
    page.on("console", (msg) => {
      const text = msg.text();
      consoleMessages.push(text);
      log(LogLevel.DEBUG, text);
    });

    // Create a simple HTML template without the CDN reference
    const htmlContent = `
    <!DOCTYPE html>
    <html>
    <head>
      <title>Mermaid Renderer</title>
      <style>
        body { 
          background: ${config.backgroundColor || "white"};
          margin: 0;
          padding: 0;
        }
        #container {
          padding: 0;
          margin: 0;
        }
      </style>
    </head>
    <body>
      <div id="container"></div>
    </body>
    </html>
    `;
    // Note: Must be set before page.goto() to ensure the page renders with the correct dimensions from the start
    await page.setViewport({
      width: 1200, // Keep the same viewport size as before
      height: 800,
      deviceScaleFactor: 3, // 2~4 is fine, the larger the PNG, the clearer and larger it is
    });

    // Write the HTML to a temporary file
    const tempHtmlPath = path.join(__dirname, "temp-mermaid.html");
    fs.writeFileSync(tempHtmlPath, htmlContent);

    log(LogLevel.INFO, `Rendering mermaid code: ${code.substring(0, 50)}...`);
    log(LogLevel.DEBUG, `Full mermaid code: ${code}`);

    // Navigate to the HTML file
    await page.goto(`file://${tempHtmlPath}`);
    log(LogLevel.DEBUG, "Navigated to HTML template");

    // Add the mermaid script to the page
    await page.addScriptTag({ path: mermaidPath });
    log(LogLevel.DEBUG, "Added Mermaid script to page");

    // Render the mermaid diagram using a more robust approach similar to the CLI
    log(LogLevel.DEBUG, "Starting Mermaid rendering in browser");
    const screenshot = await page.$eval(
      "#container",
      async (container, mermaidCode, mermaidConfig) => {
        try {
          // @ts-ignore - mermaid is loaded by the script tag
          window.mermaid.initialize({
            startOnLoad: false,
            theme: mermaidConfig.theme || "default",
            securityLevel: "loose",
            logLevel: 5,
          });

          // This will throw an error if the mermaid syntax is invalid
          // @ts-ignore - mermaid is loaded by the script tag
          const { svg: svgText } = await window.mermaid.render(
            "mermaid-svg",
            mermaidCode,
            container
          );
          container.innerHTML = svgText;

          const svg = container.querySelector("svg");
          if (!svg) {
            throw new Error("SVG element not found after rendering");
          }

          // Apply any necessary styling to the SVG
          svg.style.backgroundColor = mermaidConfig.backgroundColor || "white";

          // Return the dimensions for screenshot
          const rect = svg.getBoundingClientRect();
          return {
            width: Math.ceil(rect.width),
            height: Math.ceil(rect.height),
            success: true,
          };
        } catch (error) {
          // Return the error to be handled outside
          return {
            success: false,
            error: error instanceof Error ? error.message : String(error),
          };
        }
      },
      code,
      { theme: config.theme, backgroundColor: config.backgroundColor }
    );

    // Check if rendering was successful
    if (!screenshot.success) {
      log(
        LogLevel.ERROR,
        `Mermaid rendering failed in browser: ${screenshot.error}`
      );
      throw new Error(`Mermaid rendering failed: ${screenshot.error}`);
    }

    log(LogLevel.DEBUG, "Mermaid rendered successfully in browser");

    // Get the SVG content if needed
    let svgContent: string | undefined;
    if (config.outputFormat === "svg") {
      svgContent = await page.$eval("#container svg", (svg) => {
        return svg.outerHTML;
      });
      log(LogLevel.DEBUG, "SVG content extracted");
    }

    // Take a screenshot of the SVG for PNG output
    let base64Image = "";
    if (config.outputFormat === "png" || config.outputFormat === undefined) {
      const svgElement = await page.$("#container svg");
      if (!svgElement) {
        log(LogLevel.ERROR, "SVG element not found after successful rendering");
        throw new Error("SVG element not found");
      }

      log(LogLevel.DEBUG, "Taking screenshot of SVG");
      // Take a screenshot with the correct dimensions
      base64Image = await svgElement.screenshot({
        omitBackground: false,
        type: "png",
        encoding: "base64",
      });
    }

    // Clean up the temporary file
    fs.unlinkSync(tempHtmlPath);
    log(LogLevel.DEBUG, "Temporary HTML file cleaned up");

    log(LogLevel.INFO, "Mermaid rendered successfully");

    return { data: base64Image, svg: svgContent };
  } catch (error) {
    log(
      LogLevel.ERROR,
      `Error in renderMermaid: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
    log(
      LogLevel.ERROR,
      `Error stack: ${error instanceof Error ? error.stack : "No stack trace"}`
    );

    // Include console messages in the error for better debugging
    if (page && page.isClosed() === false) {
      log(LogLevel.ERROR, "Browser console messages:");
      consoleMessages.forEach((msg) => log(LogLevel.ERROR, `  ${msg}`));
    }

    throw error;
  } finally {
    await browser.close();
    log(LogLevel.DEBUG, "Puppeteer browser closed");
  }
}

/**
 * Saves a generated Mermaid diagram to a file
 *
 * @param base64Image - The base64-encoded PNG image
 * @param name - The name to use for the file (without extension)
 * @param folder - The folder to save the file in
 * @returns The full path to the saved file
 */
async function saveMermaidImageToFile(
  base64Image: string,
  name: string,
  folder: string
): Promise<string> {
  // Create the folder if it doesn't exist
  if (!fs.existsSync(folder)) {
    log(LogLevel.INFO, `Creating folder: ${folder}`);
    fs.mkdirSync(folder, { recursive: true });
  }

  // Generate a filename, adding timestamp if file already exists
  let filename = `${name}.png`;
  const filePath = path.join(folder, filename);

  if (fs.existsSync(filePath)) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    filename = `${name}-${timestamp}.png`;
    log(LogLevel.INFO, `File already exists, using filename: ${filename}`);
  }

  // Save the image to the file
  const imageBuffer = Buffer.from(base64Image, "base64");
  const fullPath = path.join(folder, filename);
  fs.writeFileSync(fullPath, imageBuffer);

  log(LogLevel.INFO, `Image saved to: ${fullPath}`);
  return fullPath;
}

/**
 * Saves a generated Mermaid SVG to a file
 *
 * @param svgContent - The SVG content as a string
 * @param name - The name to use for the file (without extension)
 * @param folder - The folder to save the file in
 * @returns The full path to the saved file
 */
async function saveMermaidSvgToFile(
  svgContent: string,
  name: string,
  folder: string
): Promise<string> {
  // Create the folder if it doesn't exist
  if (!fs.existsSync(folder)) {
    log(LogLevel.INFO, `Creating folder: ${folder}`);
    fs.mkdirSync(folder, { recursive: true });
  }

  // Generate a filename, adding timestamp if file already exists
  let filename = `${name}.svg`;
  const filePath = path.join(folder, filename);

  if (fs.existsSync(filePath)) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    filename = `${name}-${timestamp}.svg`;
    log(LogLevel.INFO, `File already exists, using filename: ${filename}`);
  }

  // Save the SVG to the file
  const fullPath = path.join(folder, filename);
  fs.writeFileSync(fullPath, svgContent, "utf-8");

  log(LogLevel.INFO, `SVG saved to: ${fullPath}`);
  return fullPath;
}

/**
 * Handles Mermaid syntax errors and other errors
 *
 * @param error - The error that occurred
 * @returns A response object with the error message
 */
function handleMermaidError(error: unknown): {
  content: Array<{ type: "text"; text: string }>;
  isError: boolean;
} {
  const errorMessage = error instanceof Error ? error.message : String(error);
  const isSyntaxError =
    errorMessage.includes("Syntax error") ||
    errorMessage.includes("Parse error") ||
    errorMessage.includes("Mermaid rendering failed");

  return {
    content: [
      {
        type: "text",
        text: isSyntaxError
          ? `Mermaid syntax error: ${errorMessage}\n\nPlease check your diagram syntax.`
          : `Error generating diagram: ${errorMessage}`,
      },
    ],
    isError: true,
  };
}

/**
 * Processes a generate request to create a Mermaid diagram
 *
 * @param args - The arguments for the generate request
 * @returns A response object with the generated image or file path
 */
async function processGenerateRequest(args: {
  code: string;
  theme?: "default" | "forest" | "dark" | "neutral";
  backgroundColor?: string;
  outputFormat?: "png" | "svg";
  name?: string;
  folder?: string;
}): Promise<{
  content: Array<
    | { type: "text"; text: string }
    | { type: "image"; data: string; mimeType: string }
  >;
  isError: boolean;
}> {
  try {
    const outputFormat = args.outputFormat || "png";
    const result = await renderMermaid(args.code, {
      theme: args.theme,
      backgroundColor: args.backgroundColor,
      outputFormat: outputFormat,
    });

    // Check if we need to save the file to a folder
    if (!CONTENT_IMAGE_SUPPORTED) {
      if (!args.folder) {
        throw new Error(
          "Folder parameter is required when CONTENT_IMAGE_SUPPORTED is false"
        );
      }

      // Save the file based on format
      let fullPath: string;
      if (outputFormat === "svg") {
        if (!result.svg) {
          throw new Error("SVG content not available");
        }
        fullPath = await saveMermaidSvgToFile(
          result.svg,
          args.name!,
          args.folder!
        );
      } else {
        fullPath = await saveMermaidImageToFile(
          result.data,
          args.name!,
          args.folder!
        );
      }

      return {
        content: [
          {
            type: "text",
            text: `${outputFormat.toUpperCase()} saved to: ${fullPath}`,
          },
        ],
        isError: false,
      };
    }

    // If folder is provided and CONTENT_IMAGE_SUPPORTED is true, save the file to the folder
    // but also return the content in the response
    let savedMessage = "";
    if (args.folder && args.name) {
      try {
        let fullPath: string;
        if (outputFormat === "svg") {
          if (!result.svg) {
            throw new Error("SVG content not available");
          }
          fullPath = await saveMermaidSvgToFile(
            result.svg,
            args.name,
            args.folder
          );
        } else {
          fullPath = await saveMermaidImageToFile(
            result.data,
            args.name,
            args.folder
          );
        }
        savedMessage = `${outputFormat.toUpperCase()} also saved to: ${fullPath}`;
        log(LogLevel.INFO, savedMessage);
      } catch (saveError) {
        log(
          LogLevel.ERROR,
          `Failed to save ${outputFormat} to folder: ${(saveError as Error).message}`
        );
        savedMessage = `Note: Failed to save ${outputFormat} to folder: ${
          (saveError as Error).message
        }`;
      }
    }

    // Return the appropriate content based on format
    if (outputFormat === "svg") {
      if (!result.svg) {
        throw new Error("SVG content not available");
      }
      return {
        content: [
          {
            type: "text",
            text: savedMessage
              ? `Here is the generated SVG:\n\n${result.svg}\n\n${savedMessage}`
              : `Here is the generated SVG:\n\n${result.svg}`,
          },
        ],
        isError: false,
      };
    } else {
      // Return the PNG image in the response
      return {
        content: [
          {
            type: "text",
            text: savedMessage
              ? `Here is the generated image. ${savedMessage}`
              : "Here is the generated image",
          },
          {
            type: "image",
            data: result.data,
            mimeType: "image/png",
          },
        ],
        isError: false,
      };
    }
  } catch (error) {
    return handleMermaidError(error);
  }
}

// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [GENERATE_TOOL],
}));

// Set up the request handler for tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;

    if (!args) {
      throw new Error("No arguments provided");
    }

    log(
      LogLevel.INFO,
      `Received request: ${name} with args: ${JSON.stringify(args)}`
    );

    if (name === "generate") {
      log(LogLevel.INFO, "Rendering Mermaid PNG");
      if (!isGenerateArgs(args)) {
        throw new Error("Invalid arguments for generate");
      }

      // Process the generate request
      return await processGenerateRequest(args);
    }

    return {
      content: [{ type: "text", text: `Unknown tool: ${name}` }],
      isError: true,
    };
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: `Error: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
      isError: true,
    };
  }
});

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  log(LogLevel.INFO, "Mermaid MCP Server running on stdio");
}

runServer().catch((error) => {
  log(
    LogLevel.CRITICAL,
    `Fatal error running server: ${
      error instanceof Error ? error.message : String(error)
    }`
  );
  process.exit(1);
});


================================================
FILE: package.json
================================================
{
  "name": "@peng-shawn/mermaid-mcp-server",
  "version": "0.2.0",
  "description": "A Model Context Protocol (MCP) server that converts Mermaid diagrams to PNG images",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "release": "scripts/release.sh"
  },
  "bin": {
    "mermaid-mcp-server": "dist/index.js"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "keywords": [
    "mermaid",
    "diagram",
    "mcp",
    "model-context-protocol",
    "ai",
    "visualization"
  ],
  "author": "Shawn Peng",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.6.1",
    "import-meta-resolve": "^4.1.0",
    "mermaid": "^11.4.1",
    "puppeteer": "^24.3.1"
  },
  "devDependencies": {
    "@types/node": "^22.13.9",
    "typescript": "^5.8.2"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/peng-shawn/mermaid-mcp-server.git"
  },
  "bugs": {
    "url": "https://github.com/peng-shawn/mermaid-mcp-server/issues"
  },
  "homepage": "https://github.com/peng-shawn/mermaid-mcp-server#readme"
}


================================================
FILE: scripts/release.sh
================================================
#!/bin/bash

# Script to help with the release process

# Check if a version argument was provided
if [ -z "$1" ]; then
  echo "Error: No version specified"
  echo "Usage: ./scripts/release.sh <version|patch|minor|major>"
  echo "Examples:"
  echo "  ./scripts/release.sh 0.1.4    # Set specific version"
  echo "  ./scripts/release.sh patch    # Increment patch version (0.1.3 -> 0.1.4)"
  echo "  ./scripts/release.sh minor    # Increment minor version (0.1.3 -> 0.2.0)"
  echo "  ./scripts/release.sh major    # Increment major version (0.1.3 -> 1.0.0)"
  exit 1
fi

VERSION_ARG=$1

# Check if the version is a semantic increment or specific version
if [[ "$VERSION_ARG" =~ ^(patch|minor|major)$ ]]; then
  # It's a semantic increment
  INCREMENT_TYPE=$VERSION_ARG
  # Get the current version to display in logs
  CURRENT_VERSION=$(grep -o '"version": "[^"]*"' package.json | cut -d'"' -f4)
  echo "Using semantic increment: $INCREMENT_TYPE (current version: $CURRENT_VERSION)"
else
  # It's a specific version - validate semver format
  if ! [[ $VERSION_ARG =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+)?(\+[0-9A-Za-z-]+)?$ ]]; then
    echo "Error: Version must follow semantic versioning (e.g., 1.2.3, 1.2.3-beta, etc.)"
    echo "Or use one of: patch, minor, major"
    exit 1
  fi
  # Store the specific version
  SPECIFIC_VERSION=$VERSION_ARG
  echo "Using specific version: $SPECIFIC_VERSION"
fi

# Check if we're on the main branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" != "main" ]; then
  echo "Warning: You are not on the main branch. Current branch: $CURRENT_BRANCH"
  read -p "Do you want to continue? (y/n) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 1
  fi
fi

# Check if working directory is clean
if [ -n "$(git status --porcelain)" ]; then
  echo "Error: Working directory is not clean. Please commit or stash your changes."
  exit 1
fi

# Pull latest changes
echo "Pulling latest changes from origin..."
git pull origin main

# Check current versions in files
PACKAGE_VERSION=$(grep -o '"version": "[^"]*"' package.json | cut -d'"' -f4)
PACKAGE_LOCK_VERSION=$(grep -o '"version": "[^"]*"' package-lock.json | head -1 | cut -d'"' -f4)
INDEX_VERSION=$(grep -o 'version: "[^"]*"' index.ts | cut -d'"' -f2)

echo "Current versions:"
echo "- package.json: $PACKAGE_VERSION"
echo "- package-lock.json: $PACKAGE_LOCK_VERSION"
echo "- index.ts: $INDEX_VERSION"

# Function to update index.ts version
update_index_version() {
  local old_version=$1
  local new_version=$2
  local commit_msg=$3
  
  echo "Updating version in index.ts from $old_version to $new_version..."
  sed -i '' "s/version: \"$old_version\"/version: \"$new_version\"/" index.ts
  git add index.ts
  
  if [ -n "$commit_msg" ]; then
    git commit -m "$commit_msg"
  fi
}

if [ "$PACKAGE_VERSION" != "$PACKAGE_LOCK_VERSION" ] || [ "$PACKAGE_VERSION" != "$INDEX_VERSION" ]; then
  echo "Warning: Version mismatch detected between files."
  
  if [ -n "$SPECIFIC_VERSION" ]; then
    echo "Will update all files to version: $SPECIFIC_VERSION"
  else
    echo "Will update all files using increment: $INCREMENT_TYPE"
  fi
  
  read -p "Do you want to continue? (y/n) " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 1
  fi
fi

# Handle version updates based on increment type or specific version
if [ -n "$INCREMENT_TYPE" ]; then
  # For semantic increments
  echo "Incrementing version ($INCREMENT_TYPE)..."
  
  # Use npm version to increment the version
  npm version $INCREMENT_TYPE --no-git-tag-version
  
  # Get the new version
  NEW_VERSION=$(grep -o '"version": "[^"]*"' package.json | cut -d'"' -f4)
  
  # Update index.ts with the new version
  update_index_version "$INDEX_VERSION" "$NEW_VERSION"
  
  # Commit all changes and create tag
  git add package.json package-lock.json
  git commit -m "chore: release version $NEW_VERSION"
  git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION"
else
  # For specific version
  # Use npm version to update package.json and package-lock.json without git operations
  echo "Updating version in package.json and package-lock.json..."
  npm version $SPECIFIC_VERSION --no-git-tag-version
  
  # Update index.ts
  update_index_version "$INDEX_VERSION" "$SPECIFIC_VERSION"
  
  # Commit all changes and create tag
  git add package.json package-lock.json
  git commit -m "chore: release version $SPECIFIC_VERSION"
  git tag -a "v$SPECIFIC_VERSION" -m "Version $SPECIFIC_VERSION"
fi

# Get the final version for pushing the tag
FINAL_VERSION=$(grep -o '"version": "[^"]*"' package.json | cut -d'"' -f4)

# Push changes and tag to remote
echo "Pushing changes and tag to remote..."
git push origin main
git push origin v$FINAL_VERSION

echo "Release process completed for version $FINAL_VERSION"
echo "The GitHub workflow will now build and publish the package to npm"
echo "Check the Actions tab in your GitHub repository for progress" 

================================================
FILE: smithery.yaml
================================================
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required: []
    properties:
      contentImageSupported:
        type: boolean
        default: true
        description: Set to true if images should be returned directly in the response.
          Set to false if images should be saved to disk, requiring 'name' and
          'folder' parameters.
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'node',
      args: ['dist/index.js'],
      env: {
        CONTENT_IMAGE_SUPPORTED: config.contentImageSupported ? 'true' : 'false',
        PUPPETEER_EXECUTABLE_PATH: '/usr/bin/chromium'
      }
    })
  exampleConfig:
    contentImageSupported: true


================================================
FILE: tsconfig.json
================================================
{
    "compilerOptions": {
      "target": "ES2022",
      "module": "Node16",
      "moduleResolution": "Node16",
      "outDir": "./dist",
      "rootDir": ".",
      "strict": true,
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true
    },
    "include": ["index.ts"]
  }
Download .txt
gitextract_pz7yyjpu/

├── .github/
│   └── workflows/
│       └── npm-publish.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── index.ts
├── package.json
├── scripts/
│   └── release.sh
├── smithery.yaml
└── tsconfig.json
Download .txt
SYMBOL INDEX (13 symbols across 1 files)

FILE: index.ts
  type LogLevel (line 58) | enum LogLevel {
  constant LOG_VERBOSITY (line 68) | const LOG_VERBOSITY = process.env.MERMAID_LOG_VERBOSITY
  constant CONTENT_IMAGE_SUPPORTED (line 73) | const CONTENT_IMAGE_SUPPORTED = process.env.CONTENT_IMAGE_SUPPORTED !== ...
  function getMcpLogLevel (line 76) | function getMcpLogLevel(
  function log (line 96) | function log(level: LogLevel, message: string) {
  constant GENERATE_TOOL (line 113) | const GENERATE_TOOL: Tool = {
  function isGenerateArgs (line 169) | function isGenerateArgs(args: unknown): args is {
  function renderMermaid (line 193) | async function renderMermaid(
  function saveMermaidImageToFile (line 407) | async function saveMermaidImageToFile(
  function saveMermaidSvgToFile (line 445) | async function saveMermaidSvgToFile(
  function handleMermaidError (line 480) | function handleMermaidError(error: unknown): {
  function processGenerateRequest (line 509) | async function processGenerateRequest(args: {
  function runServer (line 692) | async function runServer() {
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
  {
    "path": ".github/workflows/npm-publish.yml",
    "chars": 630,
    "preview": "name: Publish to npm\n\non:\n  push:\n    tags:\n      - 'v*' # Run workflow on version tags, e.g. v1.0.0\n\njobs:\n  build-and-"
  },
  {
    "path": ".gitignore",
    "chars": 27,
    "preview": "node_modules\ndist\n.cursor/*"
  },
  {
    "path": "Dockerfile",
    "chars": 778,
    "preview": "# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile\n# Use node:lts-slim (Debian-based) i"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2025 Shawn Peng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 8050,
    "preview": "# Mermaid MCP Server\n\nA Model Context Protocol (MCP) server that converts Mermaid diagrams to PNG images or SVG files. T"
  },
  {
    "path": "index.ts",
    "chars": 21580,
    "preview": "#!/usr/bin/env node\n\nimport puppeteer from \"puppeteer\";\nimport path from \"path\";\nimport url from \"url\";\nimport fs from \""
  },
  {
    "path": "package.json",
    "chars": 1175,
    "preview": "{\n  \"name\": \"@peng-shawn/mermaid-mcp-server\",\n  \"version\": \"0.2.0\",\n  \"description\": \"A Model Context Protocol (MCP) ser"
  },
  {
    "path": "scripts/release.sh",
    "chars": 4926,
    "preview": "#!/bin/bash\n\n# Script to help with the release process\n\n# Check if a version argument was provided\nif [ -z \"$1\" ]; then\n"
  },
  {
    "path": "smithery.yaml",
    "chars": 924,
    "preview": "# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml\n\nstartCommand:\n  type: stdio\n  configSchema:"
  },
  {
    "path": "tsconfig.json",
    "chars": 329,
    "preview": "{\n    \"compilerOptions\": {\n      \"target\": \"ES2022\",\n      \"module\": \"Node16\",\n      \"moduleResolution\": \"Node16\",\n     "
  }
]

About this extraction

This page contains the full source code of the peng-shawn/mermaid-mcp-server GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (38.6 KB), approximately 10.0k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!