flowzap_apply_change
Apply structured modifications to FlowZap workflow diagrams by inserting, removing, or updating nodes and edges while preserving existing structure, avoiding full diagram regeneration.
Instructions
Apply a structured change to FlowZap Code (insert/remove/update nodes or edges). Safer than regenerating entire diagrams - preserves existing structure.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Current FlowZap Code to modify | |
| operations | Yes | Array of patch operations to apply |
Implementation Reference
- src/tools/diffAndApply.ts:525-571 (handler)The main handler function handleApplyChange that validates input, orchestrates the applyChanges operation, creates playground URL, and returns JSON response with success/error statusexport async function handleApplyChange(code: unknown, operations: unknown): Promise<string> { if (typeof code !== "string") { return JSON.stringify({ success: false, error: "code must be a string", }); } if (!Array.isArray(operations)) { return JSON.stringify({ success: false, error: "operations must be an array", }); } // Validate operations const validOps = ["insertNode", "removeNode", "updateNode", "insertEdge", "removeEdge"]; for (const op of operations) { if (typeof op !== "object" || !op || !validOps.includes(op.op)) { return JSON.stringify({ success: false, error: `Invalid operation: ${JSON.stringify(op)}. op must be one of: ${validOps.join(", ")}`, }); } } try { const result = applyChanges(code, operations as PatchOperation[]); // Auto-create playground URL const playground = await createPlaygroundUrl(result.code); return JSON.stringify({ success: true, code: result.code, url: playground.url || null, applied: result.applied, summary: `Applied ${result.applied.length} operation(s)`, ...(playground.error && { playgroundError: playground.error }), }, null, 2); } catch (error) { return JSON.stringify({ success: false, error: `Failed to apply changes: ${error instanceof Error ? error.message : String(error)}`, }); } }
- src/tools/diffAndApply.ts:243-474 (handler)Core implementation function applyChanges that parses the code structure and executes patch operations (insertNode, removeNode, updateNode, insertEdge, removeEdge) to modify FlowZap Code while preserving existing structureexport function applyChanges(code: string, operations: PatchOperation[]): { code: string; applied: string[] } { let lines = code.split("\n"); const applied: string[] = []; // Parse current structure to understand node positions const graph = parseToGraph(code); const nodeLines = new Map<string, number>(); // nodeId -> line number const laneRanges = new Map<string, { start: number; end: number }>(); // laneId -> line range // Find node and lane positions let currentLane: string | null = null; let laneStart = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); const laneMatch = line.match(/^(.+?)\s*\{/); if (laneMatch) { currentLane = laneMatch[1].trim(); laneStart = i; } if (line === "}" && currentLane) { laneRanges.set(currentLane, { start: laneStart, end: i }); currentLane = null; } const nodeMatch = line.match(/^(n\d+):/); if (nodeMatch) { nodeLines.set(nodeMatch[1], i); } } // Find highest node number let maxNodeNum = 0; for (const node of graph.nodes) { const match = node.id.match(/^n(\d+)$/); if (match) { maxNodeNum = Math.max(maxNodeNum, parseInt(match[1], 10)); } } // Apply operations for (const op of operations) { switch (op.op) { case "insertNode": { if (!op.newNode || !op.laneId) { applied.push(`insertNode: skipped (missing newNode or laneId)`); continue; } const newNodeId = `n${++maxNodeNum}`; const shape = op.newNode.shape || "rectangle"; let nodeLine = ` ${newNodeId}: ${shape}`; if (op.newNode.label) { nodeLine += ` label:"${escapeLabel(op.newNode.label)}"`; } if (op.newNode.properties) { for (const [key, value] of Object.entries(op.newNode.properties)) { nodeLine += ` ${key}:"${escapeLabel(value)}"`; } } // Find insertion point const laneRange = laneRanges.get(op.laneId); if (!laneRange) { applied.push(`insertNode: skipped (lane "${op.laneId}" not found)`); continue; } let insertAt = laneRange.end; // Default: before closing brace if (op.afterNodeId && nodeLines.has(op.afterNodeId)) { insertAt = nodeLines.get(op.afterNodeId)! + 1; } lines.splice(insertAt, 0, nodeLine); applied.push(`insertNode: added "${op.newNode.label || newNodeId}" as ${newNodeId} in ${op.laneId}`); // Update line numbers for subsequent operations for (const [nodeId, lineNum] of nodeLines) { if (lineNum >= insertAt) { nodeLines.set(nodeId, lineNum + 1); } } for (const [laneId, range] of laneRanges) { if (range.start >= insertAt) range.start++; if (range.end >= insertAt) range.end++; } nodeLines.set(newNodeId, insertAt); break; } case "removeNode": { if (!op.nodeId) { applied.push(`removeNode: skipped (missing nodeId)`); continue; } const lineNum = nodeLines.get(op.nodeId); if (lineNum === undefined) { applied.push(`removeNode: skipped (node "${op.nodeId}" not found)`); continue; } lines.splice(lineNum, 1); applied.push(`removeNode: removed ${op.nodeId}`); // Update line numbers for (const [nodeId, ln] of nodeLines) { if (ln > lineNum) { nodeLines.set(nodeId, ln - 1); } } nodeLines.delete(op.nodeId); break; } case "updateNode": { if (!op.nodeId || !op.updates) { applied.push(`updateNode: skipped (missing nodeId or updates)`); continue; } const lineNum = nodeLines.get(op.nodeId); if (lineNum === undefined) { applied.push(`updateNode: skipped (node "${op.nodeId}" not found)`); continue; } let line = lines[lineNum]; for (const [key, value] of Object.entries(op.updates)) { // Replace existing property or add new one const propRegex = new RegExp(`${key}\\s*[:=]\\s*"[^"]*"`); if (propRegex.test(line)) { line = line.replace(propRegex, `${key}:"${escapeLabel(value)}"`); } else { // Add before end of line line = line.trimEnd() + ` ${key}:"${escapeLabel(value)}"`; } } lines[lineNum] = line; applied.push(`updateNode: updated ${op.nodeId} with ${Object.keys(op.updates).join(", ")}`); break; } case "insertEdge": { if (!op.newEdge) { applied.push(`insertEdge: skipped (missing newEdge)`); continue; } const { from, to, label, fromHandle = "right", toHandle = "left" } = op.newEdge; // Find the lane of the source node const sourceNode = graph.nodes.find((n) => n.id === from); if (!sourceNode) { applied.push(`insertEdge: skipped (source node "${from}" not found)`); continue; } const laneRange = laneRanges.get(sourceNode.laneId); if (!laneRange) { applied.push(`insertEdge: skipped (lane not found)`); continue; } // Check if target is in different lane (cross-lane edge) const targetNode = graph.nodes.find((n) => n.id === to); let edgeLine: string; if (targetNode && targetNode.laneId !== sourceNode.laneId) { edgeLine = ` ${from}.handle(${fromHandle}) -> ${targetNode.laneId}.${to}.handle(${toHandle})`; } else { edgeLine = ` ${from}.handle(${fromHandle}) -> ${to}.handle(${toHandle})`; } if (label) { edgeLine += ` [label="${escapeLabel(label)}"]`; } // Insert before closing brace of the lane lines.splice(laneRange.end, 0, edgeLine); applied.push(`insertEdge: added ${from} -> ${to}${label ? ` [${label}]` : ""}`); // Update ranges for (const [laneId, range] of laneRanges) { if (range.end >= laneRange.end) range.end++; } break; } case "removeEdge": { if (!op.newEdge) { applied.push(`removeEdge: skipped (missing edge specification)`); continue; } const { from, to } = op.newEdge; // Find and remove the edge line const edgePattern = new RegExp(`^\\s*${from}\\.handle\\([^)]+\\)\\s*->\\s*(?:\\w+\\.)?${to}\\.handle`); let removed = false; for (let i = 0; i < lines.length; i++) { if (edgePattern.test(lines[i])) { lines.splice(i, 1); removed = true; applied.push(`removeEdge: removed ${from} -> ${to}`); break; } } if (!removed) { applied.push(`removeEdge: skipped (edge ${from} -> ${to} not found)`); } break; } default: applied.push(`Unknown operation: ${(op as any).op}`); } } return { code: lines.join("\n"), applied, }; }
- src/tools/diffAndApply.ts:65-130 (schema)Tool definition applyChangeTool with complete input schema defining the structure for code and operations parameters including validation for operation types and their propertiesexport const applyChangeTool: Tool = { name: "flowzap_apply_change", description: "Apply a structured change to FlowZap Code (insert/remove/update nodes or edges). Safer than regenerating entire diagrams - preserves existing structure.", inputSchema: { type: "object" as const, properties: { code: { type: "string", description: "Current FlowZap Code to modify", }, operations: { type: "array", description: "Array of patch operations to apply", items: { type: "object", properties: { op: { type: "string", enum: ["insertNode", "removeNode", "updateNode", "insertEdge", "removeEdge"], description: "Operation type", }, nodeId: { type: "string", description: "Node ID for remove/update operations", }, afterNodeId: { type: "string", description: "Insert new node after this node ID", }, laneId: { type: "string", description: "Lane ID for insert operations", }, newNode: { type: "object", description: "New node definition for insert operations", properties: { shape: { type: "string", enum: ["circle", "rectangle", "diamond", "taskbox"] }, label: { type: "string" }, properties: { type: "object" }, }, }, newEdge: { type: "object", description: "New edge definition for insertEdge", properties: { from: { type: "string" }, to: { type: "string" }, label: { type: "string" }, fromHandle: { type: "string" }, toHandle: { type: "string" }, }, }, updates: { type: "object", description: "Property updates for updateNode operation", }, }, required: ["op"], }, }, }, required: ["code", "operations"], }, };
- src/index.ts:508-512 (registration)Handler routing in the CallToolRequestSchema switch statement that extracts code and operations args and calls handleApplyChangecase "flowzap_apply_change": { const { code, operations } = args as { code?: unknown; operations?: unknown }; const result = await handleApplyChange(code, operations); return { content: [{ type: "text", text: result }] }; }
- src/index.ts:153-202 (registration)Tools array registration where applyChangeTool is imported from tools/index.ts and added to the list of available MCP tools, plus security whitelist at line 467const tools: Tool[] = [ { name: "flowzap_validate", description: "Validate FlowZap Code syntax. Use this to check if FlowZap Code is valid before creating a playground.", inputSchema: { type: "object" as const, properties: { code: { type: "string", description: "FlowZap Code to validate", }, }, required: ["code"], }, }, { name: "flowzap_create_playground", description: "Create a FlowZap playground session with the given code and return a shareable URL. Use this after generating FlowZap Code to give the user a visual diagram. Set view to 'architecture' when user requests an architecture diagram.", inputSchema: { type: "object" as const, properties: { code: { type: "string", description: "FlowZap Code to load in the playground", }, view: { type: "string", enum: ["workflow", "sequence", "architecture"], description: "View mode for the diagram. Use 'architecture' for architecture diagrams, 'sequence' for sequence diagrams, 'workflow' (default) for workflow diagrams.", }, }, required: ["code"], }, }, { name: "flowzap_get_syntax", description: "Get FlowZap Code syntax documentation and examples. Use this to learn how to write FlowZap Code for workflow diagrams.", inputSchema: { type: "object" as const, properties: {}, }, }, exportGraphTool, artifactToDiagramTool, diffTool, applyChangeTool, ];