2025-12-17 14:50:07 +09:00
#!/usr/bin/env node
/**
* MCP Server for Next AI Draw.io
*
* Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit
* draw.io diagrams with real-time browser preview.
2025-12-21 17:41:27 +09:00
*
* Uses an embedded HTTP server - no external dependencies required.
2025-12-17 14:50:07 +09:00
*/
// Setup DOM polyfill for Node.js (required for XML operations)
import { DOMParser } from "linkedom"
;( globalThis as any ). DOMParser = DOMParser
2025-12-21 17:41:27 +09:00
// Create XMLSerializer polyfill using outerHTML
2025-12-17 14:50:07 +09:00
class XMLSerializerPolyfill {
serializeToString ( node : any ) : string {
2025-12-21 17:41:27 +09:00
if ( node . outerHTML !== undefined ) {
return node . outerHTML
}
if ( node . documentElement ) {
return node . documentElement . outerHTML
}
2025-12-17 14:50:07 +09:00
return ""
}
}
;( globalThis as any ). XMLSerializer = XMLSerializerPolyfill
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import open from "open"
import { z } from "zod"
import {
applyDiagramOperations ,
type DiagramOperation ,
} from "./diagram-operations.js"
2025-12-21 16:11:49 +09:00
import { addHistory } from "./history.js"
2025-12-21 19:38:35 +09:00
import {
getState ,
requestSync ,
setState ,
2025-12-31 18:38:20 +09:00
shutdown ,
2025-12-21 19:38:35 +09:00
startHttpServer ,
waitForSync ,
} from "./http-server.js"
2025-12-17 14:50:07 +09:00
import { log } from "./logger.js"
2025-12-21 00:32:51 +09:00
import { validateAndFixXml } from "./xml-validation.js"
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
// Server configuration
const config = {
2026-01-01 14:45:46 +09:00
port : parseInt ( process . env . PORT || "6002" , 10 ),
2025-12-21 17:41:27 +09:00
}
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
// Session state (single session for simplicity)
let currentSession : {
id : string
xml : string
version : number
2025-12-21 19:38:35 +09:00
lastGetDiagramTime : number // Track when get_diagram was last called (for enforcing workflow)
2025-12-21 17:41:27 +09:00
} | null = null
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
// Create MCP server
const server = new McpServer ({
name : "next-ai-drawio" ,
version : "0.1.2" ,
})
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
// Register prompt with workflow guidance
2025-12-17 14:50:07 +09:00
server . prompt (
"diagram-workflow" ,
"Guidelines for creating and editing draw.io diagrams" ,
() => ({
messages : [
{
role : "user" ,
content : {
type : "text" ,
2025-12-21 16:58:41 +09:00
text : `# Draw.io Diagram Workflow Guidelines
2025-12-17 14:50:07 +09:00
## Creating a New Diagram
2025-12-21 16:58:41 +09:00
1. Call start_session to open the browser preview
2025-12-29 15:15:13 +09:00
2. Use create_new_diagram with complete mxGraphModel XML to create a new diagram
2025-12-17 14:50:07 +09:00
2025-12-21 16:58:41 +09:00
## Adding Elements to Existing Diagram
1. Use edit_diagram with "add" operation
2. Provide a unique cell_id and complete mxCell XML
3. No need to call get_diagram first - the server fetches latest state automatically
2025-12-17 14:50:07 +09:00
2025-12-21 16:58:41 +09:00
## Modifying or Deleting Existing Elements
1. FIRST call get_diagram to see current cell IDs and structure
2. THEN call edit_diagram with "update" or "delete" operations
3. For update, provide the cell_id and complete new mxCell XML
2025-12-17 14:50:07 +09:00
2025-12-21 16:58:41 +09:00
## Important Notes
2025-12-29 15:15:13 +09:00
- create_new_diagram REPLACES the entire diagram - only use for new diagrams
2025-12-21 16:58:41 +09:00
- edit_diagram PRESERVES user's manual changes (fetches browser state first)
- Always use unique cell_ids when adding elements (e.g., "shape-1", "arrow-2")` ,
2025-12-17 14:50:07 +09:00
},
},
],
}),
)
// Tool: start_session
server . registerTool (
"start_session" ,
{
description :
2025-12-21 16:58:41 +09:00
"Start a new diagram session and open the browser for real-time preview. " +
"Starts an embedded server and opens a browser window with draw.io. " +
"The browser will show diagram updates as they happen." ,
2025-12-17 14:50:07 +09:00
inputSchema : {},
},
async () => {
try {
2025-12-21 17:41:27 +09:00
// Start embedded HTTP server
2025-12-17 14:50:07 +09:00
const port = await startHttpServer ( config . port )
2025-12-21 17:41:27 +09:00
// Create session
const sessionId = `mcp- ${ Date . now (). toString ( 36 ) } - ${ Math . random (). toString ( 36 ). substring ( 2 , 8 ) } `
currentSession = {
id : sessionId ,
xml : "" ,
version : 0 ,
2025-12-21 19:38:35 +09:00
lastGetDiagramTime : 0 ,
2025-12-21 17:41:27 +09:00
}
// Open browser
2025-12-17 14:50:07 +09:00
const browserUrl = `http://localhost: ${ port } ?mcp= ${ sessionId } `
await open ( browserUrl )
log . info ( `Started session ${ sessionId } , browser at ${ browserUrl } ` )
return {
content : [
{
type : "text" ,
2025-12-21 16:58:41 +09:00
text : `Session started successfully!\n\nSession ID: ${ sessionId } \nBrowser URL: ${ browserUrl } \n\nThe browser will now show real-time diagram updates.` ,
2025-12-17 14:50:07 +09:00
},
],
}
} catch ( error ) {
const message =
error instanceof Error ? error . message : String ( error )
log . error ( "start_session failed:" , message )
return {
content : [{ type : "text" , text : `Error: ${ message } ` }],
isError : true ,
}
}
},
)
2025-12-29 15:15:13 +09:00
// Tool: create_new_diagram
2025-12-17 14:50:07 +09:00
server . registerTool (
2025-12-29 15:15:13 +09:00
"create_new_diagram" ,
2025-12-17 14:50:07 +09:00
{
2026-04-10 17:18:24 +09:00
description : `Create a NEW diagram from mxGraphModel XML. ONLY use this when creating a diagram from scratch.
⚠️ DO NOT use this tool to modify an existing diagram — it will DESTROY all existing content and user changes. Use edit_diagram instead for ANY modifications to an existing diagram.
2025-12-29 15:15:13 +09:00
CRITICAL: You MUST provide the 'xml' argument in EVERY call. Do NOT call this tool without xml.
When to use this tool:
2026-04-10 17:18:24 +09:00
- Creating a new diagram from scratch (no existing diagram)
- The user explicitly asks to "start over" or "create a new diagram"
2025-12-29 15:15:13 +09:00
2026-04-10 17:18:24 +09:00
When to use edit_diagram instead (ALWAYS prefer edit_diagram if a diagram already exists):
- ANY modifications to an existing diagram
- Adding/removing/moving elements
- Changing labels, colors, styles, or positions
- Restructuring or reorganizing existing content
- Adding new elements to an existing diagram
2025-12-29 15:15:13 +09:00
XML FORMAT - Full mxGraphModel structure:
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Shape" style="rounded=1;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
LAYOUT CONSTRAINTS:
- Keep all elements within x=0-800, y=0-600 (single page viewport)
- Start from margins (x=40, y=40), keep elements grouped closely
- Use unique IDs starting from "2" (0 and 1 are reserved)
- Set parent="1" for top-level shapes
- Space shapes 150-200px apart for clear edge routing
EDGE ROUTING RULES:
- Never let multiple edges share the same path - use different exitY/entryY values
- For bidirectional connections (A↔B), use OPPOSITE sides
- Always specify exitX, exitY, entryX, entryY explicitly in edge style
- Route edges AROUND obstacles using waypoints (add 20-30px clearance)
- Use natural connection points based on flow (not corners)
COMMON STYLES:
- Shapes: rounded=1; fillColor=#hex; strokeColor=#hex
- Edges: endArrow=classic; edgeStyle=orthogonalEdgeStyle; curved=1
- Text: fontSize=14; fontStyle=1 (bold); align=center` ,
2025-12-17 14:50:07 +09:00
inputSchema : {
xml : z
. string ()
2025-12-29 15:15:13 +09:00
. describe (
"REQUIRED: The complete mxGraphModel XML. Must always be provided." ,
),
2025-12-17 14:50:07 +09:00
},
},
2025-12-21 00:32:51 +09:00
async ({ xml : inputXml }) => {
2025-12-17 14:50:07 +09:00
try {
if ( ! currentSession ) {
return {
content : [
{
type : "text" ,
2025-12-21 16:58:41 +09:00
text : "Error: No active session. Please call start_session first." ,
2025-12-17 14:50:07 +09:00
},
],
isError : true ,
}
}
2025-12-21 17:41:27 +09:00
// Validate and auto-fix XML
2025-12-21 00:32:51 +09:00
let xml = inputXml
const { valid , error , fixed , fixes } = validateAndFixXml ( xml )
if ( fixed ) {
xml = fixed
log . info ( `XML auto-fixed: ${ fixes . join ( ", " ) } ` )
}
if ( ! valid && error ) {
2025-12-21 17:41:27 +09:00
log . error ( `XML validation failed: ${ error } ` )
2025-12-21 00:32:51 +09:00
return {
content : [
{
type : "text" ,
text : `Error: XML validation failed - ${ error } ` ,
},
],
isError : true ,
}
}
2025-12-29 15:15:13 +09:00
log . info ( `Setting diagram content, ${ xml . length } chars` )
2025-12-21 17:41:27 +09:00
2025-12-21 16:58:41 +09:00
// Sync from browser state first
const browserState = getState ( currentSession . id )
if ( browserState ? . xml ) {
currentSession . xml = browserState . xml
}
// Save user's state before AI overwrites (with cached SVG)
2025-12-21 16:09:14 +09:00
if ( currentSession . xml ) {
2025-12-21 16:58:41 +09:00
addHistory (
currentSession . id ,
currentSession . xml ,
browserState ? . svg || "" ,
)
2025-12-21 16:09:14 +09:00
}
2025-12-21 17:41:27 +09:00
// Update session state
2025-12-17 14:50:07 +09:00
currentSession . xml = xml
2025-12-21 17:41:27 +09:00
currentSession . version ++
2026-01-16 21:45:46 +09:00
currentSession . lastGetDiagramTime = Date . now ()
2025-12-21 17:41:27 +09:00
// Push to embedded server state
2025-12-17 14:50:07 +09:00
setState ( currentSession . id , xml )
2025-12-21 17:41:27 +09:00
// Save AI result (no SVG yet - will be captured by browser)
2025-12-21 16:58:41 +09:00
addHistory ( currentSession . id , xml , "" )
2025-12-21 16:09:14 +09:00
2025-12-29 15:15:13 +09:00
log . info ( `Diagram content set successfully` )
2025-12-17 14:50:07 +09:00
return {
content : [
{
type : "text" ,
2025-12-29 15:15:13 +09:00
text : `Diagram content set successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${ xml . length } characters` ,
2025-12-17 14:50:07 +09:00
},
],
}
} catch ( error ) {
const message =
error instanceof Error ? error . message : String ( error )
2025-12-29 15:15:13 +09:00
log . error ( "create_new_diagram failed:" , message )
2025-12-17 14:50:07 +09:00
return {
content : [{ type : "text" , text : `Error: ${ message } ` }],
isError : true ,
}
}
},
)
// Tool: edit_diagram
server . registerTool (
"edit_diagram" ,
{
description :
2025-12-21 19:38:35 +09:00
"Edit the current diagram by ID-based operations (update/add/delete cells).\n\n" +
"⚠️ REQUIRED: You MUST call get_diagram BEFORE this tool!\n" +
"This fetches the latest state from the browser including any manual user edits.\n" +
"Skipping get_diagram WILL cause user's changes to be LOST.\n\n" +
"Workflow:\n" +
"1. Call get_diagram to see current cell IDs and structure\n" +
"2. Use the returned XML to construct your edit operations\n" +
"3. Call edit_diagram with your operations\n\n" +
2025-12-17 14:50:07 +09:00
"Operations:\n" +
2025-12-21 17:41:27 +09:00
"- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" +
"- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
"- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
2025-12-25 13:19:04 +09:00
"For add/update, new_xml must be a complete mxCell element including mxGeometry.\n\n" +
"Example - Add a rectangle:\n" +
'{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}\n\n' +
"Example - Update a cell:\n" +
'{"operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"New Label\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}\n\n' +
"Example - Delete a cell:\n" +
'{"operations": [{"operation": "delete", "cell_id": "rect-1"}]}' ,
2025-12-17 14:50:07 +09:00
inputSchema : {
operations : z
. array (
z . object ({
2025-12-25 13:19:04 +09:00
operation : z
2025-12-21 17:41:27 +09:00
. enum ([ "update" , "add" , "delete" ])
2025-12-25 13:19:04 +09:00
. describe (
"Operation to perform: add, update, or delete" ,
),
2025-12-21 17:41:27 +09:00
cell_id : z . string (). describe ( "The id of the mxCell" ),
new_xml : z
. string ()
. optional ()
. describe (
"Complete mxCell XML element (required for update/add)" ,
),
2025-12-17 14:50:07 +09:00
}),
)
2025-12-21 17:41:27 +09:00
. describe ( "Array of operations to apply" ),
2025-12-17 14:50:07 +09:00
},
},
async ({ operations }) => {
try {
if ( ! currentSession ) {
return {
content : [
{
type : "text" ,
2025-12-21 16:58:41 +09:00
text : "Error: No active session. Please call start_session first." ,
2025-12-17 14:50:07 +09:00
},
],
isError : true ,
}
}
2025-12-21 19:38:35 +09:00
// Enforce workflow: require get_diagram to be called first
const timeSinceGet = Date . now () - currentSession . lastGetDiagramTime
if ( timeSinceGet > 30000 ) {
// 30 seconds
log . warn (
"edit_diagram called without recent get_diagram - rejecting to prevent data loss" ,
)
return {
content : [
{
type : "text" ,
text :
"Error: You must call get_diagram first before edit_diagram.\n\n" +
"This ensures you have the latest diagram state including any manual edits the user made in the browser. " +
"Please call get_diagram, then use that XML to construct your edit operations." ,
},
],
isError : true ,
}
}
2025-12-21 17:41:27 +09:00
// Fetch latest state from browser
2025-12-17 14:50:07 +09:00
const browserState = getState ( currentSession . id )
if ( browserState ? . xml ) {
currentSession . xml = browserState . xml
2025-12-21 17:41:27 +09:00
log . info ( "Fetched latest diagram state from browser" )
2025-12-17 14:50:07 +09:00
}
if ( ! currentSession . xml ) {
return {
content : [
{
type : "text" ,
2025-12-29 15:15:13 +09:00
text : "Error: No diagram to edit. Please create a diagram first with create_new_diagram." ,
2025-12-17 14:50:07 +09:00
},
],
isError : true ,
}
}
2025-12-21 17:41:27 +09:00
log . info ( `Editing diagram with ${ operations . length } operation(s)` )
2025-12-21 16:58:41 +09:00
// Save before editing (with cached SVG from browser)
addHistory (
currentSession . id ,
currentSession . xml ,
browserState ? . svg || "" ,
)
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
// Validate and auto-fix new_xml for each operation
2025-12-21 00:32:51 +09:00
const validatedOps = operations . map (( op ) => {
if ( op . new_xml ) {
2025-12-21 17:41:27 +09:00
const { valid , error , fixed , fixes } = validateAndFixXml (
op . new_xml ,
)
2025-12-21 00:32:51 +09:00
if ( fixed ) {
log . info (
2025-12-25 13:19:04 +09:00
`Operation ${ op . operation } ${ op . cell_id } : XML auto-fixed: ${ fixes . join ( ", " ) } ` ,
2025-12-21 00:32:51 +09:00
)
return { ... op , new_xml : fixed }
}
2025-12-21 17:41:27 +09:00
if ( ! valid && error ) {
log . warn (
2025-12-25 13:19:04 +09:00
`Operation ${ op . operation } ${ op . cell_id } : XML validation failed: ${ error } ` ,
2025-12-21 17:41:27 +09:00
)
}
2025-12-21 00:32:51 +09:00
}
return op
})
2025-12-21 17:41:27 +09:00
// Apply operations
2025-12-17 14:50:07 +09:00
const { result , errors } = applyDiagramOperations (
currentSession . xml ,
2025-12-21 00:32:51 +09:00
validatedOps as DiagramOperation [],
2025-12-17 14:50:07 +09:00
)
2025-12-21 17:41:27 +09:00
if ( errors . length > 0 ) {
const errorMessages = errors
. map (( e ) => ` ${ e . type } ${ e . cellId } : ${ e . message } ` )
. join ( "\n" )
log . warn ( `Edit had ${ errors . length } error(s): ${ errorMessages } ` )
}
// Update state
2025-12-17 14:50:07 +09:00
currentSession . xml = result
2025-12-21 17:41:27 +09:00
currentSession . version ++
// Push to embedded server
2025-12-17 14:50:07 +09:00
setState ( currentSession . id , result )
2025-12-21 17:41:27 +09:00
// Save AI result (no SVG yet - will be captured by browser)
2025-12-21 16:58:41 +09:00
addHistory ( currentSession . id , result , "" )
2025-12-21 16:09:14 +09:00
2025-12-21 17:41:27 +09:00
log . info ( `Diagram edited successfully` )
2025-12-17 14:50:07 +09:00
2025-12-21 17:41:27 +09:00
const successMsg = `Diagram edited successfully!\n\nApplied ${ operations . length } operation(s).`
const errorMsg =
2025-12-17 14:50:07 +09:00
errors . length > 0
2025-12-21 17:41:27 +09:00
? `\n\nWarnings:\n ${ errors . map (( e ) => `- ${ e . type } ${ e . cellId } : ${ e . message } ` ). join ( "\n" ) } `
2025-12-17 14:50:07 +09:00
: ""
2025-12-21 17:41:27 +09:00
return {
content : [
{
type : "text" ,
text : successMsg + errorMsg ,
},
],
}
2025-12-17 14:50:07 +09:00
} catch ( error ) {
const message =
error instanceof Error ? error . message : String ( error )
log . error ( "edit_diagram failed:" , message )
return {
content : [{ type : "text" , text : `Error: ${ message } ` }],
isError : true ,
}
}
},
)
// Tool: get_diagram
server . registerTool (
"get_diagram" ,
{
description :
2025-12-21 17:41:27 +09:00
"Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
"Call this BEFORE edit_diagram if you need to update or delete existing elements, " +
"so you can see the current cell IDs and structure." ,
2025-12-17 14:50:07 +09:00
},
async () => {
try {
if ( ! currentSession ) {
return {
content : [
{
type : "text" ,
2025-12-21 16:58:41 +09:00
text : "Error: No active session. Please call start_session first." ,
2025-12-17 14:50:07 +09:00
},
],
isError : true ,
}
}
2025-12-21 19:38:35 +09:00
// Request browser to push fresh state and wait for it
const syncRequested = requestSync ( currentSession . id )
if ( syncRequested ) {
const synced = await waitForSync ( currentSession . id )
if ( ! synced ) {
log . warn ( "get_diagram: sync timeout - state may be stale" )
}
}
// Mark that get_diagram was called (for edit_diagram workflow check)
currentSession . lastGetDiagramTime = Date . now ()
2025-12-21 17:41:27 +09:00
// Fetch latest state from browser
2025-12-17 14:50:07 +09:00
const browserState = getState ( currentSession . id )
if ( browserState ? . xml ) {
currentSession . xml = browserState . xml
}
if ( ! currentSession . xml ) {
return {
content : [
{
type : "text" ,
2025-12-29 15:15:13 +09:00
text : "No diagram exists yet. Use create_new_diagram to create one." ,
2025-12-17 14:50:07 +09:00
},
],
}
}
return {
content : [
{
type : "text" ,
2025-12-21 17:41:27 +09:00
text : `Current diagram XML:\n\n ${ currentSession . xml } ` ,
2025-12-17 14:50:07 +09:00
},
],
}
} catch ( error ) {
const message =
error instanceof Error ? error . message : String ( error )
2025-12-21 17:41:27 +09:00
log . error ( "get_diagram failed:" , message )
2025-12-17 14:50:07 +09:00
return {
content : [{ type : "text" , text : `Error: ${ message } ` }],
isError : true ,
}
}
},
)
// Tool: export_diagram
server . registerTool (
"export_diagram" ,
{
2026-02-07 12:55:09 +09:00
description :
"Export the current diagram to a file. Supports .drawio (XML), .png, and .svg formats. " +
"The format is auto-detected from the file extension, or can be specified explicitly." ,
2025-12-17 14:50:07 +09:00
inputSchema : {
2025-12-21 17:41:27 +09:00
path : z
. string ()
. describe (
2026-02-07 12:55:09 +09:00
"File path to save the diagram (e.g., ./diagram.drawio, ./diagram.png, ./diagram.svg)" ,
),
format : z
. enum ([ "drawio" , "png" , "svg" ])
. optional ()
. describe (
"Export format. If omitted, detected from file extension. Defaults to drawio." ,
2025-12-21 17:41:27 +09:00
),
2025-12-17 14:50:07 +09:00
},
},
2026-02-07 12:55:09 +09:00
async ({ path , format }) => {
2025-12-17 14:50:07 +09:00
try {
if ( ! currentSession ) {
return {
content : [
{
type : "text" ,
2025-12-21 16:58:41 +09:00
text : "Error: No active session. Please call start_session first." ,
2025-12-17 14:50:07 +09:00
},
],
isError : true ,
}
}
2025-12-21 17:41:27 +09:00
// Fetch latest state
2025-12-17 14:50:07 +09:00
const browserState = getState ( currentSession . id )
if ( browserState ? . xml ) {
currentSession . xml = browserState . xml
}
if ( ! currentSession . xml ) {
return {
content : [
2025-12-21 17:41:27 +09:00
{
type : "text" ,
text : "Error: No diagram to export. Please create a diagram first." ,
},
2025-12-17 14:50:07 +09:00
],
isError : true ,
}
}
const fs = await import ( "node:fs/promises" )
const nodePath = await import ( "node:path" )
2026-02-07 12:55:09 +09:00
// Detect format from extension if not specified
const ext = nodePath . extname ( path ). toLowerCase ()
const detectedFormat =
format ||
( ext === ".png" ? "png" : ext === ".svg" ? "svg" : "drawio" )
// Original .drawio export path (unchanged logic)
if ( detectedFormat === "drawio" ) {
let filePath = path
if ( ! filePath . endsWith ( ".drawio" )) {
filePath = ` ${ filePath } .drawio`
}
const absolutePath = nodePath . resolve ( filePath )
await fs . writeFile ( absolutePath , currentSession . xml , "utf-8" )
log . info ( `Diagram exported to ${ absolutePath } ` )
return {
content : [
{
type : "text" ,
text : `Diagram exported successfully!\n\nFile: ${ absolutePath } \nSize: ${ currentSession . xml . length } characters` ,
},
],
}
2025-12-21 17:41:27 +09:00
}
2025-12-17 14:50:07 +09:00
2026-02-07 12:55:09 +09:00
// PNG or SVG: request browser to export via iframe
let filePath = path
if ( ext !== `. ${ detectedFormat } ` ) {
if ( ext === ".drawio" || ext === ".png" || ext === ".svg" ) {
filePath = filePath . slice ( 0 , - ext . length )
}
filePath = ` ${ filePath } . ${ detectedFormat } `
}
2025-12-17 14:50:07 +09:00
const absolutePath = nodePath . resolve ( filePath )
2026-02-07 12:55:09 +09:00
const state = getState ( currentSession . id )
if ( ! state ) {
return {
content : [
{
type : "text" ,
text : "Error: Session state not found. Is the browser open?" ,
},
],
isError : true ,
}
}
state . exportFormat = detectedFormat as "png" | "svg"
state . exportData = undefined
2025-12-17 14:50:07 +09:00
2026-02-07 12:55:09 +09:00
// Wait for browser to produce the export data
const timeoutMs = 10000
const start = Date . now ()
while ( Date . now () - start < timeoutMs ) {
if ( state . exportData ) break
await new Promise (( r ) => setTimeout ( r , 200 ))
}
const exportData = state . exportData as string | undefined
state . exportData = undefined
state . exportFormat = undefined
if ( ! exportData ) {
return {
content : [
{
type : "text" ,
text : "Error: Export timed out. Make sure the browser tab is open and the diagram is loaded." ,
},
],
isError : true ,
}
}
// Decode and write
if ( detectedFormat === "png" ) {
const base64 = exportData . replace (
/^data:image\/png;base64,/ ,
"" ,
)
await fs . writeFile ( absolutePath , Buffer . from ( base64 , "base64" ))
} else {
let svgContent = exportData
if ( svgContent . startsWith ( "data:image/svg+xml;base64," )) {
const base64 = svgContent . replace (
/^data:image\/svg\+xml;base64,/ ,
"" ,
)
svgContent = Buffer . from ( base64 , "base64" ). toString ( "utf-8" )
}
await fs . writeFile ( absolutePath , svgContent , "utf-8" )
}
const stat = await fs . stat ( absolutePath )
log . info (
`Diagram exported to ${ absolutePath } ( ${ detectedFormat } , ${ stat . size } bytes)` ,
)
2025-12-17 14:50:07 +09:00
return {
content : [
{
type : "text" ,
2026-02-07 12:55:09 +09:00
text : `Diagram exported successfully!\n\nFile: ${ absolutePath } \nFormat: ${ detectedFormat } \nSize: ${ stat . size } bytes` ,
2025-12-17 14:50:07 +09:00
},
],
}
} catch ( error ) {
const message =
error instanceof Error ? error . message : String ( error )
2025-12-21 17:41:27 +09:00
log . error ( "export_diagram failed:" , message )
2025-12-17 14:50:07 +09:00
return {
content : [{ type : "text" , text : `Error: ${ message } ` }],
isError : true ,
}
}
},
)
2025-12-31 18:38:20 +09:00
// Graceful shutdown handler
let isShuttingDown = false
function gracefulShutdown ( reason : string ) {
if ( isShuttingDown ) return
isShuttingDown = true
log . info ( `Shutting down: ${ reason } ` )
shutdown ()
process . exit ( 0 )
}
// Handle stdin close (primary method - works on all platforms including Windows)
process . stdin . on ( "close" , () => gracefulShutdown ( "stdin closed" ))
process . stdin . on ( "end" , () => gracefulShutdown ( "stdin ended" ))
// Handle signals (may not work reliably on Windows)
process . on ( "SIGINT" , () => gracefulShutdown ( "SIGINT" ))
process . on ( "SIGTERM" , () => gracefulShutdown ( "SIGTERM" ))
// Handle broken pipe (writing to closed stdout)
process . stdout . on ( "error" , ( err ) => {
if ( err . code === "EPIPE" || err . code === "ERR_STREAM_DESTROYED" ) {
gracefulShutdown ( "stdout error" )
}
})
2025-12-21 17:41:27 +09:00
// Start the MCP server
2025-12-17 14:50:07 +09:00
async function main () {
2025-12-21 17:41:27 +09:00
log . info ( "Starting MCP server for Next AI Draw.io (embedded mode)..." )
2025-12-17 14:50:07 +09:00
const transport = new StdioServerTransport ()
await server . connect ( transport )
2025-12-21 17:41:27 +09:00
log . info ( "MCP server running on stdio" )
2025-12-17 14:50:07 +09:00
}
main (). catch (( error ) => {
2025-12-21 17:41:27 +09:00
log . error ( "Fatal error:" , error )
2025-12-17 14:50:07 +09:00
process . exit ( 1 )
})