@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.
- package/README.md +48 -30
- package/dist/index.js +432 -118
- 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
|
-
- `
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
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
|
-
|
|
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 `
|
|
64
|
-
|
|
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
|
|
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
|
|
73
|
-
3.
|
|
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
|
-
`
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30890
|
-
|
|
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
|
-
|
|
30893
|
-
|
|
30894
|
-
|
|
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
|
-
|
|
30899
|
-
|
|
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
|
-
|
|
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
|
|
31473
|
+
async function handleGetWorkflow(args) {
|
|
31162
31474
|
try {
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31168
|
-
|
|
31169
|
-
|
|
31170
|
-
|
|
31171
|
-
|
|
31172
|
-
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31178
|
-
|
|
31179
|
-
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
31193
|
-
|
|
31194
|
-
|
|
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
|
-
|
|
31249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
);
|
|
31531
|
+
await fs.access(outputPath);
|
|
31532
|
+
existedBeforeSave = true;
|
|
31533
|
+
} catch {
|
|
31534
|
+
existedBeforeSave = false;
|
|
31259
31535
|
}
|
|
31260
|
-
if (
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
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
|
-
|
|
31275
|
-
|
|
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.",
|