fix(mcp): minimal history integration in index.ts

Keep only essential history integration:
- Import addHistory from history.js
- Remove unused getServerPort import
- Add browser state sync and history saving in display_diagram
- Add history saving in edit_diagram

No changes to prompts, descriptions, or code style.
This commit is contained in:
dayuan.jiang
2025-12-21 17:41:27 +09:00
parent 864375b8e4
commit b97f3ccda9

View File

@@ -4,16 +4,23 @@
* *
* Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit * Enables AI agents (Claude Desktop, Cursor, etc.) to generate and edit
* draw.io diagrams with real-time browser preview. * draw.io diagrams with real-time browser preview.
*
* Uses an embedded HTTP server - no external dependencies required.
*/ */
// Setup DOM polyfill for Node.js (required for XML operations) // Setup DOM polyfill for Node.js (required for XML operations)
import { DOMParser } from "linkedom" import { DOMParser } from "linkedom"
;(globalThis as any).DOMParser = DOMParser ;(globalThis as any).DOMParser = DOMParser
// Create XMLSerializer polyfill using outerHTML
class XMLSerializerPolyfill { class XMLSerializerPolyfill {
serializeToString(node: any): string { serializeToString(node: any): string {
if (node.outerHTML !== undefined) return node.outerHTML if (node.outerHTML !== undefined) {
if (node.documentElement) return node.documentElement.outerHTML return node.outerHTML
}
if (node.documentElement) {
return node.documentElement.outerHTML
}
return "" return ""
} }
} }
@@ -32,13 +39,25 @@ import { getState, setState, startHttpServer } from "./http-server.js"
import { log } from "./logger.js" import { log } from "./logger.js"
import { validateAndFixXml } from "./xml-validation.js" import { validateAndFixXml } from "./xml-validation.js"
const config = { port: parseInt(process.env.PORT || "6002") } // Server configuration
const config = {
port: parseInt(process.env.PORT || "6002"),
}
let currentSession: { id: string; xml: string } | null = null // Session state (single session for simplicity)
let currentSession: {
id: string
xml: string
version: number
} | null = null
const server = new McpServer({ name: "next-ai-drawio", version: "0.1.2" }) // Create MCP server
const server = new McpServer({
name: "next-ai-drawio",
version: "0.1.2",
})
// Workflow guidance prompt // Register prompt with workflow guidance
server.prompt( server.prompt(
"diagram-workflow", "diagram-workflow",
"Guidelines for creating and editing draw.io diagrams", "Guidelines for creating and editing draw.io diagrams",
@@ -86,10 +105,18 @@ server.registerTool(
}, },
async () => { async () => {
try { try {
// Start embedded HTTP server
const port = await startHttpServer(config.port) const port = await startHttpServer(config.port)
const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`
currentSession = { id: sessionId, xml: "" }
// Create session
const sessionId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`
currentSession = {
id: sessionId,
xml: "",
version: 0,
}
// Open browser
const browserUrl = `http://localhost:${port}?mcp=${sessionId}` const browserUrl = `http://localhost:${port}?mcp=${sessionId}`
await open(browserUrl) await open(browserUrl)
@@ -144,6 +171,7 @@ server.registerTool(
} }
} }
// Validate and auto-fix XML
let xml = inputXml let xml = inputXml
const { valid, error, fixed, fixes } = validateAndFixXml(xml) const { valid, error, fixed, fixes } = validateAndFixXml(xml)
if (fixed) { if (fixed) {
@@ -151,6 +179,7 @@ server.registerTool(
log.info(`XML auto-fixed: ${fixes.join(", ")}`) log.info(`XML auto-fixed: ${fixes.join(", ")}`)
} }
if (!valid && error) { if (!valid && error) {
log.error(`XML validation failed: ${error}`)
return { return {
content: [ content: [
{ {
@@ -162,6 +191,8 @@ server.registerTool(
} }
} }
log.info(`Displaying diagram, ${xml.length} chars`)
// Sync from browser state first // Sync from browser state first
const browserState = getState(currentSession.id) const browserState = getState(currentSession.id)
if (browserState?.xml) { if (browserState?.xml) {
@@ -177,19 +208,23 @@ server.registerTool(
) )
} }
// Update session state
currentSession.xml = xml currentSession.xml = xml
currentSession.version++
// Push to embedded server state
setState(currentSession.id, xml) setState(currentSession.id, xml)
// Save AI result (no SVG yet) // Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, xml, "") addHistory(currentSession.id, xml, "")
log.info(`Displayed diagram, ${xml.length} chars`) log.info(`Diagram displayed successfully`)
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: `Diagram displayed! (${xml.length} chars)`, text: `Diagram displayed successfully!\n\nThe diagram is now visible in your browser.\n\nXML length: ${xml.length} characters`,
}, },
], ],
} }
@@ -210,22 +245,33 @@ server.registerTool(
"edit_diagram", "edit_diagram",
{ {
description: description:
"Edit diagram by operations (update/add/delete cells). " + "Edit the current diagram by ID-based operations (update/add/delete cells). " +
"Fetches latest browser state first, preserving user's changes.\n\n" + "ALWAYS fetches the latest state from browser first, so user's manual changes are preserved.\n\n" +
"IMPORTANT workflow:\n" +
"- For ADD operations: Can use directly - just provide new unique cell_id and new_xml.\n" +
"- For UPDATE/DELETE: Call get_diagram FIRST to see current cell IDs, then edit.\n\n" +
"Operations:\n" + "Operations:\n" +
"- add: Add new cell (cell_id + new_xml)\n" + "- add: Add a new cell. Provide cell_id (new unique id) and new_xml.\n" +
"- update: Replace cell (cell_id + new_xml)\n" + "- update: Replace an existing cell by its id. Provide cell_id and complete new_xml.\n" +
"- delete: Remove cell (cell_id only)", "- delete: Remove a cell by its id. Only cell_id is needed.\n\n" +
"For add/update, new_xml must be a complete mxCell element including mxGeometry.",
inputSchema: { inputSchema: {
operations: z operations: z
.array( .array(
z.object({ z.object({
type: z.enum(["update", "add", "delete"]), type: z
cell_id: z.string(), .enum(["update", "add", "delete"])
new_xml: z.string().optional(), .describe("Operation type"),
cell_id: z.string().describe("The id of the mxCell"),
new_xml: z
.string()
.optional()
.describe(
"Complete mxCell XML element (required for update/add)",
),
}), }),
) )
.describe("Operations to apply"), .describe("Array of operations to apply"),
}, },
}, },
async ({ operations }) => { async ({ operations }) => {
@@ -242,10 +288,11 @@ server.registerTool(
} }
} }
// Fetch latest from browser // Fetch latest state from browser
const browserState = getState(currentSession.id) const browserState = getState(currentSession.id)
if (browserState?.xml) { if (browserState?.xml) {
currentSession.xml = browserState.xml currentSession.xml = browserState.xml
log.info("Fetched latest diagram state from browser")
} }
if (!currentSession.xml) { if (!currentSession.xml) {
@@ -253,13 +300,15 @@ server.registerTool(
content: [ content: [
{ {
type: "text", type: "text",
text: "Error: No diagram to edit. Use display_diagram first.", text: "Error: No diagram to edit. Please create a diagram first with display_diagram.",
}, },
], ],
isError: true, isError: true,
} }
} }
log.info(`Editing diagram with ${operations.length} operation(s)`)
// Save before editing (with cached SVG from browser) // Save before editing (with cached SVG from browser)
addHistory( addHistory(
currentSession.id, currentSession.id,
@@ -267,40 +316,66 @@ server.registerTool(
browserState?.svg || "", browserState?.svg || "",
) )
// Validate operations // Validate and auto-fix new_xml for each operation
const validatedOps = operations.map((op) => { const validatedOps = operations.map((op) => {
if (op.new_xml) { if (op.new_xml) {
const { fixed, fixes } = validateAndFixXml(op.new_xml) const { valid, error, fixed, fixes } = validateAndFixXml(
op.new_xml,
)
if (fixed) { if (fixed) {
log.info( log.info(
`${op.type} ${op.cell_id}: auto-fixed: ${fixes.join(", ")}`, `Operation ${op.type} ${op.cell_id}: XML auto-fixed: ${fixes.join(", ")}`,
) )
return { ...op, new_xml: fixed } return { ...op, new_xml: fixed }
} }
if (!valid && error) {
log.warn(
`Operation ${op.type} ${op.cell_id}: XML validation failed: ${error}`,
)
}
} }
return op return op
}) })
// Apply operations
const { result, errors } = applyDiagramOperations( const { result, errors } = applyDiagramOperations(
currentSession.xml, currentSession.xml,
validatedOps as DiagramOperation[], validatedOps as DiagramOperation[],
) )
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
currentSession.xml = result currentSession.xml = result
currentSession.version++
// Push to embedded server
setState(currentSession.id, result) setState(currentSession.id, result)
// Save AI result (no SVG yet) // Save AI result (no SVG yet - will be captured by browser)
addHistory(currentSession.id, result, "") addHistory(currentSession.id, result, "")
log.info(`Edited diagram: ${operations.length} operation(s)`) log.info(`Diagram edited successfully`)
const msg = `Applied ${operations.length} operation(s).` const successMsg = `Diagram edited successfully!\n\nApplied ${operations.length} operation(s).`
const warn = const errorMsg =
errors.length > 0 errors.length > 0
? `\nWarnings: ${errors.map((e) => `${e.type} ${e.cellId}: ${e.message}`).join(", ")}` ? `\n\nWarnings:\n${errors.map((e) => `- ${e.type} ${e.cellId}: ${e.message}`).join("\n")}`
: "" : ""
return { content: [{ type: "text", text: msg + warn }] } return {
content: [
{
type: "text",
text: successMsg + errorMsg,
},
],
}
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
@@ -318,8 +393,9 @@ server.registerTool(
"get_diagram", "get_diagram",
{ {
description: description:
"Get current diagram XML (fetches latest from browser). " + "Get the current diagram XML (fetches latest from browser, including user's manual edits). " +
"Call before edit_diagram to see cell IDs.", "Call this BEFORE edit_diagram if you need to update or delete existing elements, " +
"so you can see the current cell IDs and structure.",
}, },
async () => { async () => {
try { try {
@@ -335,6 +411,7 @@ server.registerTool(
} }
} }
// Fetch latest state from browser
const browserState = getState(currentSession.id) const browserState = getState(currentSession.id)
if (browserState?.xml) { if (browserState?.xml) {
currentSession.xml = browserState.xml currentSession.xml = browserState.xml
@@ -345,7 +422,7 @@ server.registerTool(
content: [ content: [
{ {
type: "text", type: "text",
text: "No diagram yet. Use display_diagram to create one.", text: "No diagram exists yet. Use display_diagram to create one.",
}, },
], ],
} }
@@ -355,13 +432,14 @@ server.registerTool(
content: [ content: [
{ {
type: "text", type: "text",
text: `Current diagram:\n\n${currentSession.xml}`, text: `Current diagram XML:\n\n${currentSession.xml}`,
}, },
], ],
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
log.error("get_diagram failed:", message)
return { return {
content: [{ type: "text", text: `Error: ${message}` }], content: [{ type: "text", text: `Error: ${message}` }],
isError: true, isError: true,
@@ -374,9 +452,13 @@ server.registerTool(
server.registerTool( server.registerTool(
"export_diagram", "export_diagram",
{ {
description: "Export diagram to .drawio file.", description: "Export the current diagram to a .drawio file.",
inputSchema: { inputSchema: {
path: z.string().describe("File path (e.g., ./diagram.drawio)"), path: z
.string()
.describe(
"File path to save the diagram (e.g., ./diagram.drawio)",
),
}, },
}, },
async ({ path }) => { async ({ path }) => {
@@ -393,6 +475,7 @@ server.registerTool(
} }
} }
// Fetch latest state
const browserState = getState(currentSession.id) const browserState = getState(currentSession.id)
if (browserState?.xml) { if (browserState?.xml) {
currentSession.xml = browserState.xml currentSession.xml = browserState.xml
@@ -401,7 +484,10 @@ server.registerTool(
if (!currentSession.xml) { if (!currentSession.xml) {
return { return {
content: [ content: [
{ type: "text", text: "Error: No diagram to export." }, {
type: "text",
text: "Error: No diagram to export. Please create a diagram first.",
},
], ],
isError: true, isError: true,
} }
@@ -411,24 +497,27 @@ server.registerTool(
const nodePath = await import("node:path") const nodePath = await import("node:path")
let filePath = path let filePath = path
if (!filePath.endsWith(".drawio")) filePath += ".drawio" if (!filePath.endsWith(".drawio")) {
filePath = `${filePath}.drawio`
}
const absolutePath = nodePath.resolve(filePath) const absolutePath = nodePath.resolve(filePath)
await fs.writeFile(absolutePath, currentSession.xml, "utf-8") await fs.writeFile(absolutePath, currentSession.xml, "utf-8")
log.info(`Exported to ${absolutePath}`) log.info(`Diagram exported to ${absolutePath}`)
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: `Exported to ${absolutePath}`, text: `Diagram exported successfully!\n\nFile: ${absolutePath}\nSize: ${currentSession.xml.length} characters`,
}, },
], ],
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
log.error("export_diagram failed:", message)
return { return {
content: [{ type: "text", text: `Error: ${message}` }], content: [{ type: "text", text: `Error: ${message}` }],
isError: true, isError: true,
@@ -437,15 +526,17 @@ server.registerTool(
}, },
) )
// Start server // Start the MCP server
async function main() { async function main() {
log.info("Starting MCP server...") log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")
const transport = new StdioServerTransport() const transport = new StdioServerTransport()
await server.connect(transport) await server.connect(transport)
log.info("MCP server running")
log.info("MCP server running on stdio")
} }
main().catch((error) => { main().catch((error) => {
log.error("Fatal:", error) log.error("Fatal error:", error)
process.exit(1) process.exit(1)
}) })