@fre4x/comfyui 1.0.61 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +48 -30
  2. package/dist/index.js +432 -118
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,18 +4,12 @@ An MCP server to interact with ComfyUI remotely. Probe server state, inspect nod
4
4
 
5
5
  ## Tools
6
6
 
7
- - `comfyui_get_system_info`: Get server hardware and software status.
8
- - `comfyui_list_object_info`: List available nodes and their schema (inputs/outputs).
9
- - `comfyui_execute_workflow`: Submit a workflow in API JSON format.
10
- - `comfyui_list_workflows`: List stored reusable workflows.
11
- - `comfyui_search_workflows`: Search stored workflows by name, tag, or description.
12
- - `comfyui_get_workflow`: Inspect a stored workflow and its editable inputs.
13
- - `comfyui_save_workflow`: Save a workflow for later reuse.
14
- - `comfyui_run_workflow`: Run a stored workflow with optional overrides.
15
- - `comfyui_get_history`: Retrieve job history and outputs.
7
+ - `comfyui_inspect_node`: List available nodes or inspect one node schema.
8
+ - `comfyui_discover_workflows`: Scan known workflow directories for stored JSON files.
9
+ - `comfyui_get_workflow`: Normalize a stored workflow and list its editable inputs.
10
+ - `comfyui_save_workflow`: Save a workflow into local `./workflows` with optional edits.
11
+ - `comfyui_workflow_run`: Run a stored workflow with optional overrides.
16
12
  - `comfyui_wait_for_workflow`: Wait for workflow completion with a timeout.
17
- - `comfyui_get_queue`: Check current server queue.
18
- - `comfyui_get_view_url`: Get a direct URL for a generated file.
19
13
 
20
14
  **Do not assume `structuredContent` is what the model actually sees.** Many MCP clients
21
15
  surface `content.text` to the model first and treat `structuredContent` as secondary
@@ -28,7 +22,6 @@ IDs, and recovery hints.
28
22
  ### Environment Variables
29
23
 
30
24
  - `COMFYUI_SERVER_URL`: The URL of your ComfyUI server (default: `http://localhost:8188`).
31
- - `COMFYUI_WORKFLOWS_DIR`: Directory used for saved workflow registry files (default: `~/.fre4x-comfyui/workflows`).
32
25
  - `MOCK`: Set to `true` to use mock data instead of a real server.
33
26
 
34
27
  ### Claude Desktop Configuration
@@ -57,35 +50,60 @@ MOCK=true npx @fre4x/comfyui
57
50
 
58
51
  ## Workflow API Format
59
52
 
60
- Note: To use `comfyui_execute_workflow`, you must use the **API Format** JSON. In ComfyUI, enable "Developer Mode" in settings, then click "Save (API Format)". This format differs from the standard workflow JSON.
53
+ `comfyui_workflow_run` and `comfyui_save_workflow` both normalize workflows into
54
+ ComfyUI **API Format** before execution or persistence. In ComfyUI, enable
55
+ "Developer Mode" in settings, then click "Save (API Format)" if you want the
56
+ raw graph JSON. Standard Web UI workflow JSON is also accepted and normalized
57
+ automatically.
61
58
 
62
59
  If you want a single call that submits and waits, pass `await: true` with a
63
- `timeout` value in seconds to `comfyui_execute_workflow`. For lower-level
64
- control, use `comfyui_wait_for_workflow` with `prompt_id`,
65
- `timeout_seconds`, and `poll_interval_ms`.
60
+ `timeout` value in seconds to `comfyui_workflow_run`. For lower-level control,
61
+ use `comfyui_wait_for_workflow` with `prompt_id` and `timeout`.
66
62
 
67
63
  ## Stored Workflow Reuse
68
64
 
69
- The workflow registry tools let you avoid re-sending large API JSON blobs.
65
+ The stored-workflow tools let you avoid re-sending large API JSON blobs and give
66
+ agents a stable local edit loop.
70
67
 
71
- 1. Save a workflow with `comfyui_save_workflow`
72
- 2. Inspect it with `comfyui_get_workflow`
73
- 3. Reuse it with `comfyui_run_workflow`
68
+ 1. Save a workflow with `comfyui_save_workflow` into local `./workflows`
69
+ 2. Inspect editable paths with `comfyui_get_workflow`
70
+ 3. Persist edits with `comfyui_save_workflow` overrides
71
+ 4. Reuse it with `comfyui_workflow_run`
74
72
 
75
- `comfyui_run_workflow` accepts structured overrides in this form:
73
+ `comfyui_workflow_run` and `comfyui_save_workflow` both accept dot-notation
74
+ overrides in this form:
76
75
 
77
76
  ```json
78
77
  {
79
78
  "workflow_id": "pony-portrait-v1",
80
- "overrides": [
81
- {
82
- "node_id": "2",
83
- "input_name": "seed",
84
- "value": 67890
85
- }
86
- ],
79
+ "overrides": {
80
+ "2.inputs.seed": 67890,
81
+ "6.inputs.text": "cinematic dragon portrait"
82
+ },
87
83
  "await": true,
88
- "timeout": 90,
89
- "require_images": true
84
+ "timeout": 90
85
+ }
86
+ ```
87
+
88
+ To inspect editable inputs and semantic hints for agent editing:
89
+
90
+ ```json
91
+ {
92
+ "workflow_id": "pony-portrait-v1",
93
+ "include_prompt": false
94
+ }
95
+ ```
96
+
97
+ To save a new or edited workflow locally:
98
+
99
+ ```json
100
+ {
101
+ "workflow_file_path": "pony-portrait-v1.json",
102
+ "overrides": {
103
+ "6.inputs.text": "studio lighting, ultra detailed",
104
+ "7.inputs.filename_prefix": "pony-portrait-agent"
105
+ },
106
+ "output_file_name": "pony-portrait-agent.json",
107
+ "overwrite": true
90
108
  }
91
109
  ```
package/dist/index.js CHANGED
@@ -30723,6 +30723,14 @@ var ComfyApiError = class extends Error {
30723
30723
  this.name = "ComfyApiError";
30724
30724
  }
30725
30725
  };
30726
+ var WorkflowInputError = class extends Error {
30727
+ constructor(message, field, kind = "validation") {
30728
+ super(message);
30729
+ this.field = field;
30730
+ this.kind = kind;
30731
+ this.name = "WorkflowInputError";
30732
+ }
30733
+ };
30726
30734
  function isRecord(value) {
30727
30735
  return typeof value === "object" && value !== null;
30728
30736
  }
@@ -30856,6 +30864,9 @@ function handleComfyError(error48) {
30856
30864
  error48.issues[0]?.message || "Invalid input."
30857
30865
  );
30858
30866
  }
30867
+ if (error48 instanceof WorkflowInputError) {
30868
+ return error48.kind === "not_found" ? createNotFoundError(error48.message) : createValidationError(error48.field, error48.message);
30869
+ }
30859
30870
  if (error48 instanceof ComfyApiError) {
30860
30871
  const detailSummary = summarizeComfyErrorDetails(error48.details);
30861
30872
  return {
@@ -30874,7 +30885,29 @@ ${detailSummary}` : error48.message
30874
30885
  }
30875
30886
  async function validateWorkflowPath(targetPath) {
30876
30887
  const resolvedPath = path.resolve(targetPath);
30877
- const allowedRoots = [
30888
+ const allowedRoots = getWorkflowDirectories();
30889
+ const isAllowed = allowedRoots.some(
30890
+ (root) => resolvedPath.startsWith(path.resolve(root))
30891
+ );
30892
+ if (!isAllowed) {
30893
+ throw new WorkflowInputError(
30894
+ `Access denied: ${targetPath} is outside allowed workflow directories.`,
30895
+ "workflow_file_path"
30896
+ );
30897
+ }
30898
+ try {
30899
+ await fs.access(resolvedPath);
30900
+ return resolvedPath;
30901
+ } catch {
30902
+ throw new WorkflowInputError(
30903
+ `Workflow file not found: ${targetPath}`,
30904
+ "workflow_file_path",
30905
+ "not_found"
30906
+ );
30907
+ }
30908
+ }
30909
+ function getWorkflowDirectories(extraPath) {
30910
+ const directories = [
30878
30911
  path.join(
30879
30912
  homedir(),
30880
30913
  "comfy",
@@ -30886,20 +30919,285 @@ async function validateWorkflowPath(targetPath) {
30886
30919
  path.join(homedir(), ".fre4x-comfyui", "workflows"),
30887
30920
  path.join(process.cwd(), "workflows")
30888
30921
  ];
30889
- const isAllowed = allowedRoots.some(
30890
- (root) => resolvedPath.startsWith(path.resolve(root))
30922
+ if (extraPath) {
30923
+ directories.push(extraPath);
30924
+ }
30925
+ return directories;
30926
+ }
30927
+ function getWorkflowNameCandidates(name) {
30928
+ const trimmed = name.trim();
30929
+ if (trimmed.length === 0) {
30930
+ return [];
30931
+ }
30932
+ if (trimmed.endsWith(".json")) {
30933
+ return [trimmed];
30934
+ }
30935
+ return [trimmed, `${trimmed}.json`];
30936
+ }
30937
+ function normalizeWorkflowFileName(fileName) {
30938
+ const trimmed = fileName.trim();
30939
+ if (trimmed.length === 0) {
30940
+ throw new WorkflowInputError(
30941
+ "Filename cannot be empty.",
30942
+ "output_file_name"
30943
+ );
30944
+ }
30945
+ if (path.basename(trimmed) !== trimmed) {
30946
+ throw new WorkflowInputError(
30947
+ "Filename must not include directory separators.",
30948
+ "output_file_name"
30949
+ );
30950
+ }
30951
+ return trimmed.endsWith(".json") ? trimmed : `${trimmed}.json`;
30952
+ }
30953
+ function isConnectionValue(value) {
30954
+ return Array.isArray(value) && value.length >= 2 && (typeof value[0] === "string" || typeof value[0] === "number") && typeof value[1] === "number";
30955
+ }
30956
+ async function getObjectInfo() {
30957
+ return IS_MOCK ? MOCK_FIXTURES.object_info : await fetchComfyJson(
30958
+ "/object_info"
30891
30959
  );
30892
- if (!isAllowed) {
30893
- throw new Error(
30894
- `Access denied: ${targetPath} is outside allowed workflow directories.`
30960
+ }
30961
+ async function resolveWorkflowReference(args) {
30962
+ if (args.workflow_file_path) {
30963
+ const targetPath = args.workflow_file_path;
30964
+ if (path.basename(targetPath) === targetPath) {
30965
+ const candidates = getWorkflowNameCandidates(targetPath);
30966
+ for (const dir of getWorkflowDirectories()) {
30967
+ for (const candidate of candidates) {
30968
+ const candidatePath = path.join(dir, candidate);
30969
+ try {
30970
+ const content = await fs.readFile(
30971
+ candidatePath,
30972
+ "utf8"
30973
+ );
30974
+ return {
30975
+ targetPath: candidatePath,
30976
+ content
30977
+ };
30978
+ } catch {
30979
+ }
30980
+ }
30981
+ }
30982
+ throw new WorkflowInputError(
30983
+ `Workflow file not found: ${targetPath}`,
30984
+ "workflow_file_path",
30985
+ "not_found"
30986
+ );
30987
+ }
30988
+ const validatedPath = await validateWorkflowPath(targetPath);
30989
+ return {
30990
+ targetPath: validatedPath,
30991
+ content: await fs.readFile(validatedPath, "utf8")
30992
+ };
30993
+ }
30994
+ if (args.workflow_id) {
30995
+ const requestedId = args.workflow_id.trim();
30996
+ if (requestedId.length === 0) {
30997
+ throw new WorkflowInputError(
30998
+ "Workflow ID cannot be empty.",
30999
+ "workflow_id"
31000
+ );
31001
+ }
31002
+ for (const dir of getWorkflowDirectories()) {
31003
+ try {
31004
+ const entries = await fs.readdir(dir, { withFileTypes: true });
31005
+ for (const entry of entries) {
31006
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
31007
+ continue;
31008
+ }
31009
+ const fullPath = path.join(dir, entry.name);
31010
+ const defaultId = getWorkflowId(fullPath);
31011
+ if (requestedId === defaultId || requestedId === entry.name.replace(/\.json$/, "")) {
31012
+ return {
31013
+ targetPath: fullPath,
31014
+ content: await fs.readFile(fullPath, "utf8")
31015
+ };
31016
+ }
31017
+ }
31018
+ } catch {
31019
+ }
31020
+ }
31021
+ throw new WorkflowInputError(
31022
+ `Workflow ID not found: ${args.workflow_id}`,
31023
+ "workflow_id",
31024
+ "not_found"
30895
31025
  );
30896
31026
  }
31027
+ throw new WorkflowInputError(
31028
+ "Must provide either workflow_file_path or workflow_id.",
31029
+ "workflow_id"
31030
+ );
31031
+ }
31032
+ async function normalizeWorkflowPrompt(workflow) {
31033
+ if (!isRecord(workflow)) {
31034
+ throw new WorkflowInputError(
31035
+ "Workflow must be a JSON object.",
31036
+ "workflow"
31037
+ );
31038
+ }
31039
+ let sourceFormat = "api";
31040
+ let normalizedWorkflow = workflow;
31041
+ if (isWebUIFormat(normalizedWorkflow)) {
31042
+ sourceFormat = "webui";
31043
+ normalizedWorkflow = convertWebUIToAPI(
31044
+ normalizedWorkflow,
31045
+ await getObjectInfo()
31046
+ );
31047
+ }
31048
+ const prompt = isRecord(normalizedWorkflow.prompt) && normalizedWorkflow.prompt ? normalizedWorkflow.prompt : normalizedWorkflow;
31049
+ if (!isRecord(prompt)) {
31050
+ throw new WorkflowInputError(
31051
+ "Workflow prompt must be a JSON object.",
31052
+ "workflow"
31053
+ );
31054
+ }
31055
+ return {
31056
+ prompt,
31057
+ sourceFormat
31058
+ };
31059
+ }
31060
+ async function loadWorkflow(args) {
31061
+ const { targetPath, content } = await resolveWorkflowReference(args);
31062
+ let parsed;
30897
31063
  try {
30898
- await fs.access(resolvedPath);
30899
- return resolvedPath;
31064
+ parsed = JSON.parse(content);
31065
+ } catch (error48) {
31066
+ throw new WorkflowInputError(
31067
+ `Invalid JSON in ${targetPath}: ${error48 instanceof Error ? error48.message : String(error48)}`,
31068
+ args.workflow_file_path ? "workflow_file_path" : "workflow_id"
31069
+ );
31070
+ }
31071
+ const { prompt, sourceFormat } = await normalizeWorkflowPrompt(parsed);
31072
+ return {
31073
+ targetPath,
31074
+ prompt,
31075
+ sourceFormat
31076
+ };
31077
+ }
31078
+ function inferEditableRole(nodeId, classType, inputName, consumerMap) {
31079
+ if (classType === "CLIPTextEncode" && inputName === "text") {
31080
+ const consumers = consumerMap.get(nodeId) ?? [];
31081
+ if (consumers.some((consumer) => consumer.inputName === "positive")) {
31082
+ return "positive_prompt";
31083
+ }
31084
+ if (consumers.some((consumer) => consumer.inputName === "negative")) {
31085
+ return "negative_prompt";
31086
+ }
31087
+ return "prompt_text";
31088
+ }
31089
+ switch (inputName) {
31090
+ case "seed":
31091
+ return "seed";
31092
+ case "steps":
31093
+ return "steps";
31094
+ case "cfg":
31095
+ return "cfg";
31096
+ case "sampler_name":
31097
+ return "sampler";
31098
+ case "scheduler":
31099
+ return "scheduler";
31100
+ case "denoise":
31101
+ return "denoise";
31102
+ case "width":
31103
+ return "width";
31104
+ case "height":
31105
+ return "height";
31106
+ case "batch_size":
31107
+ return "batch_size";
31108
+ case "ckpt_name":
31109
+ return "checkpoint";
31110
+ case "filename_prefix":
31111
+ return "filename_prefix";
31112
+ case "text":
31113
+ return "text";
31114
+ default:
31115
+ return void 0;
31116
+ }
31117
+ }
31118
+ function formatValuePreview(value) {
31119
+ if (typeof value === "string") {
31120
+ return value.length > 100 ? `${value.slice(0, 97)}...` : value;
31121
+ }
31122
+ try {
31123
+ const serialized = JSON.stringify(value);
31124
+ if (!serialized) {
31125
+ return String(value);
31126
+ }
31127
+ return serialized.length > 100 ? `${serialized.slice(0, 97)}...` : serialized;
30900
31128
  } catch {
30901
- throw new Error(`Workflow file not found: ${targetPath}`);
31129
+ return String(value);
31130
+ }
31131
+ }
31132
+ function analyzeEditableInputs(prompt) {
31133
+ const consumerMap = /* @__PURE__ */ new Map();
31134
+ for (const [nodeId, rawNode] of Object.entries(prompt)) {
31135
+ if (!isRecord(rawNode) || !isRecord(rawNode.inputs)) {
31136
+ continue;
31137
+ }
31138
+ const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
31139
+ for (const [inputName, value] of Object.entries(rawNode.inputs)) {
31140
+ if (!isConnectionValue(value)) {
31141
+ continue;
31142
+ }
31143
+ const sourceNodeId = String(value[0]);
31144
+ const consumers = consumerMap.get(sourceNodeId) ?? [];
31145
+ consumers.push({
31146
+ inputName,
31147
+ classType,
31148
+ nodeId
31149
+ });
31150
+ consumerMap.set(sourceNodeId, consumers);
31151
+ }
31152
+ }
31153
+ const editableInputs = [];
31154
+ for (const [nodeId, rawNode] of Object.entries(prompt)) {
31155
+ if (!isRecord(rawNode) || !isRecord(rawNode.inputs)) {
31156
+ continue;
31157
+ }
31158
+ const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
31159
+ for (const [inputName, value] of Object.entries(rawNode.inputs)) {
31160
+ if (isConnectionValue(value)) {
31161
+ continue;
31162
+ }
31163
+ editableInputs.push({
31164
+ path: `${nodeId}.inputs.${inputName}`,
31165
+ node_id: nodeId,
31166
+ class_type: classType,
31167
+ input_name: inputName,
31168
+ value,
31169
+ value_preview: formatValuePreview(value),
31170
+ role: inferEditableRole(
31171
+ nodeId,
31172
+ classType,
31173
+ inputName,
31174
+ consumerMap
31175
+ )
31176
+ });
31177
+ }
31178
+ }
31179
+ return editableInputs.sort(
31180
+ (left, right) => left.path.localeCompare(right.path, void 0, { numeric: true })
31181
+ );
31182
+ }
31183
+ function formatEditableInputsText(editableInputs) {
31184
+ if (editableInputs.length === 0) {
31185
+ return "No literal editable inputs were detected.";
31186
+ }
31187
+ return formatListItems(editableInputs, (entry) => {
31188
+ const role = entry.role ? ` [${entry.role}]` : "";
31189
+ const classType = entry.class_type ? ` (${entry.class_type})` : "";
31190
+ return `${entry.path}${role}${classType}: ${entry.value_preview}`;
31191
+ });
31192
+ }
31193
+ function deriveSavedWorkflowFileName(sourcePath, requestedName) {
31194
+ if (requestedName) {
31195
+ return normalizeWorkflowFileName(requestedName);
31196
+ }
31197
+ if (sourcePath) {
31198
+ return normalizeWorkflowFileName(path.basename(sourcePath));
30902
31199
  }
31200
+ return `workflow-${Date.now()}.json`;
30903
31201
  }
30904
31202
  function applyOverrides(prompt, overrides) {
30905
31203
  for (const [key, value] of Object.entries(overrides)) {
@@ -30937,6 +31235,20 @@ var WorkflowRunSchema = z3.object({
30937
31235
  await: z3.boolean().default(true).describe("Wait for completion and return native images."),
30938
31236
  timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
30939
31237
  });
31238
+ var GetWorkflowSchema = z3.object({
31239
+ workflow_file_path: z3.string().optional().describe("Workflow path or filename to inspect."),
31240
+ workflow_id: z3.string().optional().describe("Workflow identifier returned by discovery."),
31241
+ include_prompt: z3.boolean().default(false).describe("Include normalized API JSON in structuredContent.")
31242
+ });
31243
+ var SaveWorkflowSchema = z3.object({
31244
+ workflow: z3.unknown().optional().describe("Workflow JSON object to save locally."),
31245
+ workflow_file_path: z3.string().optional().describe("Existing workflow path or filename to save."),
31246
+ workflow_id: z3.string().optional().describe("Existing workflow identifier to save."),
31247
+ overrides: z3.record(z3.string(), z3.unknown()).optional().describe("Dot-notation overrides to persist before saving."),
31248
+ output_file_name: z3.string().optional().describe("Filename to write under ./workflows."),
31249
+ overwrite: z3.boolean().default(false).describe("Replace an existing local file when true."),
31250
+ include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent.")
31251
+ });
30940
31252
  var WaitForWorkflowSchema = z3.object({
30941
31253
  prompt_id: z3.string().describe("The prompt_id of the submitted workflow."),
30942
31254
  timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
@@ -31158,122 +31470,112 @@ async function handleWaitForWorkflow(args) {
31158
31470
  return handleComfyError(error48);
31159
31471
  }
31160
31472
  }
31161
- async function handleWorkflowRun(args) {
31473
+ async function handleGetWorkflow(args) {
31162
31474
  try {
31163
- let content = "";
31164
- let targetPath = "";
31165
- if (args.workflow_file_path) {
31166
- targetPath = args.workflow_file_path;
31167
- try {
31168
- if (!targetPath.includes(path.sep)) {
31169
- const paths = [
31170
- path.join(
31171
- homedir(),
31172
- "comfy",
31173
- "ComfyUI",
31174
- "user",
31175
- "default",
31176
- "workflows",
31177
- targetPath
31178
- ),
31179
- path.join(
31180
- homedir(),
31181
- ".fre4x-comfyui",
31182
- "workflows",
31183
- targetPath
31184
- ),
31185
- path.join(process.cwd(), "workflows", targetPath)
31186
- ];
31187
- let found = false;
31188
- for (const p of paths) {
31189
- try {
31190
- content = await fs.readFile(p, "utf8");
31191
- targetPath = p;
31192
- found = true;
31193
- break;
31194
- } catch (_e) {
31195
- }
31196
- }
31197
- if (!found) {
31198
- throw new Error(
31199
- `Workflow file not found: ${targetPath}`
31200
- );
31201
- }
31202
- } else {
31203
- targetPath = await validateWorkflowPath(targetPath);
31204
- content = await fs.readFile(targetPath, "utf8");
31205
- }
31206
- } catch (err) {
31207
- return handleComfyError(err);
31208
- }
31209
- } else if (args.workflow_id) {
31210
- const pathsToScan = [
31211
- path.join(
31212
- homedir(),
31213
- "comfy",
31214
- "ComfyUI",
31215
- "user",
31216
- "default",
31217
- "workflows"
31218
- ),
31219
- path.join(homedir(), ".fre4x-comfyui", "workflows"),
31220
- path.join(process.cwd(), "workflows")
31221
- ];
31222
- let found = false;
31223
- for (const dir of pathsToScan) {
31224
- try {
31225
- const entries = await fs.readdir(dir, {
31226
- withFileTypes: true
31227
- });
31228
- for (const entry of entries) {
31229
- if (entry.isFile() && entry.name.endsWith(".json")) {
31230
- const fullPath = path.join(dir, entry.name);
31231
- const defaultId = getWorkflowId(fullPath);
31232
- if (args.workflow_id === defaultId || args.workflow_id === entry.name.replace(/\.json$/, "")) {
31233
- content = await fs.readFile(fullPath, "utf8");
31234
- targetPath = fullPath;
31235
- found = true;
31236
- break;
31237
- }
31238
- }
31239
- }
31240
- if (found) break;
31241
- } catch (_e) {
31242
- }
31243
- }
31244
- if (!found) {
31245
- throw new Error(`Workflow ID not found: ${args.workflow_id}`);
31246
- }
31475
+ const loadedWorkflow = await loadWorkflow(args);
31476
+ const editableInputs = analyzeEditableInputs(loadedWorkflow.prompt);
31477
+ const workflowId = getWorkflowId(loadedWorkflow.targetPath);
31478
+ const text = [
31479
+ `Workflow ${workflowId} loaded from ${loadedWorkflow.targetPath}.`,
31480
+ loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format.",
31481
+ `Editable inputs (${editableInputs.length}):`,
31482
+ formatEditableInputsText(editableInputs)
31483
+ ].join("\n");
31484
+ const structuredContent = {
31485
+ workflow_id: workflowId,
31486
+ path: loadedWorkflow.targetPath,
31487
+ source_format: loadedWorkflow.sourceFormat,
31488
+ editable_inputs: editableInputs
31489
+ };
31490
+ if (args.include_prompt) {
31491
+ structuredContent.prompt = loadedWorkflow.prompt;
31492
+ }
31493
+ return {
31494
+ content: [{ type: "text", text }],
31495
+ structuredContent
31496
+ };
31497
+ } catch (error48) {
31498
+ return handleComfyError(error48);
31499
+ }
31500
+ }
31501
+ async function handleSaveWorkflow(args) {
31502
+ try {
31503
+ let prompt;
31504
+ let sourcePath;
31505
+ if (args.workflow !== void 0) {
31506
+ const normalized = await normalizeWorkflowPrompt(args.workflow);
31507
+ prompt = normalized.prompt;
31247
31508
  } else {
31248
- throw new Error(
31249
- "Must provide either workflow_file_path or workflow_id."
31250
- );
31509
+ if (!args.workflow_file_path && !args.workflow_id) {
31510
+ return createValidationError(
31511
+ "workflow",
31512
+ "Provide workflow JSON or an existing workflow reference."
31513
+ );
31514
+ }
31515
+ const loadedWorkflow = await loadWorkflow(args);
31516
+ prompt = loadedWorkflow.prompt;
31517
+ sourcePath = loadedWorkflow.targetPath;
31518
+ }
31519
+ if (args.overrides) {
31520
+ applyOverrides(prompt, args.overrides);
31251
31521
  }
31252
- let promptObj = {};
31522
+ const workflowsDir = path.join(process.cwd(), "workflows");
31523
+ await fs.mkdir(workflowsDir, { recursive: true });
31524
+ const outputFileName = deriveSavedWorkflowFileName(
31525
+ sourcePath,
31526
+ args.output_file_name
31527
+ );
31528
+ const outputPath = path.join(workflowsDir, outputFileName);
31529
+ let existedBeforeSave = false;
31253
31530
  try {
31254
- promptObj = JSON.parse(content);
31255
- } catch (e) {
31256
- throw new Error(
31257
- `SyntaxError: Invalid JSON in ${targetPath}: ${e instanceof Error ? e.message : String(e)}`
31258
- );
31531
+ await fs.access(outputPath);
31532
+ existedBeforeSave = true;
31533
+ } catch {
31534
+ existedBeforeSave = false;
31259
31535
  }
31260
- if (isWebUIFormat(promptObj)) {
31261
- let objectInfo;
31262
- if (IS_MOCK) {
31263
- objectInfo = MOCK_FIXTURES.object_info;
31264
- } else {
31265
- objectInfo = await fetchComfyJson(
31266
- "/object_info"
31267
- );
31268
- }
31269
- promptObj = convertWebUIToAPI(
31270
- promptObj,
31271
- objectInfo
31536
+ if (existedBeforeSave && !args.overwrite) {
31537
+ return createValidationError(
31538
+ "output_file_name",
31539
+ `File already exists: ${outputFileName}. Set overwrite=true to replace it.`
31272
31540
  );
31273
31541
  }
31274
- if (promptObj.prompt && typeof promptObj.prompt === "object") {
31275
- promptObj = promptObj.prompt;
31276
- }
31542
+ await fs.writeFile(
31543
+ outputPath,
31544
+ `${JSON.stringify(prompt, null, 2)}
31545
+ `,
31546
+ "utf8"
31547
+ );
31548
+ const editableInputs = analyzeEditableInputs(prompt);
31549
+ const workflowId = getWorkflowId(outputPath);
31550
+ const structuredContent = {
31551
+ workflow_id: workflowId,
31552
+ path: outputPath,
31553
+ source_path: sourcePath,
31554
+ overwritten: existedBeforeSave,
31555
+ editable_inputs: editableInputs
31556
+ };
31557
+ if (args.include_prompt) {
31558
+ structuredContent.prompt = prompt;
31559
+ }
31560
+ const persistedChanges = args.overrides && Object.keys(args.overrides).length > 0 ? `Persisted ${Object.keys(args.overrides).length} override(s).` : "No overrides were applied.";
31561
+ const text = [
31562
+ `Saved workflow ${workflowId} to ${outputPath}.`,
31563
+ persistedChanges,
31564
+ `Editable inputs (${editableInputs.length}):`,
31565
+ formatEditableInputsText(editableInputs)
31566
+ ].join("\n");
31567
+ return {
31568
+ content: [{ type: "text", text }],
31569
+ structuredContent
31570
+ };
31571
+ } catch (error48) {
31572
+ return handleComfyError(error48);
31573
+ }
31574
+ }
31575
+ async function handleWorkflowRun(args) {
31576
+ try {
31577
+ const { prompt } = await loadWorkflow(args);
31578
+ const promptObj = { ...prompt };
31277
31579
  if (args.overrides) {
31278
31580
  applyOverrides(promptObj, args.overrides);
31279
31581
  }
@@ -31339,6 +31641,18 @@ function createServer() {
31339
31641
  WorkflowRunSchema.shape,
31340
31642
  handleWorkflowRun
31341
31643
  );
31644
+ server.tool(
31645
+ "comfyui_get_workflow",
31646
+ "Inspect a stored workflow and list editable inputs.",
31647
+ GetWorkflowSchema.shape,
31648
+ handleGetWorkflow
31649
+ );
31650
+ server.tool(
31651
+ "comfyui_save_workflow",
31652
+ "Save a workflow into ./workflows with optional edits.",
31653
+ SaveWorkflowSchema.shape,
31654
+ handleSaveWorkflow
31655
+ );
31342
31656
  server.tool(
31343
31657
  "comfyui_wait_for_workflow",
31344
31658
  "Wait for a submitted workflow to finish and fetch its images.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fre4x/comfyui",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "description": "MCP server for ComfyUI. Execute workflows and probe server state remotely.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",