#!/usr/bin/env node
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";
import { exec } from "child_process";
import { promisify } from "util";
import { writeFile, readFile, unlink } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const execAsync = promisify(exec);
// PlantUML JAR path - can be configured via environment variable
const PLANTUML_JAR = join(__dirname, "..", "plantuml.jar");
interface GenerateDiagramArgs {
source: string;
format?: "png" | "svg" | "txt" | "utxt" | "eps" | "latex" | "pdf";
darkMode?: boolean;
filename?: string;
}
interface CheckSyntaxArgs {
source: string;
}
interface ExtractSourceArgs {
filePath: string;
}
const tools: Tool[] = [
{
name: "check_syntax",
description: "Check PlantUML diagram syntax without generating images. Returns syntax errors if any.",
inputSchema: {
type: "object",
properties: {
source: {
type: "string",
description: "PlantUML source code to check",
},
},
required: ["source"],
},
},
{
name: "extract_source",
description: "Extract embedded PlantUML source from PNG or SVG metadata.",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path to the PNG or SVG file",
},
},
required: ["filePath"],
},
},
{
name: "get_plantuml_version",
description: "Get PlantUML and Java version information.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "generate_diagram",
description: "Generate a diagram from PlantUML source code. Returns the diagram as base64-encoded data and optionally saves to a file.",
inputSchema: {
type: "object",
properties: {
source: {
type: "string",
description: "PlantUML source code for the diagram",
},
format: {
type: "string",
enum: ["png", "svg", "txt", "utxt", "eps", "latex", "pdf"],
description: "Output format for the diagram (default: png)",
default: "png",
},
darkMode: {
type: "boolean",
description: "Render diagram in dark mode",
default: false,
},
filename: {
type: "string",
description: "Optional output filename path to save the diagram to disk",
},
},
required: ["source"],
},
},
];
async function generateDiagram(args: GenerateDiagramArgs): Promise<string> {
const { source, format = "png", darkMode = false, filename } = args;
// Create temporary files
const tmpDir = tmpdir();
const inputFile = join(tmpDir, `plantuml-${Date.now()}.puml`);
const outputExt = format === "txt" || format === "utxt" ? "atxt" : format;
const outputFile = join(tmpDir, `plantuml-${Date.now()}.${outputExt}`);
try {
// Write source to temporary file
await writeFile(inputFile, source, "utf-8");
// Build command
const darkModeFlag = darkMode ? "--dark-mode" : "";
const formatFlag = `--${format}`;
const command = `java -jar "${PLANTUML_JAR}" ${formatFlag} ${darkModeFlag} -pipe < "${inputFile}"`;
// Execute PlantUML
const { stdout, stderr } = await execAsync(command, {
encoding: "buffer",
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
});
if (stderr && stderr.length > 0) {
const errorText = stderr.toString("utf-8");
if (errorText.includes("Error") || errorText.includes("Syntax")) {
throw new Error(`PlantUML error: ${errorText}`);
}
}
// Save to file if filename is provided
if (filename) {
await writeFile(filename, stdout);
// Save the .puml source file alongside the generated image
const pumlFilename = filename.replace(/\.[^.]+$/, '.puml');
await writeFile(pumlFilename, source, "utf-8");
}
// Return base64-encoded output
const base64Data = stdout.toString("base64");
return base64Data;
} finally {
// Cleanup temporary files
try {
await unlink(inputFile);
} catch (e) {
// Ignore cleanup errors
}
}
}
async function checkSyntax(args: CheckSyntaxArgs): Promise<string> {
const { source } = args;
const tmpDir = tmpdir();
const inputFile = join(tmpDir, `plantuml-check-${Date.now()}.puml`);
try {
await writeFile(inputFile, source, "utf-8");
const command = `java -jar "${PLANTUML_JAR}" --check-syntax "${inputFile}"`;
try {
const { stdout, stderr } = await execAsync(command);
return "Syntax is valid";
} catch (error: any) {
// PlantUML returns non-zero exit code on syntax errors
const output = error.stdout || error.stderr || error.message;
return `Syntax errors found:\n${output}`;
}
} finally {
try {
await unlink(inputFile);
} catch (e) {
// Ignore cleanup errors
}
}
}
async function extractSource(args: ExtractSourceArgs): Promise<string> {
const { filePath } = args;
try {
const command = `java -jar "${PLANTUML_JAR}" --extract-source "${filePath}"`;
const { stdout, stderr } = await execAsync(command);
if (stderr && stderr.includes("Error")) {
throw new Error(`Failed to extract source: ${stderr}`);
}
return stdout || "Source extracted successfully";
} catch (error: any) {
throw new Error(`Failed to extract source: ${error.message}`);
}
}
async function getVersion(): Promise<string> {
try {
const command = `java -jar "${PLANTUML_JAR}" --version`;
const { stdout } = await execAsync(command);
return stdout;
} catch (error: any) {
throw new Error(`Failed to get version: ${error.message}`);
}
}
const server = new Server(
{
name: "plantuml-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "generate_diagram": {
const result = await generateDiagram(args as unknown as GenerateDiagramArgs);
const diagramArgs = args as unknown as GenerateDiagramArgs;
const format = diagramArgs.format || "png";
let message = `Diagram generated successfully in ${format} format.`;
if (diagramArgs.filename) {
const pumlFilename = diagramArgs.filename.replace(/\.[^.]+$/, '.puml');
message += `\n\nFiles saved:\n- Image: ${diagramArgs.filename}\n- Source: ${pumlFilename}`;
}
message += `\n\nBase64 data:\n${result}`;
return {
content: [
{
type: "text",
text: message,
},
],
};
}
case "check_syntax": {
const result = await checkSyntax(args as unknown as CheckSyntaxArgs);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
case "extract_source": {
const result = await extractSource(args as unknown as ExtractSourceArgs);
return {
content: [
{
type: "text",
text: result,
},
],
};
}
case "get_plantuml_version": {
const result = await getVersion();
return {
content: [
{
type: "text",
text: result,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("PlantUML MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});