# Directory Structure
```
├── .gitignore
├── .python-version
├── main.py
├── mcp_client.py
├── mcp_server.py
├── pyproject.toml
├── README_MCP.md
├── README.md
├── requirements.txt
├── server.py
├── test.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.10
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
temp_files/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Mermaid Diagram Generator Server
A simple Flask server that generates diagrams from Mermaid syntax using mermaid-cli.
## Prerequisites
- Python 3.7+
- Node.js and npm (for mermaid-cli)
- mermaid-cli installed globally: `npm install -g @mermaid-js/mermaid-cli`
## Installation
1. Clone this repository
2. Install Python dependencies:
```
pip install -r requirements.txt
```
3. Ensure mermaid-cli is installed globally:
```
npm install -g @mermaid-js/mermaid-cli
```
## Running the Server
Start the server with:
```
python server.py
```
By default, the server runs on `http://localhost:5000`.
### Temporary Files
The server creates a local directory called `temp_files` in the project folder for storing temporary files. This approach:
- Avoids permission issues with system temp directories
- Works better in virtual environments
- Automatically cleans up files older than 30 minutes
## API Usage
### Web Interface
Open your browser and navigate to `http://localhost:5000` to use the web interface.
### API Endpoint
Send a POST request to `/generate` with a JSON body containing your Mermaid diagram:
```json
{
"mermaid": "graph TD\nA[Client] --> B[Load Balancer]\nB --> C[Server1]\nB --> D[Server2]",
"theme": "default", // optional: default, dark, forest, neutral
"background": "white" // optional: white, transparent
}
```
The server will return a PNG image of the rendered diagram.
Example using curl:
```bash
curl -X POST http://localhost:5000/generate \
-H "Content-Type: application/json" \
-d '{"mermaid":"graph TD\nA[Client] --> B[Load Balancer]"}' \
--output diagram.png
```
## Testing
Run the included test script to verify everything is working:
```
python test.py
```
This will generate a sample diagram and save it as `output_diagram.png`.
## Troubleshooting
If you encounter errors:
1. Ensure mermaid-cli (mmdc) is installed and accessible in your PATH
2. Check server logs for specific error messages
3. Make sure your Mermaid syntax is valid
4. Verify the `temp_files` directory exists and has appropriate permissions
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
flask==2.3.3
Werkzeug==2.3.7
requests==2.31.0
mcp>=0.1.0
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
def main():
print("Hello from test-mcp!")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "test-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"mcp[cli]>=1.4.1",
]
```
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
import requests
import os
import sys
# Mermaid diagram text
mermaid_text = """
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server 1]
B --> D[Server 2]
B --> E[Server 3]
C --> F[Database]
D --> F
E --> F
"""
# Base URL of the server
base_url = "http://localhost:5000"
def test_generate_diagram():
"""Test generating a diagram via the API"""
print("Testing diagram generation API...")
# Prepare the request data
data = {
"mermaid": mermaid_text,
"theme": "default",
"background": "white"
}
try:
# Send the request
print(f"Sending request to {base_url}/generate...")
response = requests.post(f"{base_url}/generate", json=data)
# Check if the request was successful
if response.status_code == 200:
# Save the image to a file
output_file = "output_diagram.png"
with open(output_file, "wb") as f:
f.write(response.content)
print(f"Diagram generated successfully and saved to {output_file}")
file_size = os.path.getsize(output_file)
print(f"File size: {file_size} bytes")
if file_size == 0:
print("Warning: Generated file is empty!")
return False
return True
else:
print(f"Error generating diagram. Status code: {response.status_code}")
print(f"Error message: {response.text}")
return False
except requests.exceptions.ConnectionError:
print(f"Error: Could not connect to server at {base_url}")
print("Make sure the server is running and accessible.")
return False
except Exception as e:
print(f"Unexpected error: {str(e)}")
return False
if __name__ == "__main__":
success = test_generate_diagram()
if not success:
sys.exit(1) # Exit with error code if test failed
```
--------------------------------------------------------------------------------
/README_MCP.md:
--------------------------------------------------------------------------------
```markdown
# Mermaid Diagram Generator for MCP
This project provides a Mermaid diagram generator tool for the Model Control Protocol (MCP) framework. It allows you to generate diagrams from Mermaid syntax through MCP tools and resources.
## Features
- Generate diagrams from Mermaid syntax using MCP tools
- Multiple theme options (default, dark, forest, neutral)
- Background options (white, transparent)
- Access example diagrams through resources
- Automatic cleanup of temporary files
- Native image handling using MCP's `Image` class
## Prerequisites
- Python 3.7+
- Node.js and npm (for mermaid-cli)
- mermaid-cli installed globally: `npm install -g @mermaid-js/mermaid-cli`
- MCP Python package: `pip install mcp`
## Installation
1. Clone this repository
2. Install Python dependencies:
```
pip install mcp
```
3. Ensure mermaid-cli is installed globally:
```
npm install -g @mermaid-js/mermaid-cli
```
## Usage
### Running the MCP Server
Start the MCP server with:
```
python mcp_server.py
```
By default, the server runs on port 7000.
### Using the Client
The `mcp_client.py` file demonstrates how to use the MCP client to interact with the server:
```python
from mcp_client import MermaidClient
# Create client
client = MermaidClient()
# Get example diagrams
examples = client.get_examples()
# Generate a diagram
client.generate_diagram(
mermaid_code="""
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server]
""",
output_path="diagram.png",
theme="dark",
background="transparent"
)
```
### Running the Example Client
```
python mcp_client.py
```
This will generate example diagrams in the `output` directory.
## MCP Server API
### Tools
- **generate_mermaid_diagram**: Generates a PNG image from Mermaid code
- Parameters:
- `mermaid_code` (str): The Mermaid diagram code
- `theme` (str, optional): Theme to use (default, dark, forest, neutral)
- `background` (str, optional): Background color (white, transparent)
- Returns: `Image` object containing the diagram
### Resources
- **mermaid://examples**: Returns a dictionary of example Mermaid diagrams
- Examples include: flowchart, sequence diagram, class diagram, state diagram
## Image Handling
The server uses MCP's `Image` class from `FastMCP` to return images, providing:
- Direct handling of binary image data
- Proper format specification (PNG)
- Alt text for accessibility
- Automatic serialization by the MCP framework
The client code demonstrates how to handle the `Image` object returned by the server and save it to a file. The MCP client automatically converts the `Image` object to a format that can be easily processed by client applications.
## Temporary Files
The server creates a local directory called `temp_files` in the project folder for storing temporary files. This directory is cleaned up automatically to remove files older than 30 minutes.
## Troubleshooting
If you encounter errors:
1. Ensure mermaid-cli (mmdc) is installed and accessible in your PATH
2. Check server logs for specific error messages
3. Make sure your Mermaid syntax is valid
4. Verify the `temp_files` directory exists and has appropriate permissions
```
--------------------------------------------------------------------------------
/mcp_client.py:
--------------------------------------------------------------------------------
```python
from mcp.client import MCPClient
import os
import logging
from typing import Optional
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class MermaidClient:
def __init__(self, server_url: str = "http://localhost:7000"):
"""
Initialize the Mermaid client.
Args:
server_url: URL of the MCP server
"""
self.client = MCPClient(server_url)
logging.info(f"Connected to MCP server at {server_url}")
def get_examples(self) -> dict:
"""
Get example Mermaid diagrams.
Returns:
Dictionary of example diagrams
"""
try:
examples = self.client.get_resource("mermaid://examples")
logging.info(f"Retrieved {len(examples)} example diagrams")
return examples
except Exception as e:
logging.error(f"Error getting examples: {str(e)}")
raise
def generate_diagram(
self,
mermaid_code: str,
output_path: str,
theme: Optional[str] = "default",
background: Optional[str] = "white"
) -> str:
"""
Generate a diagram from Mermaid code and save it to a file.
Args:
mermaid_code: The Mermaid diagram code
output_path: Path to save the image
theme: The theme to use (default, dark, forest, neutral)
background: The background color (white, transparent)
Returns:
Path to the saved image
"""
try:
logging.info(f"Generating diagram with theme '{theme}' and background '{background}'")
# Call the MCP tool to generate the diagram
# The result is now an Image object from FastMCP which contains raw image data
image_result = self.client.call_tool(
"generate_mermaid_diagram",
mermaid_code=mermaid_code,
theme=theme,
background=background
)
# The client already handles the Image object conversion,
# so we should get raw image data directly
if hasattr(image_result, 'data') and isinstance(image_result.data, bytes):
# Handle Image object with direct data attribute
image_data = image_result.data
elif isinstance(image_result, dict) and 'data' in image_result:
# Handle dict response with data field
image_data = image_result['data']
if not isinstance(image_data, bytes):
logging.warning("Converting data to bytes")
image_data = bytes(image_data)
elif isinstance(image_result, bytes):
# Handle direct bytes response
image_data = image_result
else:
# Log the actual type for debugging
logging.error(f"Unexpected result type: {type(image_result)}")
logging.error(f"Result content: {str(image_result)[:200]}...")
raise ValueError(f"Unexpected image format returned from server")
# Save the image to a file
with open(output_path, 'wb') as f:
f.write(image_data)
logging.info(f"Saved diagram to {output_path} ({len(image_data)} bytes)")
return output_path
except Exception as e:
logging.error(f"Error generating diagram: {str(e)}")
raise
def main():
# Create the client
client = MermaidClient()
try:
# Get example diagrams
examples = client.get_examples()
# Create output directory
os.makedirs("output", exist_ok=True)
# Generate diagrams for each example
for name, mermaid_code in examples.items():
output_path = f"output/{name}_diagram.png"
client.generate_diagram(mermaid_code, output_path)
print(f"Generated {name} diagram: {output_path}")
# Generate a custom diagram
custom_code = """
graph LR
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug]
D --> B
"""
client.generate_diagram(
custom_code,
"output/custom_diagram.png",
theme="dark",
background="transparent"
)
print("Generated custom diagram: output/custom_diagram.png")
except Exception as e:
print(f"Error: {str(e)}")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/mcp_server.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import FastMCP, Image
import base64
import subprocess
import os
import uuid
import logging
import sys
from typing import Optional
from mcp.types import ImageContent
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Create a local temp directory in the project folder
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp_files')
os.makedirs(TEMP_DIR, exist_ok=True)
logging.info(f"Temporary directory at: {TEMP_DIR}")
# Create an MCP server
mcp = FastMCP("Mermaid Diagram Generator")
@mcp.tool()
def generate_mermaid_diagram(
mermaid_code: str,
theme: Optional[str] = "default",
background: Optional[str] = "white"
) -> Image:
"""
Generate a diagram from Mermaid code and return it as an image.
Args:
mermaid_code: The Mermaid diagram code
theme: The theme to use (default, dark, forest, neutral)
background: The background color (white, transparent)
Returns:
An image of the generated diagram
"""
logging.info(f"Generating diagram with theme '{theme}' and background '{background}'")
logging.info(f"Mermaid code: {mermaid_code[:50]}...")
try:
# Create temporary files with unique names
unique_id = str(uuid.uuid4())
input_file = os.path.join(TEMP_DIR, f"{unique_id}.mmd")
output_file = os.path.join(TEMP_DIR, f"{unique_id}.png")
# Write mermaid text to the input file
with open(input_file, 'w', encoding='utf-8') as f:
f.write(mermaid_code)
# Find mmdc executable
mmdc_cmd = find_mmdc_executable()
if not mmdc_cmd:
raise Exception("mermaid-cli not found. Please ensure it's installed and in PATH")
# Build the command
cmd = [mmdc_cmd, '-i', input_file, '-o', output_file]
# Add optional parameters
if theme:
cmd.extend(['-t', theme])
if background:
cmd.extend(['-b', background])
logging.info(f"Running command: {' '.join(cmd)}")
# Run the command
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
logging.error(f"Error running mmdc: {result.stderr}")
raise Exception(f"Error generating diagram: {result.stderr}")
# Check if output file exists and has content
if not os.path.exists(output_file):
raise Exception("Failed to create diagram: output file not found")
file_size = os.path.getsize(output_file)
if file_size == 0:
raise Exception("Generated image is empty")
# Read the image file
with open(output_file, 'rb') as f:
image_data = f.read()
# Create and return Image object
# The Image class from FastMCP handles the image data directly
return Image(data=image_data, format="png", alt_text=f"Mermaid diagram with theme {theme} and background {background}")
except Exception as e:
logging.exception("Error generating diagram")
raise e
finally:
# Clean up temporary files
try:
if os.path.exists(input_file):
os.remove(input_file)
if os.path.exists(output_file):
os.remove(output_file)
except Exception as cleanup_error:
logging.exception(f"Error during cleanup: {cleanup_error}")
def find_mmdc_executable():
"""
Find the mmdc executable path.
Returns:
Path to mmdc executable or None if not found
"""
mmdc_paths = [
'mmdc', # Default PATH
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'npm', 'mmdc.cmd'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'npm', 'mmdc'),
os.path.join('C:', 'Program Files', 'nodejs', 'node_modules', '@mermaid-js', 'mermaid-cli', 'bin', 'mmdc'),
os.path.join('C:', 'Program Files', 'nodejs', 'node_modules', '.bin', 'mmdc')
]
for path in mmdc_paths:
logging.info(f"Trying mmdc at: {path}")
try:
test_result = subprocess.run([path, '--version'], capture_output=True, text=True)
if test_result.returncode == 0:
logging.info(f"Found working mmdc at: {path}")
return path
except Exception as e:
logging.info(f"Could not use mmdc at {path}: {str(e)}")
return None
@mcp.resource("mermaid://examples")
def get_mermaid_examples() -> dict:
"""Get example Mermaid diagrams"""
return {
"flowchart": """
graph TD
A[Client] --> B[Load Balancer]
B --> C[Server 1]
B --> D[Server 2]
B --> E[Server 3]
C --> F[Database]
D --> F
E --> F
""",
"sequence": """
sequenceDiagram
participant Browser
participant API
participant Database
Browser->>API: GET /data
API->>Database: SELECT * FROM data
Database-->>API: Return data
API-->>Browser: Return JSON
""",
"class": """
classDiagram
class Animal {
+String name
+move()
}
class Dog {
+bark()
}
class Bird {
+fly()
}
Animal <|-- Dog
Animal <|-- Bird
""",
"state": """
stateDiagram-v2
[*] --> Idle
Idle --> Processing: Start
Processing --> Completed
Processing --> Error
Completed --> [*]
Error --> Idle: Retry
"""
}
# Cleanup function for old temp files
def cleanup_old_files(max_age_minutes=30):
"""Remove files older than max_age_minutes from the temp directory"""
import time
try:
current_time = time.time()
for filename in os.listdir(TEMP_DIR):
file_path = os.path.join(TEMP_DIR, filename)
if os.path.isfile(file_path):
# Check file age
file_age_minutes = (current_time - os.path.getmtime(file_path)) / 60
if file_age_minutes > max_age_minutes:
os.remove(file_path)
logging.info(f"Removed old file: {file_path}")
except Exception as e:
logging.exception(f"Error cleaning up old files: {e}")
if __name__ == "__main__":
# Clean up old files before starting
cleanup_old_files()
# Start the MCP server
mcp.run()
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
# server.py
from flask import Flask, request, send_file, Response
import subprocess
import tempfile
import os
import uuid
import logging
import shutil
import sys
app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Create a local temp directory in the project folder
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp_files')
os.makedirs(TEMP_DIR, exist_ok=True)
logging.info(f"Temporary directory at: {TEMP_DIR}")
logging.info(f"Python executable: {sys.executable}")
logging.info(f"Current directory: {os.getcwd()}")
@app.route('/generate', methods=['POST'])
def generate_diagram():
input_file = None
output_file = None
try:
# Get mermaid text from the request
if not request.is_json:
return Response("Request must be JSON", status=400)
data = request.get_json()
if 'mermaid' not in data:
return Response("Missing 'mermaid' field in JSON", status=400)
mermaid_text = data['mermaid']
logging.info(f"Received mermaid text: {mermaid_text[:50]}...")
# Create temporary files with unique names in our local temp directory
unique_id = str(uuid.uuid4())
input_file = os.path.join(TEMP_DIR, f"{unique_id}.mmd")
output_file = os.path.join(TEMP_DIR, f"{unique_id}.png")
logging.info(f"Input file: {input_file}")
logging.info(f"Output file: {output_file}")
# Write mermaid text to the input file
with open(input_file, 'w', encoding='utf-8') as f:
f.write(mermaid_text)
logging.info(f"Wrote mermaid text to input file, size: {os.path.getsize(input_file)} bytes")
# Run mmdc command
# Try to find mmdc in common locations if direct command fails
mmdc_paths = [
'mmdc', # Default PATH
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'npm', 'mmdc.cmd'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'npm', 'mmdc'),
os.path.join('C:', 'Program Files', 'nodejs', 'node_modules', '@mermaid-js', 'mermaid-cli', 'bin', 'mmdc'),
os.path.join('C:', 'Program Files', 'nodejs', 'node_modules', '.bin', 'mmdc')
]
# Try each path
mmdc_cmd = None
for path in mmdc_paths:
logging.info(f"Trying mmdc at: {path}")
try:
# Just test if the command exists
test_result = subprocess.run([path, '--version'],
capture_output=True,
text=True)
if test_result.returncode == 0:
mmdc_cmd = path
logging.info(f"Found working mmdc at: {path}")
break
except Exception as e:
logging.info(f"Could not use mmdc at {path}: {str(e)}")
if not mmdc_cmd:
logging.error("Could not find mmdc executable in any location")
return Response("mermaid-cli not found. Please ensure it's installed and in PATH", status=500)
cmd = [mmdc_cmd, '-i', input_file, '-o', output_file]
# Add optional parameters if provided
if 'theme' in data:
cmd.extend(['-t', data['theme']])
if 'background' in data:
cmd.extend(['-b', data['background']])
logging.info(f"Running command: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True)
logging.info(f"Command exit code: {result.returncode}")
logging.info(f"Command stdout: {result.stdout}")
logging.info(f"Command stderr: {result.stderr}")
if result.returncode != 0:
logging.error(f"Error running mmdc: {result.stderr}")
return Response(f"Error generating diagram: {result.stderr}", status=500)
except Exception as cmd_error:
logging.exception(f"Exception running command: {str(cmd_error)}")
return Response(f"Error executing mmdc command: {str(cmd_error)}", status=500)
# Check if output file exists
if not os.path.exists(output_file):
logging.error(f"Output file not created: {output_file}")
return Response("Failed to create diagram: output file not found", status=500)
file_size = os.path.getsize(output_file)
logging.info(f"Output file size: {file_size} bytes")
if file_size == 0:
logging.error("Generated image has zero size")
return Response("Generated image is empty", status=500)
# Return the generated image
return send_file(output_file, mimetype='image/png', as_attachment=False)
except Exception as e:
logging.exception("Error processing request")
return Response(f"Server error: {str(e)}", status=500)
finally:
# Clean up temporary files
try:
if input_file and os.path.exists(input_file):
os.remove(input_file)
logging.info(f"Cleaned up input file: {input_file}")
# We don't delete the output file here as send_file needs it
# The cleanup function below will handle old files
except Exception as cleanup_error:
logging.exception(f"Error during cleanup: {cleanup_error}")
# Cleanup function to remove old temp files
def cleanup_old_files(max_age_minutes=30):
"""Remove files older than max_age_minutes from the temp directory"""
try:
current_time = time.time()
for filename in os.listdir(TEMP_DIR):
file_path = os.path.join(TEMP_DIR, filename)
if os.path.isfile(file_path):
# Check file age
file_age_minutes = (current_time - os.path.getmtime(file_path)) / 60
if file_age_minutes > max_age_minutes:
os.remove(file_path)
logging.info(f"Removed old file: {file_path}")
except Exception as e:
logging.exception(f"Error cleaning up old files: {e}")
@app.route('/', methods=['GET'])
def index():
return """
<html>
<head>
<title>Mermaid Diagram Generator</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
textarea { width: 100%; height: 200px; margin-bottom: 10px; }
button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
#result { margin-top: 20px; }
pre { background-color: #f5f5f5; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h1>Mermaid Diagram Generator</h1>
<p>Enter your Mermaid diagram text below:</p>
<textarea id="mermaidText">graph TD
A[Client] --> B[Load Balancer]
B --> C[Server1]
B --> D[Server2]</textarea>
<div>
<label for="theme">Theme:</label>
<select id="theme">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="forest">Forest</option>
<option value="neutral">Neutral</option>
</select>
<label for="background">Background:</label>
<select id="background">
<option value="white">White</option>
<option value="transparent">Transparent</option>
</select>
</div>
<button onclick="generateDiagram()">Generate Diagram</button>
<div id="result"></div>
<script>
function generateDiagram() {
const mermaidText = document.getElementById('mermaidText').value;
const theme = document.getElementById('theme').value;
const background = document.getElementById('background').value;
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = 'Processing...';
fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
mermaid: mermaidText,
theme: theme,
background: background
})
})
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(text) });
}
return response.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
resultDiv.innerHTML = `<h3>Generated Diagram:</h3><img src="${url}" alt="Generated Diagram">`;
})
.catch(error => {
resultDiv.innerHTML = `<h3>Error:</h3><pre>${error.message}</pre>`;
});
}
</script>
</body>
</html>
"""
if __name__ == '__main__':
# Import here to avoid issues when importing the app in other modules
import time
# Schedule cleanup of old files on startup
cleanup_old_files()
app.run(host='0.0.0.0', port=5000, debug=True)
```