Skip to main content
Glama

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
NameRequiredDescriptionDefault
codeYesCurrent FlowZap Code to modify
operationsYesArray of patch operations to apply

Implementation Reference

  • The main handler function handleApplyChange that validates input, orchestrates the applyChanges operation, creates playground URL, and returns JSON response with success/error status
    export 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)}`,
        });
      }
    }
  • 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 structure
    export 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,
      };
    }
  • Tool definition applyChangeTool with complete input schema defining the structure for code and operations parameters including validation for operation types and their properties
    export 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 handleApplyChange
    case "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 467
    const 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,
    ];

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flowzap-xyz/flowzap-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server