Skip to main content
Glama
jau123

MeiGen AI Image Generation MCP

comfyui_workflow

Destructive

List, view parameters, import from file, modify settings, or delete ComfyUI workflow templates.

Instructions

Manage ComfyUI workflow templates: list, view parameters, import from file, modify settings, or delete.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
actionYesAction to perform on ComfyUI workflows
nameNoWorkflow name. Required for view/modify/delete. For import, used as the save name (defaults to filename).
filePathNoPath to a ComfyUI API-format workflow JSON file (for import action).
nodeIdNoNode ID to modify (for modify action). Use "view" action first to see available node IDs.
inputNoInput field name to modify (for modify action). E.g. "steps", "cfg", "sampler_name", "ckpt_name".
valueNoNew value as JSON (for modify action). Examples: "30", "\"euler\"", "7.5", "true".

Implementation Reference

  • Registers the 'comfyui_workflow' tool with MCP server via server.tool(), routing to action handlers based on the 'action' parameter.
    export function registerComfyuiWorkflow(server: McpServer, config: MeiGenConfig) {
      server.tool(
        'comfyui_workflow',
        'Manage ComfyUI workflow templates: list, view parameters, import from file, modify settings, or delete.',
        comfyuiWorkflowSchema,
        { readOnlyHint: false, destructiveHint: true },
        async ({ action, name, filePath, nodeId, input, value }) => {
          try {
            switch (action) {
              case 'list':
                return handleList(config)
              case 'view':
                return handleView(name, config)
              case 'import':
                return handleImport(name, filePath)
              case 'modify':
                return handleModify(name, nodeId, input, value, config)
              case 'delete':
                return handleDelete(name)
              default:
                return errorResult(`Unknown action: ${action}`)
            }
          } catch (e) {
            return errorResult(e instanceof Error ? e.message : String(e))
          }
        }
      )
    }
  • Zod schema defining tool inputs: action (list/view/import/modify/delete), name, filePath, nodeId, input, and value.
    export const comfyuiWorkflowSchema = {
      action: z.enum(['list', 'view', 'import', 'modify', 'delete'])
        .describe('Action to perform on ComfyUI workflows'),
    
      name: z.string().optional()
        .describe('Workflow name. Required for view/modify/delete. For import, used as the save name (defaults to filename).'),
    
      filePath: z.string().optional()
        .describe('Path to a ComfyUI API-format workflow JSON file (for import action).'),
    
      nodeId: z.string().optional()
        .describe('Node ID to modify (for modify action). Use "view" action first to see available node IDs.'),
      input: z.string().optional()
        .describe('Input field name to modify (for modify action). E.g. "steps", "cfg", "sampler_name", "ckpt_name".'),
      value: z.string().optional()
        .describe('New value as JSON (for modify action). Examples: "30", "\\"euler\\"", "7.5", "true".'),
    }
  • Five action handlers: handleList (lists saved workflows with summaries), handleView (shows editable nodes), handleImport (reads/validates/saves JSON workflow), handleModify (sets node input values), handleDelete (removes workflow file).
    function handleList(config: MeiGenConfig) {
      const workflows = listWorkflows()
      if (workflows.length === 0) {
        return textResult(
          'No ComfyUI workflows saved.\n\n' +
          'To import a workflow:\n' +
          '1. Open ComfyUI in your browser\n' +
          '2. Load your preferred workflow\n' +
          '3. Enable Dev Mode (Settings → Enable Dev mode options)\n' +
          '4. Click "Save (API Format)"\n' +
          '5. Use: comfyui_workflow import with the file path'
        )
      }
    
      const defaultName = config.comfyuiDefaultWorkflow || workflows[0]
    
      const lines = workflows.map((wfName, i) => {
        try {
          const wf = loadWorkflow(wfName)
          const s = getWorkflowSummary(wf)
          const isDefault = wfName === defaultName ? ' (default)' : ''
          const ckpt = s.checkpoint || 'unknown model'
          const params = [
            s.steps != null ? `${s.steps} steps` : null,
            s.cfg != null ? `CFG ${s.cfg}` : null,
            s.sampler ? s.sampler : null,
            s.width && s.height ? `${s.width}×${s.height}` : null,
          ].filter(Boolean).join(', ')
    
          return `${i + 1}. ${wfName}${isDefault}\n   ${ckpt}\n   ${params || 'Unable to parse parameters'}\n   Nodes: ${s.nodeCount}`
        } catch {
          return `${i + 1}. ${wfName} (error reading workflow)`
        }
      })
    
      return textResult(`Saved ComfyUI Workflows:\n\n${lines.join('\n\n')}`)
    }
    
    function handleView(name: string | undefined, config: MeiGenConfig) {
      const resolvedName = resolveName(name, config)
      const wf = loadWorkflow(resolvedName)
      const details = getEditableNodes(wf)
    
      return textResult(`Workflow: ${resolvedName}\n\n## Editable Parameters\n\n${details}`)
    }
    
    function handleImport(name: string | undefined, filePath: string | undefined) {
      if (!filePath) {
        return errorResult('filePath is required for import action. Provide the path to your ComfyUI API-format workflow JSON.')
      }
    
      // Read file
      let content: string
      try {
        // Expand ~ to home directory
        const expandedPath = filePath.replace(/^~/, homedir())
        content = readFileSync(expandedPath, 'utf-8')
      } catch (e) {
        return errorResult(`Cannot read file "${filePath}": ${e instanceof Error ? e.message : String(e)}`)
      }
    
      // Parse JSON
      let workflow: ComfyUIWorkflow
      try {
        workflow = JSON.parse(content) as ComfyUIWorkflow
      } catch {
        return errorResult('Invalid JSON file. Please ensure this is a valid ComfyUI API-format workflow.')
      }
    
      // Basic validation: should be an object with node entries
      if (typeof workflow !== 'object' || Array.isArray(workflow) || Object.keys(workflow).length === 0) {
        return errorResult('Invalid workflow format. Expected an object with node IDs as keys.')
      }
    
      // Best-effort detection (never fails)
      const nodeMap = detectNodes(workflow)
    
      // Determine save name
      const saveName = name || filePath.replace(/^.*[\\/]/, '').replace(/\.json$/i, '') || 'workflow'
    
      // Check if already exists
      const isOverwrite = workflowExists(saveName)
    
      // Save
      saveWorkflow(saveName, workflow)
    
      // Build confirmation
      const summary = getWorkflowSummary(workflow)
      const lines = [
        `Workflow "${saveName}" ${isOverwrite ? 'updated' : 'imported'} successfully!`,
        '',
        'Auto-detected:',
        nodeMap.positivePrompt ? `  Prompt injection: Node #${nodeMap.positivePrompt}` : '  Prompt injection: not detected — use "view" to find the text node and "modify" to set prompt before generating',
        nodeMap.loadImages?.length ? `  Reference images: Node #${nodeMap.loadImages.join(', #')}` : null,
        nodeMap.sampler ? `  Sampler: Node #${nodeMap.sampler}` : null,
        nodeMap.checkpoint ? `  Checkpoint: ${summary.checkpoint || 'Node #' + nodeMap.checkpoint}` : null,
        '',
        'Summary:',
        summary.checkpoint ? `  Model: ${summary.checkpoint}` : null,
        summary.steps != null ? `  Steps: ${summary.steps}` : null,
        summary.cfg != null ? `  CFG: ${summary.cfg}` : null,
        summary.sampler ? `  Sampler: ${summary.sampler}` : null,
        summary.width && summary.height ? `  Size: ${summary.width}×${summary.height}` : null,
        `  Total nodes: ${summary.nodeCount}`,
        '',
        'Use "view" to see all node parameters. Use "modify" to change any parameter before generating.',
      ].filter(Boolean)
    
      // If this is the first workflow, hint about default
      const existing = listWorkflows()
      if (existing.length === 1) {
        lines.push('')
        lines.push(`This is your first workflow — it will be used as the default for image generation.`)
      }
    
      return textResult(lines.join('\n'))
    }
    
    function handleModify(
      name: string | undefined,
      nodeId: string | undefined,
      input: string | undefined,
      value: string | undefined,
      config: MeiGenConfig,
    ) {
      if (!nodeId || !input || value === undefined) {
        return errorResult(
          'modify action requires: nodeId, input, and value.\n' +
          'Use "view" action first to see available node IDs and parameters.\n' +
          'Example: nodeId="3", input="steps", value="30"'
        )
      }
    
      const resolvedName = resolveName(name, config)
      const wf = loadWorkflow(resolvedName)
    
      // Verify node exists
      const node = wf[nodeId]
      if (!node) {
        return errorResult(`Node #${nodeId} not found in workflow "${resolvedName}". Use "view" action to see available nodes.`)
      }
    
      // Verify input exists
      if (!(input in node.inputs)) {
        const available = Object.keys(node.inputs).filter(k => !Array.isArray(node.inputs[k]))
        return errorResult(
          `Input "${input}" not found in Node #${nodeId} (${node.class_type}).\n` +
          `Available inputs: ${available.join(', ')}`
        )
      }
    
      // Parse new value
      let parsedValue: unknown
      try {
        parsedValue = JSON.parse(value)
      } catch {
        return errorResult(`Invalid value "${value}". Must be valid JSON. Examples: 30, "euler", 7.5, true`)
      }
    
      // Record old value
      const oldValue = node.inputs[input]
    
      // Modify and save
      node.inputs[input] = parsedValue
      saveWorkflow(resolvedName, wf)
    
      return textResult(
        `Modified workflow "${resolvedName}":\n\n` +
        `Node #${nodeId} (${node.class_type})\n` +
        `  ${input}: ${JSON.stringify(oldValue)} → ${JSON.stringify(parsedValue)}`
      )
    }
    
    function handleDelete(name: string | undefined) {
      if (!name) {
        return errorResult('name is required for delete action.')
      }
    
      if (!workflowExists(name)) {
        return errorResult(`Workflow "${name}" not found.`)
      }
    
      deleteWorkflow(name)
      return textResult(`Workflow "${name}" deleted.`)
    }
  • Helper function resolveName resolves workflow name from argument, config default, or first available workflow. Also includes textResult and errorResult response formatters.
    function resolveName(name: string | undefined, config: MeiGenConfig): string {
      if (name) {
        if (!workflowExists(name)) {
          throw new Error(`Workflow "${name}" not found. Use "list" action to see available workflows.`)
        }
        return name
      }
    
      const defaultName = config.comfyuiDefaultWorkflow
      if (defaultName && workflowExists(defaultName)) return defaultName
    
      const all = listWorkflows()
      if (all.length === 0) {
        throw new Error('No workflows saved. Use "import" action to add a workflow first.')
      }
      return all[0]
    }
  • getEditableNodes helper from the comfyui library that lists all workflow nodes with their editable inputs, used by handleView.
    export function getEditableNodes(workflow: ComfyUIWorkflow): string {
      const lines: string[] = []
    
      // Mark auto-injected nodes so LLM knows what's handled automatically
      const nodes = detectNodes(workflow)
      const promptNodeId = nodes.positivePrompt
      const loadImageIds = nodes.loadImages || []
    
      for (const [id, node] of Object.entries(workflow)) {
        const title = node._meta?.title || node.class_type
        const marker =
          id === promptNodeId ? ' ← prompt injected here at generation'
          : loadImageIds.includes(id) ? ' ← reference image injected here'
          : ''
    
        lines.push(`Node #${id} (${node.class_type}) — ${title}${marker}`)
    
        for (const [key, val] of Object.entries(node.inputs)) {
          if (Array.isArray(val)) continue // Node connections — not directly editable
          lines.push(`  ${key}: ${JSON.stringify(val)}`)
        }
        lines.push('')
      }
    
      lines.push('To modify a parameter, use action "modify" with nodeId, input, and value.')
      lines.push('Example: nodeId="3", input="steps", value="30"')
    
      return lines.join('\n')
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already provide destructiveHint: true, so the description adds minimal behavioral context. It mentions delete action, which aligns, but no additional traits like persistence or side effects are disclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single sentence with front-loaded actions. No wasted words. Efficient and clear.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 6 parameters, 1 required, no output schema, and rich sibling context, the description covers the gist well. Could mention return values for list/view, but overall adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with detailed descriptions. The description adds marginal value, e.g., default save name for import. Baseline 3 is appropriate as the schema carries most semantic weight.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it manages ComfyUI workflow templates and lists specific actions (list, view, import, modify, delete). This verb+resource combination distinguishes it from siblings which are about prompt/image generation, models, preferences, etc.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implicitly identifies this as the sole tool for workflow management, but lacks explicit guidance on when to use vs alternatives or when not to use. It does provide parameter dependencies in schema descriptions, e.g., which actions require which parameters.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/jau123/mei-gen-ai-design-mcp'

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