[
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "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-publish:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '18'\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        run: npm run build\n\n      - name: Publish to npm\n        run: npm publish --access public\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} "
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n.cursor/*"
  },
  {
    "path": "Dockerfile",
    "content": "# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile\n# Use node:lts-slim (Debian-based) instead of Alpine for better Chrome compatibility\nFROM node:lts-slim\n\n# Set working directory\nWORKDIR /app\n\n# Install Chromium with its dependencies\nRUN apt-get update && apt-get install -y \\\n    chromium \\\n    --no-install-recommends \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy package files and install dependencies\n# Use PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true to avoid downloading Chromium again\nENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true\nCOPY package.json package-lock.json ./\nRUN npm install\n\n# Copy the rest of the application files\nCOPY . .\n\n# Build the TypeScript code\nRUN npm run build\n\n# Command to run the MCP server\nCMD [ \"node\", \"dist/index.js\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shawn Peng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Mermaid MCP Server\n\nA 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.\n\n## Features\n\n- Converts Mermaid diagram code to PNG images or SVG files\n- Supports multiple diagram themes (default, forest, dark, neutral)\n- Customizable background colors\n- Uses Puppeteer for high-quality headless browser rendering\n- Implements the MCP protocol for seamless integration with AI assistants\n- Flexible output options: return images/SVG directly or save to disk\n- Error handling with detailed error messages\n\n## How It Works\n\nThe 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:\n\n1. Launching a headless browser instance\n2. Creating an HTML template with the Mermaid code\n3. Loading the Mermaid.js library\n4. Rendering the diagram to SVG\n5. Either saving the SVG directly or taking a screenshot as PNG\n6. Either returning the image/SVG directly or saving it to disk\n\n## Build\n\n```bash\nnpx tsc\n```\n\n## Usage\n\n### Use with Claude desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"mermaid\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@peng-shawn/mermaid-mcp-server\"]\n    }\n  }\n}\n```\n\n### Use with Cursor and Cline\n\n```bash\nenv CONTENT_IMAGE_SUPPORTED=false npx -y @peng-shawn/mermaid-mcp-server\n```\n\nYou 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\"\n\n### Run with inspector\n\nRun the server with inspector for testing and debugging:\n\n```bash\nnpx @modelcontextprotocol/inspector node dist/index.js\n```\n\nThe server will start and listen on stdio for MCP protocol messages.\n\nLearn more about inspector [here](https://modelcontextprotocol.io/docs/tools/inspector).\n\n### Installing via Smithery\n\nTo install Mermaid Diagram Generator for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@peng-shawn/mermaid-mcp-server):\n\n```bash\nnpx -y @smithery/cli install @peng-shawn/mermaid-mcp-server --client claude\n```\n\n### Docker and Smithery Environments\n\nWhen running in Docker containers (including via Smithery), you may need to handle Chrome dependencies:\n\n1. The server now attempts to use Puppeteer's bundled browser by default\n2. If you encounter browser-related errors, you have two options:\n\n   **Option 1: During Docker image build:**\n\n   - Set `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true` when installing Puppeteer\n   - Install Chrome/Chromium in your Docker container\n   - Set `PUPPETEER_EXECUTABLE_PATH` at runtime to point to the Chrome installation\n\n   **Option 2: Use Puppeteer's bundled Chrome:**\n\n   - Ensure your Docker container has the necessary dependencies for Chrome\n   - No need to set `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`\n   - The code will use the bundled browser automatically\n\nFor Smithery users, the latest version should work without additional configuration.\n\n## API\n\nThe server exposes a single tool:\n\n- `generate`: Converts Mermaid diagram code to a PNG image or SVG file\n  - Parameters:\n    - `code`: The Mermaid diagram code to render\n    - `theme`: (optional) Theme for the diagram. Options: \"default\", \"forest\", \"dark\", \"neutral\"\n    - `backgroundColor`: (optional) Background color for the diagram, e.g. 'white', 'transparent', '#F0F0F0'\n    - `outputFormat`: (optional) Output format for the diagram. Options: \"png\", \"svg\" (defaults to \"png\")\n    - `name`: Name for the generated file (required when CONTENT_IMAGE_SUPPORTED=false)\n    - `folder`: Absolute path to save the image/SVG to (required when CONTENT_IMAGE_SUPPORTED=false)\n\nThe behavior of the `generate` tool depends on the `CONTENT_IMAGE_SUPPORTED` environment variable:\n\n- When `CONTENT_IMAGE_SUPPORTED=true` (default): The tool returns the image/SVG directly in the response\n- When `CONTENT_IMAGE_SUPPORTED=false`: The tool saves the image/SVG to the specified folder and returns the file path\n\n## Environment Variables\n\n- `CONTENT_IMAGE_SUPPORTED`: Controls whether images are returned directly in the response or saved to disk\n  - `true` (default): Images are returned directly in the response\n  - `false`: Images are saved to disk, requiring `name` and `folder` parameters\n\n## Examples\n\n### Basic Usage\n\n```javascript\n// Generate a flowchart with default settings\n{\n  \"code\": \"flowchart TD\\n    A[Start] --> B{Is it?}\\n    B -->|Yes| C[OK]\\n    B -->|No| D[End]\"\n}\n```\n\n### With Theme and Background Color\n\n```javascript\n// Generate a sequence diagram with forest theme and light gray background\n{\n  \"code\": \"sequenceDiagram\\n    Alice->>John: Hello John, how are you?\\n    John-->>Alice: Great!\",\n  \"theme\": \"forest\",\n  \"backgroundColor\": \"#F0F0F0\"\n}\n```\n\n### Saving to Disk (when CONTENT_IMAGE_SUPPORTED=false)\n\n```javascript\n// Generate a class diagram and save it to disk as PNG\n{\n  \"code\": \"classDiagram\\n    Class01 <|-- AveryLongClass\\n    Class03 *-- Class04\\n    Class05 o-- Class06\",\n  \"theme\": \"dark\",\n  \"name\": \"class_diagram\",\n  \"folder\": \"/path/to/diagrams\"\n}\n```\n\n### Generating SVG Output\n\n```javascript\n// Generate a state diagram as SVG\n{\n  \"code\": \"stateDiagram-v2\\n    [*] --> Still\\n    Still --> [*]\\n    Still --> Moving\\n    Moving --> Still\\n    Moving --> Crash\\n    Crash --> [*]\",\n  \"outputFormat\": \"svg\",\n  \"name\": \"state_diagram\",\n  \"folder\": \"/path/to/diagrams\"\n}\n```\n\n## FAQ\n\n### Doesn't Claude desktop already support mermaid via canvas?\n\nYes, 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.\n\n### Why do I need to specify CONTENT_IMAGE_SUPPORTED=false when using with Cursor?\n\nCursor doesn't support inline images in responses yet.\n\n## Publishing\n\nThis project uses GitHub Actions to automate the publishing process to npm.\n\n### Method 1: Using the Release Script (Recommended)\n\n1. Make sure all your changes are committed and pushed\n2. Run the release script with either a specific version number or a semantic version increment:\n\n   ```bash\n   # Using a specific version number\n   npm run release 0.1.4\n\n   # Using semantic version increments\n   npm run release patch  # Increments the patch version (e.g., 0.1.3 → 0.1.4)\n   npm run release minor  # Increments the minor version (e.g., 0.1.3 → 0.2.0)\n   npm run release major  # Increments the major version (e.g., 0.1.3 → 1.0.0)\n   ```\n\n3. The script will:\n   - Validate the version format or semantic increment\n   - Check if you're on the main branch\n   - Detect and warn about version mismatches between files\n   - Update all version references consistently (package.json, package-lock.json, and index.ts)\n   - Create a single commit with all version changes\n   - Create and push a git tag\n   - The GitHub workflow will then automatically build and publish to npm\n\n### Method 2: Manual Process\n\n1. Update your code and commit the changes\n2. Create and push a new tag with the version number:\n   ```bash\n   git tag v0.1.4  # Use the appropriate version number\n   git push origin v0.1.4\n   ```\n3. The GitHub workflow will automatically:\n   - Build the project\n   - Publish to npm with the version from the tag\n\nNote: You need to set up the `NPM_TOKEN` secret in your GitHub repository settings. To do this:\n\n1. Generate an npm access token with publish permissions\n2. Go to your GitHub repository → Settings → Secrets and variables → Actions\n3. Create a new repository secret named `NPM_TOKEN` with your npm token as the value\n\n## Badges\n\n[![smithery badge](https://smithery.ai/badge/@peng-shawn/mermaid-mcp-server)](https://smithery.ai/server/@peng-shawn/mermaid-mcp-server)\n\n<a href=\"https://glama.ai/mcp/servers/lzjlbitkzr\">\n  <img width=\"380\" height=\"200\" src=\"https://glama.ai/mcp/servers/lzjlbitkzr/badge\" alt=\"mermaid-mcp-server MCP server\" />\n</a>\n\n## License\n\nMIT\n"
  },
  {
    "path": "index.ts",
    "content": "#!/usr/bin/env node\n\nimport puppeteer from \"puppeteer\";\nimport path from \"path\";\nimport url from \"url\";\nimport fs from \"fs\";\nimport { resolve } from \"import-meta-resolve\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n  CallToolRequestSchema,\n  ListToolsRequestSchema,\n  Tool,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\n/**\n * Mermaid MCP Server\n *\n * This server provides a tool to render Mermaid diagrams as PNG images or SVG files.\n *\n * Environment Variables:\n * - MERMAID_LOG_VERBOSITY: Controls the verbosity of logging (default: 2)\n *   0 = EMERGENCY - Only the most critical errors\n *   1 = CRITICAL - Critical errors that require immediate attention\n *   2 = ERROR - Error conditions (default)\n *   3 = WARNING - Warning conditions\n *   4 = INFO - Informational messages\n *   5 = DEBUG - Debug-level messages\n * - CONTENT_IMAGE_SUPPORTED: Controls whether images can be returned directly in the response (default: true)\n *   When set to 'false', the 'name' and 'folder' parameters become mandatory, and all images must be saved to disk.\n *\n * Example:\n *   MERMAID_LOG_VERBOSITY=2 node index.js  # Only show ERROR and more severe logs (default)\n *   MERMAID_LOG_VERBOSITY=4 node index.js  # Show INFO and more severe logs\n *   MERMAID_LOG_VERBOSITY=5 node index.js  # Show DEBUG and more severe logs\n *   CONTENT_IMAGE_SUPPORTED=false node index.js  # Require all images to be saved to disk\n *\n * Tool Parameters:\n * - code: The mermaid markdown to generate an image from (required)\n * - theme: Theme for the diagram (optional, one of: \"default\", \"forest\", \"dark\", \"neutral\")\n * - backgroundColor: Background color for the diagram (optional, e.g., \"white\", \"transparent\", \"#F0F0F0\")\n * - outputFormat: Output format for the diagram (optional, \"png\" or \"svg\", defaults to \"png\")\n * - name: Name for the generated file (required when saving to folder or when CONTENT_IMAGE_SUPPORTED=false)\n * - folder: Folder path to save the image to (optional, but required when CONTENT_IMAGE_SUPPORTED=false)\n *\n * File Saving Behavior:\n * - When 'folder' is specified, the image will be saved to disk instead of returned in the response\n * - The 'name' parameter is required when 'folder' is specified\n * - If a file with the same name already exists, a timestamp will be appended to the filename\n * - When CONTENT_IMAGE_SUPPORTED=false, all images must be saved to disk, and 'name' and 'folder' are required\n * - SVG files are saved as .svg text files, PNG files are saved as .png binary files\n */\n\n// __dirname is not available in ESM modules by default\nconst __dirname = url.fileURLToPath(new URL(\".\", import.meta.url));\n\n// Define log levels with numeric values for comparison\nenum LogLevel {\n  EMERGENCY = 0,\n  CRITICAL = 1,\n  ERROR = 2,\n  WARNING = 3,\n  INFO = 4,\n  DEBUG = 5,\n}\n\n// Get verbosity level from environment variable, default to INFO (4)\nconst LOG_VERBOSITY = process.env.MERMAID_LOG_VERBOSITY\n  ? parseInt(process.env.MERMAID_LOG_VERBOSITY, 10)\n  : LogLevel.ERROR;\n\n// Check if content images are supported (default: true)\nconst CONTENT_IMAGE_SUPPORTED = process.env.CONTENT_IMAGE_SUPPORTED !== \"false\";\n\n// Convert LogLevel to MCP log level string\nfunction getMcpLogLevel(\n  level: LogLevel\n): \"error\" | \"info\" | \"debug\" | \"warning\" | \"critical\" | \"emergency\" {\n  switch (level) {\n    case LogLevel.EMERGENCY:\n      return \"emergency\";\n    case LogLevel.CRITICAL:\n      return \"critical\";\n    case LogLevel.ERROR:\n      return \"error\";\n    case LogLevel.WARNING:\n      return \"warning\";\n    case LogLevel.DEBUG:\n      return \"debug\";\n    case LogLevel.INFO:\n    default:\n      return \"info\";\n  }\n}\n\nfunction log(level: LogLevel, message: string) {\n  // Only log if the current level is less than or equal to the verbosity setting\n  if (level <= LOG_VERBOSITY) {\n    // Get the appropriate MCP log level\n    const mcpLevel = getMcpLogLevel(level);\n\n    server.sendLoggingMessage({\n      level: mcpLevel,\n      data: message,\n    });\n\n    // Only console.error is consumed by MCP inspector\n    console.error(`${LogLevel[level]} - ${message}`);\n  }\n}\n\n// Define tools\nconst GENERATE_TOOL: Tool = {\n  name: \"generate\",\n  description: \"Generate PNG image or SVG from mermaid markdown\",\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      code: {\n        type: \"string\",\n        description: \"The mermaid markdown to generate an image from\",\n      },\n      theme: {\n        type: \"string\",\n        enum: [\"default\", \"forest\", \"dark\", \"neutral\"],\n        description: \"Theme for the diagram (optional)\",\n      },\n      backgroundColor: {\n        type: \"string\",\n        description:\n          \"Background color for the diagram, e.g. 'white', 'transparent', '#F0F0F0' (optional)\",\n      },\n      outputFormat: {\n        type: \"string\",\n        enum: [\"png\", \"svg\"],\n        description: \"Output format for the diagram (optional, defaults to 'png')\",\n      },\n      name: {\n        type: \"string\",\n        description: CONTENT_IMAGE_SUPPORTED\n          ? \"Name of the diagram (optional)\"\n          : \"Name for the generated file (required)\",\n      },\n      folder: {\n        type: \"string\",\n        description: CONTENT_IMAGE_SUPPORTED\n          ? \"Absolute path to save the image to (optional)\"\n          : \"Absolute path to save the image to (required)\",\n      },\n    },\n    required: CONTENT_IMAGE_SUPPORTED ? [\"code\"] : [\"code\", \"name\", \"folder\"],\n  },\n};\n\n// Server implementation\nconst server = new Server(\n  {\n    name: \"mermaid-mcp-server\",\n    version: \"0.2.0\",\n  },\n  {\n    capabilities: {\n      tools: {},\n      logging: {},\n    },\n  }\n);\n\nfunction isGenerateArgs(args: unknown): args is {\n  code: string;\n  theme?: \"default\" | \"forest\" | \"dark\" | \"neutral\";\n  backgroundColor?: string;\n  outputFormat?: \"png\" | \"svg\";\n  name?: string;\n  folder?: string;\n} {\n  return (\n    typeof args === \"object\" &&\n    args !== null &&\n    \"code\" in args &&\n    typeof (args as any).code === \"string\" &&\n    (!(args as any).theme ||\n      [\"default\", \"forest\", \"dark\", \"neutral\"].includes((args as any).theme)) &&\n    (!(args as any).backgroundColor ||\n      typeof (args as any).backgroundColor === \"string\") &&\n    (!(args as any).outputFormat ||\n      [\"png\", \"svg\"].includes((args as any).outputFormat)) &&\n    (!(args as any).name || typeof (args as any).name === \"string\") &&\n    (!(args as any).folder || typeof (args as any).folder === \"string\")\n  );\n}\n\nasync function renderMermaid(\n  code: string,\n  config: {\n    theme?: \"default\" | \"forest\" | \"dark\" | \"neutral\";\n    backgroundColor?: string;\n    outputFormat?: \"png\" | \"svg\";\n  } = {}\n): Promise<{ data: string; svg?: string }> {\n  log(LogLevel.INFO, \"Launching Puppeteer\");\n  log(LogLevel.DEBUG, `Rendering with config: ${JSON.stringify(config)}`);\n\n  // Resolve the path to the local mermaid.js file\n  const distPath = path.dirname(\n    url.fileURLToPath(resolve(\"mermaid\", import.meta.url))\n  );\n  const mermaidPath = path.resolve(distPath, \"mermaid.min.js\");\n  log(LogLevel.DEBUG, `Using Mermaid from: ${mermaidPath}`);\n\n  const browser = await puppeteer.launch({\n    headless: true,\n    // Use the bundled browser instead of looking for Chrome on the system\n    executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,\n    args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n  });\n\n  // Declare page outside try block so it's accessible in catch and finally\n  let page: puppeteer.Page | null = null;\n  // Store console messages for error reporting\n  const consoleMessages: string[] = [];\n\n  try {\n    page = await browser.newPage();\n    log(LogLevel.DEBUG, \"Browser page created\");\n\n    // Capture browser console messages for better error reporting\n    page.on(\"console\", (msg) => {\n      const text = msg.text();\n      consoleMessages.push(text);\n      log(LogLevel.DEBUG, text);\n    });\n\n    // Create a simple HTML template without the CDN reference\n    const htmlContent = `\n    <!DOCTYPE html>\n    <html>\n    <head>\n      <title>Mermaid Renderer</title>\n      <style>\n        body { \n          background: ${config.backgroundColor || \"white\"};\n          margin: 0;\n          padding: 0;\n        }\n        #container {\n          padding: 0;\n          margin: 0;\n        }\n      </style>\n    </head>\n    <body>\n      <div id=\"container\"></div>\n    </body>\n    </html>\n    `;\n    // Note: Must be set before page.goto() to ensure the page renders with the correct dimensions from the start\n    await page.setViewport({\n      width: 1200, // Keep the same viewport size as before\n      height: 800,\n      deviceScaleFactor: 3, // 2~4 is fine, the larger the PNG, the clearer and larger it is\n    });\n\n    // Write the HTML to a temporary file\n    const tempHtmlPath = path.join(__dirname, \"temp-mermaid.html\");\n    fs.writeFileSync(tempHtmlPath, htmlContent);\n\n    log(LogLevel.INFO, `Rendering mermaid code: ${code.substring(0, 50)}...`);\n    log(LogLevel.DEBUG, `Full mermaid code: ${code}`);\n\n    // Navigate to the HTML file\n    await page.goto(`file://${tempHtmlPath}`);\n    log(LogLevel.DEBUG, \"Navigated to HTML template\");\n\n    // Add the mermaid script to the page\n    await page.addScriptTag({ path: mermaidPath });\n    log(LogLevel.DEBUG, \"Added Mermaid script to page\");\n\n    // Render the mermaid diagram using a more robust approach similar to the CLI\n    log(LogLevel.DEBUG, \"Starting Mermaid rendering in browser\");\n    const screenshot = await page.$eval(\n      \"#container\",\n      async (container, mermaidCode, mermaidConfig) => {\n        try {\n          // @ts-ignore - mermaid is loaded by the script tag\n          window.mermaid.initialize({\n            startOnLoad: false,\n            theme: mermaidConfig.theme || \"default\",\n            securityLevel: \"loose\",\n            logLevel: 5,\n          });\n\n          // This will throw an error if the mermaid syntax is invalid\n          // @ts-ignore - mermaid is loaded by the script tag\n          const { svg: svgText } = await window.mermaid.render(\n            \"mermaid-svg\",\n            mermaidCode,\n            container\n          );\n          container.innerHTML = svgText;\n\n          const svg = container.querySelector(\"svg\");\n          if (!svg) {\n            throw new Error(\"SVG element not found after rendering\");\n          }\n\n          // Apply any necessary styling to the SVG\n          svg.style.backgroundColor = mermaidConfig.backgroundColor || \"white\";\n\n          // Return the dimensions for screenshot\n          const rect = svg.getBoundingClientRect();\n          return {\n            width: Math.ceil(rect.width),\n            height: Math.ceil(rect.height),\n            success: true,\n          };\n        } catch (error) {\n          // Return the error to be handled outside\n          return {\n            success: false,\n            error: error instanceof Error ? error.message : String(error),\n          };\n        }\n      },\n      code,\n      { theme: config.theme, backgroundColor: config.backgroundColor }\n    );\n\n    // Check if rendering was successful\n    if (!screenshot.success) {\n      log(\n        LogLevel.ERROR,\n        `Mermaid rendering failed in browser: ${screenshot.error}`\n      );\n      throw new Error(`Mermaid rendering failed: ${screenshot.error}`);\n    }\n\n    log(LogLevel.DEBUG, \"Mermaid rendered successfully in browser\");\n\n    // Get the SVG content if needed\n    let svgContent: string | undefined;\n    if (config.outputFormat === \"svg\") {\n      svgContent = await page.$eval(\"#container svg\", (svg) => {\n        return svg.outerHTML;\n      });\n      log(LogLevel.DEBUG, \"SVG content extracted\");\n    }\n\n    // Take a screenshot of the SVG for PNG output\n    let base64Image = \"\";\n    if (config.outputFormat === \"png\" || config.outputFormat === undefined) {\n      const svgElement = await page.$(\"#container svg\");\n      if (!svgElement) {\n        log(LogLevel.ERROR, \"SVG element not found after successful rendering\");\n        throw new Error(\"SVG element not found\");\n      }\n\n      log(LogLevel.DEBUG, \"Taking screenshot of SVG\");\n      // Take a screenshot with the correct dimensions\n      base64Image = await svgElement.screenshot({\n        omitBackground: false,\n        type: \"png\",\n        encoding: \"base64\",\n      });\n    }\n\n    // Clean up the temporary file\n    fs.unlinkSync(tempHtmlPath);\n    log(LogLevel.DEBUG, \"Temporary HTML file cleaned up\");\n\n    log(LogLevel.INFO, \"Mermaid rendered successfully\");\n\n    return { data: base64Image, svg: svgContent };\n  } catch (error) {\n    log(\n      LogLevel.ERROR,\n      `Error in renderMermaid: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n    log(\n      LogLevel.ERROR,\n      `Error stack: ${error instanceof Error ? error.stack : \"No stack trace\"}`\n    );\n\n    // Include console messages in the error for better debugging\n    if (page && page.isClosed() === false) {\n      log(LogLevel.ERROR, \"Browser console messages:\");\n      consoleMessages.forEach((msg) => log(LogLevel.ERROR, `  ${msg}`));\n    }\n\n    throw error;\n  } finally {\n    await browser.close();\n    log(LogLevel.DEBUG, \"Puppeteer browser closed\");\n  }\n}\n\n/**\n * Saves a generated Mermaid diagram to a file\n *\n * @param base64Image - The base64-encoded PNG image\n * @param name - The name to use for the file (without extension)\n * @param folder - The folder to save the file in\n * @returns The full path to the saved file\n */\nasync function saveMermaidImageToFile(\n  base64Image: string,\n  name: string,\n  folder: string\n): Promise<string> {\n  // Create the folder if it doesn't exist\n  if (!fs.existsSync(folder)) {\n    log(LogLevel.INFO, `Creating folder: ${folder}`);\n    fs.mkdirSync(folder, { recursive: true });\n  }\n\n  // Generate a filename, adding timestamp if file already exists\n  let filename = `${name}.png`;\n  const filePath = path.join(folder, filename);\n\n  if (fs.existsSync(filePath)) {\n    const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n    filename = `${name}-${timestamp}.png`;\n    log(LogLevel.INFO, `File already exists, using filename: ${filename}`);\n  }\n\n  // Save the image to the file\n  const imageBuffer = Buffer.from(base64Image, \"base64\");\n  const fullPath = path.join(folder, filename);\n  fs.writeFileSync(fullPath, imageBuffer);\n\n  log(LogLevel.INFO, `Image saved to: ${fullPath}`);\n  return fullPath;\n}\n\n/**\n * Saves a generated Mermaid SVG to a file\n *\n * @param svgContent - The SVG content as a string\n * @param name - The name to use for the file (without extension)\n * @param folder - The folder to save the file in\n * @returns The full path to the saved file\n */\nasync function saveMermaidSvgToFile(\n  svgContent: string,\n  name: string,\n  folder: string\n): Promise<string> {\n  // Create the folder if it doesn't exist\n  if (!fs.existsSync(folder)) {\n    log(LogLevel.INFO, `Creating folder: ${folder}`);\n    fs.mkdirSync(folder, { recursive: true });\n  }\n\n  // Generate a filename, adding timestamp if file already exists\n  let filename = `${name}.svg`;\n  const filePath = path.join(folder, filename);\n\n  if (fs.existsSync(filePath)) {\n    const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n    filename = `${name}-${timestamp}.svg`;\n    log(LogLevel.INFO, `File already exists, using filename: ${filename}`);\n  }\n\n  // Save the SVG to the file\n  const fullPath = path.join(folder, filename);\n  fs.writeFileSync(fullPath, svgContent, \"utf-8\");\n\n  log(LogLevel.INFO, `SVG saved to: ${fullPath}`);\n  return fullPath;\n}\n\n/**\n * Handles Mermaid syntax errors and other errors\n *\n * @param error - The error that occurred\n * @returns A response object with the error message\n */\nfunction handleMermaidError(error: unknown): {\n  content: Array<{ type: \"text\"; text: string }>;\n  isError: boolean;\n} {\n  const errorMessage = error instanceof Error ? error.message : String(error);\n  const isSyntaxError =\n    errorMessage.includes(\"Syntax error\") ||\n    errorMessage.includes(\"Parse error\") ||\n    errorMessage.includes(\"Mermaid rendering failed\");\n\n  return {\n    content: [\n      {\n        type: \"text\",\n        text: isSyntaxError\n          ? `Mermaid syntax error: ${errorMessage}\\n\\nPlease check your diagram syntax.`\n          : `Error generating diagram: ${errorMessage}`,\n      },\n    ],\n    isError: true,\n  };\n}\n\n/**\n * Processes a generate request to create a Mermaid diagram\n *\n * @param args - The arguments for the generate request\n * @returns A response object with the generated image or file path\n */\nasync function processGenerateRequest(args: {\n  code: string;\n  theme?: \"default\" | \"forest\" | \"dark\" | \"neutral\";\n  backgroundColor?: string;\n  outputFormat?: \"png\" | \"svg\";\n  name?: string;\n  folder?: string;\n}): Promise<{\n  content: Array<\n    | { type: \"text\"; text: string }\n    | { type: \"image\"; data: string; mimeType: string }\n  >;\n  isError: boolean;\n}> {\n  try {\n    const outputFormat = args.outputFormat || \"png\";\n    const result = await renderMermaid(args.code, {\n      theme: args.theme,\n      backgroundColor: args.backgroundColor,\n      outputFormat: outputFormat,\n    });\n\n    // Check if we need to save the file to a folder\n    if (!CONTENT_IMAGE_SUPPORTED) {\n      if (!args.folder) {\n        throw new Error(\n          \"Folder parameter is required when CONTENT_IMAGE_SUPPORTED is false\"\n        );\n      }\n\n      // Save the file based on format\n      let fullPath: string;\n      if (outputFormat === \"svg\") {\n        if (!result.svg) {\n          throw new Error(\"SVG content not available\");\n        }\n        fullPath = await saveMermaidSvgToFile(\n          result.svg,\n          args.name!,\n          args.folder!\n        );\n      } else {\n        fullPath = await saveMermaidImageToFile(\n          result.data,\n          args.name!,\n          args.folder!\n        );\n      }\n\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `${outputFormat.toUpperCase()} saved to: ${fullPath}`,\n          },\n        ],\n        isError: false,\n      };\n    }\n\n    // If folder is provided and CONTENT_IMAGE_SUPPORTED is true, save the file to the folder\n    // but also return the content in the response\n    let savedMessage = \"\";\n    if (args.folder && args.name) {\n      try {\n        let fullPath: string;\n        if (outputFormat === \"svg\") {\n          if (!result.svg) {\n            throw new Error(\"SVG content not available\");\n          }\n          fullPath = await saveMermaidSvgToFile(\n            result.svg,\n            args.name,\n            args.folder\n          );\n        } else {\n          fullPath = await saveMermaidImageToFile(\n            result.data,\n            args.name,\n            args.folder\n          );\n        }\n        savedMessage = `${outputFormat.toUpperCase()} also saved to: ${fullPath}`;\n        log(LogLevel.INFO, savedMessage);\n      } catch (saveError) {\n        log(\n          LogLevel.ERROR,\n          `Failed to save ${outputFormat} to folder: ${(saveError as Error).message}`\n        );\n        savedMessage = `Note: Failed to save ${outputFormat} to folder: ${\n          (saveError as Error).message\n        }`;\n      }\n    }\n\n    // Return the appropriate content based on format\n    if (outputFormat === \"svg\") {\n      if (!result.svg) {\n        throw new Error(\"SVG content not available\");\n      }\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: savedMessage\n              ? `Here is the generated SVG:\\n\\n${result.svg}\\n\\n${savedMessage}`\n              : `Here is the generated SVG:\\n\\n${result.svg}`,\n          },\n        ],\n        isError: false,\n      };\n    } else {\n      // Return the PNG image in the response\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: savedMessage\n              ? `Here is the generated image. ${savedMessage}`\n              : \"Here is the generated image\",\n          },\n          {\n            type: \"image\",\n            data: result.data,\n            mimeType: \"image/png\",\n          },\n        ],\n        isError: false,\n      };\n    }\n  } catch (error) {\n    return handleMermaidError(error);\n  }\n}\n\n// Tool handlers\nserver.setRequestHandler(ListToolsRequestSchema, async () => ({\n  tools: [GENERATE_TOOL],\n}));\n\n// Set up the request handler for tool calls\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n  try {\n    const { name, arguments: args } = request.params;\n\n    if (!args) {\n      throw new Error(\"No arguments provided\");\n    }\n\n    log(\n      LogLevel.INFO,\n      `Received request: ${name} with args: ${JSON.stringify(args)}`\n    );\n\n    if (name === \"generate\") {\n      log(LogLevel.INFO, \"Rendering Mermaid PNG\");\n      if (!isGenerateArgs(args)) {\n        throw new Error(\"Invalid arguments for generate\");\n      }\n\n      // Process the generate request\n      return await processGenerateRequest(args);\n    }\n\n    return {\n      content: [{ type: \"text\", text: `Unknown tool: ${name}` }],\n      isError: true,\n    };\n  } catch (error) {\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Error: ${\n            error instanceof Error ? error.message : String(error)\n          }`,\n        },\n      ],\n      isError: true,\n    };\n  }\n});\n\nasync function runServer() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n  log(LogLevel.INFO, \"Mermaid MCP Server running on stdio\");\n}\n\nrunServer().catch((error) => {\n  log(\n    LogLevel.CRITICAL,\n    `Fatal error running server: ${\n      error instanceof Error ? error.message : String(error)\n    }`\n  );\n  process.exit(1);\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@peng-shawn/mermaid-mcp-server\",\n  \"version\": \"0.2.0\",\n  \"description\": \"A Model Context Protocol (MCP) server that converts Mermaid diagrams to PNG images\",\n  \"main\": \"dist/index.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"start\": \"node dist/index.js\",\n    \"release\": \"scripts/release.sh\"\n  },\n  \"bin\": {\n    \"mermaid-mcp-server\": \"dist/index.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"keywords\": [\n    \"mermaid\",\n    \"diagram\",\n    \"mcp\",\n    \"model-context-protocol\",\n    \"ai\",\n    \"visualization\"\n  ],\n  \"author\": \"Shawn Peng\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.6.1\",\n    \"import-meta-resolve\": \"^4.1.0\",\n    \"mermaid\": \"^11.4.1\",\n    \"puppeteer\": \"^24.3.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.13.9\",\n    \"typescript\": \"^5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/peng-shawn/mermaid-mcp-server.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/peng-shawn/mermaid-mcp-server/issues\"\n  },\n  \"homepage\": \"https://github.com/peng-shawn/mermaid-mcp-server#readme\"\n}\n"
  },
  {
    "path": "scripts/release.sh",
    "content": "#!/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  echo \"Error: No version specified\"\n  echo \"Usage: ./scripts/release.sh <version|patch|minor|major>\"\n  echo \"Examples:\"\n  echo \"  ./scripts/release.sh 0.1.4    # Set specific version\"\n  echo \"  ./scripts/release.sh patch    # Increment patch version (0.1.3 -> 0.1.4)\"\n  echo \"  ./scripts/release.sh minor    # Increment minor version (0.1.3 -> 0.2.0)\"\n  echo \"  ./scripts/release.sh major    # Increment major version (0.1.3 -> 1.0.0)\"\n  exit 1\nfi\n\nVERSION_ARG=$1\n\n# Check if the version is a semantic increment or specific version\nif [[ \"$VERSION_ARG\" =~ ^(patch|minor|major)$ ]]; then\n  # It's a semantic increment\n  INCREMENT_TYPE=$VERSION_ARG\n  # Get the current version to display in logs\n  CURRENT_VERSION=$(grep -o '\"version\": \"[^\"]*\"' package.json | cut -d'\"' -f4)\n  echo \"Using semantic increment: $INCREMENT_TYPE (current version: $CURRENT_VERSION)\"\nelse\n  # It's a specific version - validate semver format\n  if ! [[ $VERSION_ARG =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z-]+)?(\\+[0-9A-Za-z-]+)?$ ]]; then\n    echo \"Error: Version must follow semantic versioning (e.g., 1.2.3, 1.2.3-beta, etc.)\"\n    echo \"Or use one of: patch, minor, major\"\n    exit 1\n  fi\n  # Store the specific version\n  SPECIFIC_VERSION=$VERSION_ARG\n  echo \"Using specific version: $SPECIFIC_VERSION\"\nfi\n\n# Check if we're on the main branch\nCURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)\nif [ \"$CURRENT_BRANCH\" != \"main\" ]; then\n  echo \"Warning: You are not on the main branch. Current branch: $CURRENT_BRANCH\"\n  read -p \"Do you want to continue? (y/n) \" -n 1 -r\n  echo\n  if [[ ! $REPLY =~ ^[Yy]$ ]]; then\n    exit 1\n  fi\nfi\n\n# Check if working directory is clean\nif [ -n \"$(git status --porcelain)\" ]; then\n  echo \"Error: Working directory is not clean. Please commit or stash your changes.\"\n  exit 1\nfi\n\n# Pull latest changes\necho \"Pulling latest changes from origin...\"\ngit pull origin main\n\n# Check current versions in files\nPACKAGE_VERSION=$(grep -o '\"version\": \"[^\"]*\"' package.json | cut -d'\"' -f4)\nPACKAGE_LOCK_VERSION=$(grep -o '\"version\": \"[^\"]*\"' package-lock.json | head -1 | cut -d'\"' -f4)\nINDEX_VERSION=$(grep -o 'version: \"[^\"]*\"' index.ts | cut -d'\"' -f2)\n\necho \"Current versions:\"\necho \"- package.json: $PACKAGE_VERSION\"\necho \"- package-lock.json: $PACKAGE_LOCK_VERSION\"\necho \"- index.ts: $INDEX_VERSION\"\n\n# Function to update index.ts version\nupdate_index_version() {\n  local old_version=$1\n  local new_version=$2\n  local commit_msg=$3\n  \n  echo \"Updating version in index.ts from $old_version to $new_version...\"\n  sed -i '' \"s/version: \\\"$old_version\\\"/version: \\\"$new_version\\\"/\" index.ts\n  git add index.ts\n  \n  if [ -n \"$commit_msg\" ]; then\n    git commit -m \"$commit_msg\"\n  fi\n}\n\nif [ \"$PACKAGE_VERSION\" != \"$PACKAGE_LOCK_VERSION\" ] || [ \"$PACKAGE_VERSION\" != \"$INDEX_VERSION\" ]; then\n  echo \"Warning: Version mismatch detected between files.\"\n  \n  if [ -n \"$SPECIFIC_VERSION\" ]; then\n    echo \"Will update all files to version: $SPECIFIC_VERSION\"\n  else\n    echo \"Will update all files using increment: $INCREMENT_TYPE\"\n  fi\n  \n  read -p \"Do you want to continue? (y/n) \" -n 1 -r\n  echo\n  if [[ ! $REPLY =~ ^[Yy]$ ]]; then\n    exit 1\n  fi\nfi\n\n# Handle version updates based on increment type or specific version\nif [ -n \"$INCREMENT_TYPE\" ]; then\n  # For semantic increments\n  echo \"Incrementing version ($INCREMENT_TYPE)...\"\n  \n  # Use npm version to increment the version\n  npm version $INCREMENT_TYPE --no-git-tag-version\n  \n  # Get the new version\n  NEW_VERSION=$(grep -o '\"version\": \"[^\"]*\"' package.json | cut -d'\"' -f4)\n  \n  # Update index.ts with the new version\n  update_index_version \"$INDEX_VERSION\" \"$NEW_VERSION\"\n  \n  # Commit all changes and create tag\n  git add package.json package-lock.json\n  git commit -m \"chore: release version $NEW_VERSION\"\n  git tag -a \"v$NEW_VERSION\" -m \"Version $NEW_VERSION\"\nelse\n  # For specific version\n  # Use npm version to update package.json and package-lock.json without git operations\n  echo \"Updating version in package.json and package-lock.json...\"\n  npm version $SPECIFIC_VERSION --no-git-tag-version\n  \n  # Update index.ts\n  update_index_version \"$INDEX_VERSION\" \"$SPECIFIC_VERSION\"\n  \n  # Commit all changes and create tag\n  git add package.json package-lock.json\n  git commit -m \"chore: release version $SPECIFIC_VERSION\"\n  git tag -a \"v$SPECIFIC_VERSION\" -m \"Version $SPECIFIC_VERSION\"\nfi\n\n# Get the final version for pushing the tag\nFINAL_VERSION=$(grep -o '\"version\": \"[^\"]*\"' package.json | cut -d'\"' -f4)\n\n# Push changes and tag to remote\necho \"Pushing changes and tag to remote...\"\ngit push origin main\ngit push origin v$FINAL_VERSION\n\necho \"Release process completed for version $FINAL_VERSION\"\necho \"The GitHub workflow will now build and publish the package to npm\"\necho \"Check the Actions tab in your GitHub repository for progress\" "
  },
  {
    "path": "smithery.yaml",
    "content": "# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml\n\nstartCommand:\n  type: stdio\n  configSchema:\n    # JSON Schema defining the configuration options for the MCP.\n    type: object\n    required: []\n    properties:\n      contentImageSupported:\n        type: boolean\n        default: true\n        description: Set to true if images should be returned directly in the response.\n          Set to false if images should be saved to disk, requiring 'name' and\n          'folder' parameters.\n  commandFunction:\n    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.\n    |-\n    (config) => ({\n      command: 'node',\n      args: ['dist/index.js'],\n      env: {\n        CONTENT_IMAGE_SUPPORTED: config.contentImageSupported ? 'true' : 'false',\n        PUPPETEER_EXECUTABLE_PATH: '/usr/bin/chromium'\n      }\n    })\n  exampleConfig:\n    contentImageSupported: true\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n      \"target\": \"ES2022\",\n      \"module\": \"Node16\",\n      \"moduleResolution\": \"Node16\",\n      \"outDir\": \"./dist\",\n      \"rootDir\": \".\",\n      \"strict\": true,\n      \"esModuleInterop\": true,\n      \"skipLibCheck\": true,\n      \"forceConsistentCasingInFileNames\": true\n    },\n    \"include\": [\"index.ts\"]\n  }"
  }
]