@fre4x/comfyui 1.0.61 → 1.0.64
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 +562 -163
- 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
|
@@ -30630,9 +30630,16 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
|
|
|
30630
30630
|
const prompt = {};
|
|
30631
30631
|
const links = webUIJson.links || [];
|
|
30632
30632
|
const nodes = webUIJson.nodes || [];
|
|
30633
|
+
const serverNodeIds = /* @__PURE__ */ new Set();
|
|
30634
|
+
for (const node of nodes) {
|
|
30635
|
+
if (!node.id || !node.type || !objectInfo[node.type]) {
|
|
30636
|
+
continue;
|
|
30637
|
+
}
|
|
30638
|
+
serverNodeIds.add(String(node.id));
|
|
30639
|
+
}
|
|
30633
30640
|
const linkMap = /* @__PURE__ */ new Map();
|
|
30634
30641
|
for (const link of links) {
|
|
30635
|
-
if (isLinkData(link)) {
|
|
30642
|
+
if (isLinkData(link) && serverNodeIds.has(String(link[3]))) {
|
|
30636
30643
|
linkMap.set(link[0], link);
|
|
30637
30644
|
}
|
|
30638
30645
|
}
|
|
@@ -30650,7 +30657,7 @@ function convertWebUIToAPI(webUIJson, objectInfo) {
|
|
|
30650
30657
|
for (const input of node.inputs) {
|
|
30651
30658
|
if (input.name && input.link !== null && input.link !== void 0) {
|
|
30652
30659
|
const linkData = linkMap.get(input.link);
|
|
30653
|
-
if (Array.isArray(linkData) && linkData.length >= 6) {
|
|
30660
|
+
if (Array.isArray(linkData) && linkData.length >= 6 && String(linkData[3]) === nodeIdStr) {
|
|
30654
30661
|
const originNodeId = String(linkData[1]);
|
|
30655
30662
|
const originSlot = linkData[2];
|
|
30656
30663
|
promptNode.inputs[input.name] = [
|
|
@@ -30723,6 +30730,14 @@ var ComfyApiError = class extends Error {
|
|
|
30723
30730
|
this.name = "ComfyApiError";
|
|
30724
30731
|
}
|
|
30725
30732
|
};
|
|
30733
|
+
var WorkflowInputError = class extends Error {
|
|
30734
|
+
constructor(message, field, kind = "validation") {
|
|
30735
|
+
super(message);
|
|
30736
|
+
this.field = field;
|
|
30737
|
+
this.kind = kind;
|
|
30738
|
+
this.name = "WorkflowInputError";
|
|
30739
|
+
}
|
|
30740
|
+
};
|
|
30726
30741
|
function isRecord(value) {
|
|
30727
30742
|
return typeof value === "object" && value !== null;
|
|
30728
30743
|
}
|
|
@@ -30832,7 +30847,7 @@ async function fetchComfyJson(apiPath, options) {
|
|
|
30832
30847
|
}
|
|
30833
30848
|
return body;
|
|
30834
30849
|
}
|
|
30835
|
-
async function
|
|
30850
|
+
async function fetchImageAsset(filename, type, subfolder) {
|
|
30836
30851
|
const params = new URLSearchParams({ filename, type });
|
|
30837
30852
|
if (subfolder) params.set("subfolder", subfolder);
|
|
30838
30853
|
const url2 = new URL(
|
|
@@ -30845,10 +30860,47 @@ async function fetchImageBase64(filename, type, subfolder) {
|
|
|
30845
30860
|
const arrayBuffer = await response.arrayBuffer();
|
|
30846
30861
|
const mimeType = response.headers.get("content-type") || "image/png";
|
|
30847
30862
|
return {
|
|
30848
|
-
|
|
30863
|
+
buffer: Buffer.from(arrayBuffer),
|
|
30849
30864
|
mimeType
|
|
30850
30865
|
};
|
|
30851
30866
|
}
|
|
30867
|
+
function createMockImageAsset() {
|
|
30868
|
+
return {
|
|
30869
|
+
buffer: Buffer.from("mock-image-data"),
|
|
30870
|
+
mimeType: "image/png"
|
|
30871
|
+
};
|
|
30872
|
+
}
|
|
30873
|
+
function getSessionWorkspaceDir() {
|
|
30874
|
+
return process.env.MCP_WORKSPACE_DIR;
|
|
30875
|
+
}
|
|
30876
|
+
function getSafePathSegments(subfolder) {
|
|
30877
|
+
if (!subfolder) return [];
|
|
30878
|
+
return subfolder.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".." && s !== ".");
|
|
30879
|
+
}
|
|
30880
|
+
async function saveToWorkspace(promptId, image, asset) {
|
|
30881
|
+
const workspaceDir = getSessionWorkspaceDir();
|
|
30882
|
+
if (!workspaceDir) {
|
|
30883
|
+
return void 0;
|
|
30884
|
+
}
|
|
30885
|
+
const safeFileName = path.basename(image.filename);
|
|
30886
|
+
const safeSubfolderSegments = getSafePathSegments(image.subfolder);
|
|
30887
|
+
const relativePath = path.join(
|
|
30888
|
+
"comfyui",
|
|
30889
|
+
promptId,
|
|
30890
|
+
...safeSubfolderSegments,
|
|
30891
|
+
safeFileName
|
|
30892
|
+
);
|
|
30893
|
+
const workspacePath = path.join(workspaceDir, relativePath);
|
|
30894
|
+
await fs.mkdir(path.dirname(workspacePath), { recursive: true });
|
|
30895
|
+
await fs.writeFile(workspacePath, asset.buffer);
|
|
30896
|
+
return {
|
|
30897
|
+
workspace_path: workspacePath,
|
|
30898
|
+
relative_path: relativePath,
|
|
30899
|
+
filename: safeFileName,
|
|
30900
|
+
mime_type: asset.mimeType,
|
|
30901
|
+
subfolder: image.subfolder
|
|
30902
|
+
};
|
|
30903
|
+
}
|
|
30852
30904
|
function handleComfyError(error48) {
|
|
30853
30905
|
if (error48 instanceof ZodError) {
|
|
30854
30906
|
return createValidationError(
|
|
@@ -30856,6 +30908,9 @@ function handleComfyError(error48) {
|
|
|
30856
30908
|
error48.issues[0]?.message || "Invalid input."
|
|
30857
30909
|
);
|
|
30858
30910
|
}
|
|
30911
|
+
if (error48 instanceof WorkflowInputError) {
|
|
30912
|
+
return error48.kind === "not_found" ? createNotFoundError(error48.message) : createValidationError(error48.field, error48.message);
|
|
30913
|
+
}
|
|
30859
30914
|
if (error48 instanceof ComfyApiError) {
|
|
30860
30915
|
const detailSummary = summarizeComfyErrorDetails(error48.details);
|
|
30861
30916
|
return {
|
|
@@ -30870,11 +30925,27 @@ ${detailSummary}` : error48.message
|
|
|
30870
30925
|
isError: true
|
|
30871
30926
|
};
|
|
30872
30927
|
}
|
|
30928
|
+
const errorMessage = error48 instanceof Error ? error48.message : String(error48);
|
|
30929
|
+
if (errorMessage.includes("Timeout waiting for workflow")) {
|
|
30930
|
+
return {
|
|
30931
|
+
content: [
|
|
30932
|
+
{
|
|
30933
|
+
type: "text",
|
|
30934
|
+
text: `${errorMessage}.
|
|
30935
|
+
|
|
30936
|
+
The workflow is likely still running on the server. You can re-attach to it and wait for completion by calling 'comfyui_wait_for_workflow' again with the same prompt_id.`
|
|
30937
|
+
}
|
|
30938
|
+
],
|
|
30939
|
+
isError: true
|
|
30940
|
+
};
|
|
30941
|
+
}
|
|
30873
30942
|
return createInternalError(error48);
|
|
30874
30943
|
}
|
|
30875
|
-
|
|
30876
|
-
const
|
|
30877
|
-
|
|
30944
|
+
function getWorkflowDirectories(extraPath) {
|
|
30945
|
+
const directories = [
|
|
30946
|
+
process.cwd(),
|
|
30947
|
+
path.join(process.cwd(), "workflows"),
|
|
30948
|
+
path.join(homedir(), ".fre4x-comfyui", "workflows"),
|
|
30878
30949
|
path.join(
|
|
30879
30950
|
homedir(),
|
|
30880
30951
|
"comfy",
|
|
@@ -30882,24 +30953,291 @@ async function validateWorkflowPath(targetPath) {
|
|
|
30882
30953
|
"user",
|
|
30883
30954
|
"default",
|
|
30884
30955
|
"workflows"
|
|
30885
|
-
)
|
|
30886
|
-
path.join(homedir(), ".fre4x-comfyui", "workflows"),
|
|
30887
|
-
path.join(process.cwd(), "workflows")
|
|
30956
|
+
)
|
|
30888
30957
|
];
|
|
30889
|
-
|
|
30890
|
-
|
|
30958
|
+
if (extraPath) {
|
|
30959
|
+
directories.push(extraPath);
|
|
30960
|
+
}
|
|
30961
|
+
return directories;
|
|
30962
|
+
}
|
|
30963
|
+
function getWorkflowNameCandidates(name) {
|
|
30964
|
+
const trimmed = name.trim();
|
|
30965
|
+
if (trimmed.length === 0) {
|
|
30966
|
+
return [];
|
|
30967
|
+
}
|
|
30968
|
+
if (trimmed.endsWith(".json")) {
|
|
30969
|
+
return [trimmed];
|
|
30970
|
+
}
|
|
30971
|
+
return [trimmed, `${trimmed}.json`];
|
|
30972
|
+
}
|
|
30973
|
+
function normalizeWorkflowFileName(fileName) {
|
|
30974
|
+
const trimmed = fileName.trim();
|
|
30975
|
+
if (trimmed.length === 0) {
|
|
30976
|
+
throw new WorkflowInputError(
|
|
30977
|
+
"Filename cannot be empty.",
|
|
30978
|
+
"output_file_name"
|
|
30979
|
+
);
|
|
30980
|
+
}
|
|
30981
|
+
return trimmed.endsWith(".json") ? trimmed : `${trimmed}.json`;
|
|
30982
|
+
}
|
|
30983
|
+
function isConnectionValue(value) {
|
|
30984
|
+
return Array.isArray(value) && value.length >= 2 && (typeof value[0] === "string" || typeof value[0] === "number") && typeof value[1] === "number";
|
|
30985
|
+
}
|
|
30986
|
+
async function getObjectInfo() {
|
|
30987
|
+
return IS_MOCK ? MOCK_FIXTURES.object_info : await fetchComfyJson(
|
|
30988
|
+
"/object_info"
|
|
30891
30989
|
);
|
|
30892
|
-
|
|
30893
|
-
|
|
30894
|
-
|
|
30990
|
+
}
|
|
30991
|
+
async function resolveWorkflowReference(args) {
|
|
30992
|
+
if (args.workflow_file_path) {
|
|
30993
|
+
const targetPath = args.workflow_file_path;
|
|
30994
|
+
try {
|
|
30995
|
+
const resolvedPath = path.resolve(targetPath);
|
|
30996
|
+
const content = await fs.readFile(resolvedPath, "utf8");
|
|
30997
|
+
return {
|
|
30998
|
+
targetPath: resolvedPath,
|
|
30999
|
+
content
|
|
31000
|
+
};
|
|
31001
|
+
} catch {
|
|
31002
|
+
if (path.basename(targetPath) === targetPath) {
|
|
31003
|
+
const candidates = getWorkflowNameCandidates(targetPath);
|
|
31004
|
+
for (const dir of getWorkflowDirectories()) {
|
|
31005
|
+
for (const candidate of candidates) {
|
|
31006
|
+
const candidatePath = path.join(dir, candidate);
|
|
31007
|
+
try {
|
|
31008
|
+
const content = await fs.readFile(
|
|
31009
|
+
candidatePath,
|
|
31010
|
+
"utf8"
|
|
31011
|
+
);
|
|
31012
|
+
return {
|
|
31013
|
+
targetPath: candidatePath,
|
|
31014
|
+
content
|
|
31015
|
+
};
|
|
31016
|
+
} catch {
|
|
31017
|
+
}
|
|
31018
|
+
}
|
|
31019
|
+
}
|
|
31020
|
+
}
|
|
31021
|
+
throw new WorkflowInputError(
|
|
31022
|
+
`Workflow file not found: ${targetPath}`,
|
|
31023
|
+
"workflow_file_path",
|
|
31024
|
+
"not_found"
|
|
31025
|
+
);
|
|
31026
|
+
}
|
|
31027
|
+
}
|
|
31028
|
+
if (args.workflow_id) {
|
|
31029
|
+
const requestedId = args.workflow_id.trim();
|
|
31030
|
+
if (requestedId.length === 0) {
|
|
31031
|
+
throw new WorkflowInputError(
|
|
31032
|
+
"Workflow ID cannot be empty.",
|
|
31033
|
+
"workflow_id"
|
|
31034
|
+
);
|
|
31035
|
+
}
|
|
31036
|
+
for (const dir of getWorkflowDirectories()) {
|
|
31037
|
+
try {
|
|
31038
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
31039
|
+
for (const entry of entries) {
|
|
31040
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
31041
|
+
continue;
|
|
31042
|
+
}
|
|
31043
|
+
const fullPath = path.join(dir, entry.name);
|
|
31044
|
+
const defaultId = getWorkflowId(fullPath);
|
|
31045
|
+
if (requestedId === defaultId || requestedId === entry.name.replace(/\.json$/, "")) {
|
|
31046
|
+
return {
|
|
31047
|
+
targetPath: fullPath,
|
|
31048
|
+
content: await fs.readFile(fullPath, "utf8")
|
|
31049
|
+
};
|
|
31050
|
+
}
|
|
31051
|
+
}
|
|
31052
|
+
} catch {
|
|
31053
|
+
}
|
|
31054
|
+
}
|
|
31055
|
+
throw new WorkflowInputError(
|
|
31056
|
+
`Workflow ID not found: ${args.workflow_id}`,
|
|
31057
|
+
"workflow_id",
|
|
31058
|
+
"not_found"
|
|
30895
31059
|
);
|
|
30896
31060
|
}
|
|
31061
|
+
throw new WorkflowInputError(
|
|
31062
|
+
"Must provide either workflow_file_path or workflow_id.",
|
|
31063
|
+
"workflow_id"
|
|
31064
|
+
);
|
|
31065
|
+
}
|
|
31066
|
+
async function normalizeWorkflowPrompt(workflow) {
|
|
31067
|
+
if (!isRecord(workflow)) {
|
|
31068
|
+
throw new WorkflowInputError(
|
|
31069
|
+
"Workflow must be a JSON object.",
|
|
31070
|
+
"workflow"
|
|
31071
|
+
);
|
|
31072
|
+
}
|
|
31073
|
+
let sourceFormat = "api";
|
|
31074
|
+
let normalizedWorkflow = workflow;
|
|
31075
|
+
if (isWebUIFormat(normalizedWorkflow)) {
|
|
31076
|
+
sourceFormat = "webui";
|
|
31077
|
+
normalizedWorkflow = convertWebUIToAPI(
|
|
31078
|
+
normalizedWorkflow,
|
|
31079
|
+
await getObjectInfo()
|
|
31080
|
+
);
|
|
31081
|
+
}
|
|
31082
|
+
const prompt = isRecord(normalizedWorkflow.prompt) && normalizedWorkflow.prompt ? normalizedWorkflow.prompt : normalizedWorkflow;
|
|
31083
|
+
if (!isRecord(prompt)) {
|
|
31084
|
+
throw new WorkflowInputError(
|
|
31085
|
+
"Workflow prompt must be a JSON object.",
|
|
31086
|
+
"workflow"
|
|
31087
|
+
);
|
|
31088
|
+
}
|
|
31089
|
+
return {
|
|
31090
|
+
prompt,
|
|
31091
|
+
sourceFormat
|
|
31092
|
+
};
|
|
31093
|
+
}
|
|
31094
|
+
async function loadWorkflow(args) {
|
|
31095
|
+
const { targetPath, content } = await resolveWorkflowReference(args);
|
|
31096
|
+
let parsed;
|
|
31097
|
+
try {
|
|
31098
|
+
parsed = JSON.parse(content);
|
|
31099
|
+
} catch (error48) {
|
|
31100
|
+
throw new WorkflowInputError(
|
|
31101
|
+
`Invalid JSON in ${targetPath}: ${error48 instanceof Error ? error48.message : String(error48)}`,
|
|
31102
|
+
args.workflow_file_path ? "workflow_file_path" : "workflow_id"
|
|
31103
|
+
);
|
|
31104
|
+
}
|
|
31105
|
+
const { prompt, sourceFormat } = await normalizeWorkflowPrompt(parsed);
|
|
31106
|
+
return {
|
|
31107
|
+
targetPath,
|
|
31108
|
+
prompt,
|
|
31109
|
+
sourceFormat
|
|
31110
|
+
};
|
|
31111
|
+
}
|
|
31112
|
+
function inferEditableRole(nodeId, classType, inputName, consumerMap) {
|
|
31113
|
+
if (classType === "CLIPTextEncode" && inputName === "text") {
|
|
31114
|
+
const consumers = consumerMap.get(nodeId) ?? [];
|
|
31115
|
+
if (consumers.some((consumer) => consumer.inputName === "positive")) {
|
|
31116
|
+
return "positive_prompt";
|
|
31117
|
+
}
|
|
31118
|
+
if (consumers.some((consumer) => consumer.inputName === "negative")) {
|
|
31119
|
+
return "negative_prompt";
|
|
31120
|
+
}
|
|
31121
|
+
return "prompt_text";
|
|
31122
|
+
}
|
|
31123
|
+
switch (inputName) {
|
|
31124
|
+
case "seed":
|
|
31125
|
+
return "seed";
|
|
31126
|
+
case "steps":
|
|
31127
|
+
return "steps";
|
|
31128
|
+
case "cfg":
|
|
31129
|
+
return "cfg";
|
|
31130
|
+
case "sampler_name":
|
|
31131
|
+
return "sampler";
|
|
31132
|
+
case "scheduler":
|
|
31133
|
+
return "scheduler";
|
|
31134
|
+
case "denoise":
|
|
31135
|
+
return "denoise";
|
|
31136
|
+
case "width":
|
|
31137
|
+
return "width";
|
|
31138
|
+
case "height":
|
|
31139
|
+
return "height";
|
|
31140
|
+
case "batch_size":
|
|
31141
|
+
return "batch_size";
|
|
31142
|
+
case "ckpt_name":
|
|
31143
|
+
return "checkpoint";
|
|
31144
|
+
case "filename_prefix":
|
|
31145
|
+
return "filename_prefix";
|
|
31146
|
+
case "text":
|
|
31147
|
+
return "text";
|
|
31148
|
+
default:
|
|
31149
|
+
return void 0;
|
|
31150
|
+
}
|
|
31151
|
+
}
|
|
31152
|
+
function formatValuePreview(value) {
|
|
31153
|
+
if (typeof value === "string") {
|
|
31154
|
+
return value.length > 100 ? `${value.slice(0, 97)}...` : value;
|
|
31155
|
+
}
|
|
30897
31156
|
try {
|
|
30898
|
-
|
|
30899
|
-
|
|
31157
|
+
const serialized = JSON.stringify(value);
|
|
31158
|
+
if (!serialized) {
|
|
31159
|
+
return String(value);
|
|
31160
|
+
}
|
|
31161
|
+
return serialized.length > 100 ? `${serialized.slice(0, 97)}...` : serialized;
|
|
30900
31162
|
} catch {
|
|
30901
|
-
|
|
31163
|
+
return String(value);
|
|
31164
|
+
}
|
|
31165
|
+
}
|
|
31166
|
+
function analyzeEditableInputs(prompt) {
|
|
31167
|
+
const consumerMap = /* @__PURE__ */ new Map();
|
|
31168
|
+
for (const [nodeId, rawNode] of Object.entries(prompt)) {
|
|
31169
|
+
if (!isRecord(rawNode) || !isRecord(rawNode.inputs)) {
|
|
31170
|
+
continue;
|
|
31171
|
+
}
|
|
31172
|
+
const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
|
|
31173
|
+
if (!classType || ["Note", "MarkdownNote", "PrimitiveNode"].includes(classType) || nodeId === "meta") {
|
|
31174
|
+
continue;
|
|
31175
|
+
}
|
|
31176
|
+
for (const [inputName, value] of Object.entries(rawNode.inputs)) {
|
|
31177
|
+
if (!isConnectionValue(value)) {
|
|
31178
|
+
continue;
|
|
31179
|
+
}
|
|
31180
|
+
const sourceNodeId = String(value[0]);
|
|
31181
|
+
const consumers = consumerMap.get(sourceNodeId) ?? [];
|
|
31182
|
+
consumers.push({
|
|
31183
|
+
inputName,
|
|
31184
|
+
classType,
|
|
31185
|
+
nodeId
|
|
31186
|
+
});
|
|
31187
|
+
consumerMap.set(sourceNodeId, consumers);
|
|
31188
|
+
}
|
|
31189
|
+
}
|
|
31190
|
+
const editableInputs = [];
|
|
31191
|
+
for (const [nodeId, rawNode] of Object.entries(prompt)) {
|
|
31192
|
+
if (!isRecord(rawNode) || !isRecord(rawNode.inputs)) {
|
|
31193
|
+
continue;
|
|
31194
|
+
}
|
|
31195
|
+
const classType = typeof rawNode.class_type === "string" ? rawNode.class_type : "";
|
|
31196
|
+
if (!classType || ["Note", "MarkdownNote", "PrimitiveNode"].includes(classType) || nodeId === "meta") {
|
|
31197
|
+
continue;
|
|
31198
|
+
}
|
|
31199
|
+
for (const [inputName, value] of Object.entries(rawNode.inputs)) {
|
|
31200
|
+
if (isConnectionValue(value)) {
|
|
31201
|
+
continue;
|
|
31202
|
+
}
|
|
31203
|
+
editableInputs.push({
|
|
31204
|
+
path: `${nodeId}.inputs.${inputName}`,
|
|
31205
|
+
node_id: nodeId,
|
|
31206
|
+
class_type: classType,
|
|
31207
|
+
input_name: inputName,
|
|
31208
|
+
value,
|
|
31209
|
+
value_preview: formatValuePreview(value),
|
|
31210
|
+
role: inferEditableRole(
|
|
31211
|
+
nodeId,
|
|
31212
|
+
classType,
|
|
31213
|
+
inputName,
|
|
31214
|
+
consumerMap
|
|
31215
|
+
)
|
|
31216
|
+
});
|
|
31217
|
+
}
|
|
31218
|
+
}
|
|
31219
|
+
return editableInputs.sort(
|
|
31220
|
+
(left, right) => left.path.localeCompare(right.path, void 0, { numeric: true })
|
|
31221
|
+
);
|
|
31222
|
+
}
|
|
31223
|
+
function formatEditableInputsText(editableInputs) {
|
|
31224
|
+
if (editableInputs.length === 0) {
|
|
31225
|
+
return "No literal editable inputs were detected.";
|
|
31226
|
+
}
|
|
31227
|
+
return formatListItems(editableInputs, (entry) => {
|
|
31228
|
+
const role = entry.role ? ` [${entry.role}]` : "";
|
|
31229
|
+
const classType = entry.class_type ? ` (${entry.class_type})` : "";
|
|
31230
|
+
return `${entry.path}${role}${classType}: ${entry.value_preview}`;
|
|
31231
|
+
});
|
|
31232
|
+
}
|
|
31233
|
+
function deriveSavedWorkflowFileName(sourcePath, requestedName) {
|
|
31234
|
+
if (requestedName) {
|
|
31235
|
+
return normalizeWorkflowFileName(requestedName);
|
|
31236
|
+
}
|
|
31237
|
+
if (sourcePath) {
|
|
31238
|
+
return normalizeWorkflowFileName(path.basename(sourcePath));
|
|
30902
31239
|
}
|
|
31240
|
+
return `workflow-${Date.now()}.json`;
|
|
30903
31241
|
}
|
|
30904
31242
|
function applyOverrides(prompt, overrides) {
|
|
30905
31243
|
for (const [key, value] of Object.entries(overrides)) {
|
|
@@ -30908,7 +31246,10 @@ function applyOverrides(prompt, overrides) {
|
|
|
30908
31246
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
30909
31247
|
const part = parts[i];
|
|
30910
31248
|
if (typeof current[part] !== "object" || current[part] === null) {
|
|
30911
|
-
|
|
31249
|
+
throw new WorkflowInputError(
|
|
31250
|
+
`Invalid override path: '${key}'. The segment '${part}' does not exist or is not an object. Please check the editable inputs using 'comfyui_get_workflow' to find the correct path.`,
|
|
31251
|
+
"overrides"
|
|
31252
|
+
);
|
|
30912
31253
|
}
|
|
30913
31254
|
current = current[part];
|
|
30914
31255
|
}
|
|
@@ -30937,6 +31278,20 @@ var WorkflowRunSchema = z3.object({
|
|
|
30937
31278
|
await: z3.boolean().default(true).describe("Wait for completion and return native images."),
|
|
30938
31279
|
timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
|
|
30939
31280
|
});
|
|
31281
|
+
var GetWorkflowSchema = z3.object({
|
|
31282
|
+
workflow_file_path: z3.string().optional().describe("Workflow path or filename to inspect."),
|
|
31283
|
+
workflow_id: z3.string().optional().describe("Workflow identifier returned by discovery."),
|
|
31284
|
+
include_prompt: z3.boolean().default(false).describe("Include normalized API JSON in structuredContent.")
|
|
31285
|
+
});
|
|
31286
|
+
var SaveWorkflowSchema = z3.object({
|
|
31287
|
+
workflow: z3.unknown().optional().describe("Workflow JSON object to save locally."),
|
|
31288
|
+
workflow_file_path: z3.string().optional().describe("Existing workflow path or filename to save."),
|
|
31289
|
+
workflow_id: z3.string().optional().describe("Existing workflow identifier to save."),
|
|
31290
|
+
overrides: z3.record(z3.string(), z3.unknown()).optional().describe("Dot-notation overrides to persist before saving."),
|
|
31291
|
+
output_file_name: z3.string().optional().describe("Filename to write locally. (default: anchored to ./)"),
|
|
31292
|
+
overwrite: z3.boolean().default(false).describe("Replace an existing local file when true."),
|
|
31293
|
+
include_prompt: z3.boolean().default(false).describe("Include saved API JSON in structuredContent.")
|
|
31294
|
+
});
|
|
30940
31295
|
var WaitForWorkflowSchema = z3.object({
|
|
30941
31296
|
prompt_id: z3.string().describe("The prompt_id of the submitted workflow."),
|
|
30942
31297
|
timeout: z3.number().int().min(1).default(120).describe("Wait timeout in seconds.")
|
|
@@ -30948,8 +31303,11 @@ async function handleInspectNode(args) {
|
|
|
30948
31303
|
);
|
|
30949
31304
|
if (args.node_class) {
|
|
30950
31305
|
const node = rawInfo[args.node_class];
|
|
30951
|
-
if (!node)
|
|
30952
|
-
return createNotFoundError(
|
|
31306
|
+
if (!node) {
|
|
31307
|
+
return createNotFoundError(
|
|
31308
|
+
`Node class '${args.node_class}' not found. Use 'comfyui_inspect_node' without arguments to see all available nodes or use the 'query' parameter to search for it.`
|
|
31309
|
+
);
|
|
31310
|
+
}
|
|
30953
31311
|
return {
|
|
30954
31312
|
content: [
|
|
30955
31313
|
{ type: "text", text: JSON.stringify(node, null, 2) }
|
|
@@ -30999,19 +31357,7 @@ function getWorkflowId(absolutePath) {
|
|
|
30999
31357
|
return crypto.createHash("sha256").update(absolutePath).digest("hex").substring(0, 8);
|
|
31000
31358
|
}
|
|
31001
31359
|
async function handleDiscoverWorkflows(args) {
|
|
31002
|
-
const pathsToScan =
|
|
31003
|
-
path.join(
|
|
31004
|
-
homedir(),
|
|
31005
|
-
"comfy",
|
|
31006
|
-
"ComfyUI",
|
|
31007
|
-
"user",
|
|
31008
|
-
"default",
|
|
31009
|
-
"workflows"
|
|
31010
|
-
),
|
|
31011
|
-
path.join(homedir(), ".fre4x-comfyui", "workflows"),
|
|
31012
|
-
path.join(process.cwd(), "workflows")
|
|
31013
|
-
];
|
|
31014
|
-
if (args.directory_path) pathsToScan.push(args.directory_path);
|
|
31360
|
+
const pathsToScan = getWorkflowDirectories(args.directory_path);
|
|
31015
31361
|
const foundWorkflows = [];
|
|
31016
31362
|
for (const dir of pathsToScan) {
|
|
31017
31363
|
try {
|
|
@@ -31055,13 +31401,14 @@ async function handleDiscoverWorkflows(args) {
|
|
|
31055
31401
|
}
|
|
31056
31402
|
}
|
|
31057
31403
|
if (foundWorkflows.length === 0) {
|
|
31404
|
+
const text2 = `No workflows found in searched directories.
|
|
31405
|
+
|
|
31406
|
+
Searched paths:
|
|
31407
|
+
${pathsToScan.map((p) => `- ${p}`).join("\n")}
|
|
31408
|
+
|
|
31409
|
+
Hint: If your workflows are stored elsewhere, provide a custom path using the 'directory_path' parameter.`;
|
|
31058
31410
|
return {
|
|
31059
|
-
content: [
|
|
31060
|
-
{
|
|
31061
|
-
type: "text",
|
|
31062
|
-
text: `No workflows found in searched directories.`
|
|
31063
|
-
}
|
|
31064
|
-
],
|
|
31411
|
+
content: [{ type: "text", text: text2 }],
|
|
31065
31412
|
structuredContent: {
|
|
31066
31413
|
workflows: [],
|
|
31067
31414
|
searched_paths: pathsToScan
|
|
@@ -31116,6 +31463,7 @@ async function handleWaitForWorkflow(args) {
|
|
|
31116
31463
|
args.timeout
|
|
31117
31464
|
);
|
|
31118
31465
|
const outputsRecord = historyEntry.outputs || {};
|
|
31466
|
+
const workspaceOutputs = [];
|
|
31119
31467
|
const content = [];
|
|
31120
31468
|
let summaryText = `Workflow ${args.prompt_id} completed.
|
|
31121
31469
|
`;
|
|
@@ -31123,157 +31471,194 @@ async function handleWaitForWorkflow(args) {
|
|
|
31123
31471
|
if (output && Array.isArray(output.images)) {
|
|
31124
31472
|
for (const image of output.images) {
|
|
31125
31473
|
if (image.filename && image.type) {
|
|
31126
|
-
|
|
31127
|
-
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31131
|
-
|
|
31132
|
-
|
|
31133
|
-
|
|
31134
|
-
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
summaryText += `Fetched image: ${image.filename}
|
|
31474
|
+
try {
|
|
31475
|
+
const asset = IS_MOCK ? createMockImageAsset() : await fetchImageAsset(
|
|
31476
|
+
image.filename,
|
|
31477
|
+
image.type,
|
|
31478
|
+
image.subfolder
|
|
31479
|
+
);
|
|
31480
|
+
content.push({
|
|
31481
|
+
type: "image",
|
|
31482
|
+
data: asset.buffer.toString("base64"),
|
|
31483
|
+
mimeType: asset.mimeType
|
|
31484
|
+
});
|
|
31485
|
+
summaryText += `Fetched image: ${image.filename}
|
|
31139
31486
|
`;
|
|
31140
|
-
|
|
31141
|
-
|
|
31487
|
+
const summary = await saveToWorkspace(
|
|
31488
|
+
args.prompt_id,
|
|
31489
|
+
image,
|
|
31490
|
+
asset
|
|
31491
|
+
);
|
|
31492
|
+
if (summary) {
|
|
31493
|
+
workspaceOutputs.push(summary);
|
|
31494
|
+
summaryText += `Copied image to workspace: ${summary.relative_path}
|
|
31142
31495
|
`;
|
|
31143
31496
|
}
|
|
31144
|
-
}
|
|
31145
|
-
summaryText += `
|
|
31497
|
+
} catch (e) {
|
|
31498
|
+
summaryText += `Failed to fetch image ${image.filename}: ${String(e)}
|
|
31146
31499
|
`;
|
|
31147
31500
|
}
|
|
31148
31501
|
}
|
|
31149
31502
|
}
|
|
31150
31503
|
}
|
|
31151
31504
|
}
|
|
31505
|
+
if (IS_MOCK && outputsRecord) {
|
|
31506
|
+
for (const [, output] of Object.entries(outputsRecord)) {
|
|
31507
|
+
if (output && Array.isArray(output.images)) {
|
|
31508
|
+
for (const image of output.images) {
|
|
31509
|
+
summaryText += `Mock image: ${image.filename}
|
|
31510
|
+
`;
|
|
31511
|
+
}
|
|
31512
|
+
}
|
|
31513
|
+
}
|
|
31514
|
+
}
|
|
31152
31515
|
content.unshift({ type: "text", text: summaryText });
|
|
31153
31516
|
return {
|
|
31154
31517
|
content,
|
|
31155
|
-
structuredContent:
|
|
31518
|
+
structuredContent: {
|
|
31519
|
+
...historyEntry,
|
|
31520
|
+
workspace_outputs: workspaceOutputs
|
|
31521
|
+
}
|
|
31156
31522
|
};
|
|
31157
31523
|
} catch (error48) {
|
|
31158
31524
|
return handleComfyError(error48);
|
|
31159
31525
|
}
|
|
31160
31526
|
}
|
|
31161
|
-
async function
|
|
31527
|
+
async function handleGetWorkflow(args) {
|
|
31162
31528
|
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
|
-
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
|
-
}
|
|
31529
|
+
const loadedWorkflow = await loadWorkflow(args);
|
|
31530
|
+
const editableInputs = analyzeEditableInputs(loadedWorkflow.prompt);
|
|
31531
|
+
const workflowId = getWorkflowId(loadedWorkflow.targetPath);
|
|
31532
|
+
const text = [
|
|
31533
|
+
`Workflow ${workflowId} loaded from ${loadedWorkflow.targetPath}.`,
|
|
31534
|
+
loadedWorkflow.sourceFormat === "webui" ? "Source format: Web UI (normalized to API format)." : "Source format: API format.",
|
|
31535
|
+
`Editable inputs (${editableInputs.length}):`,
|
|
31536
|
+
formatEditableInputsText(editableInputs)
|
|
31537
|
+
].join("\n");
|
|
31538
|
+
const structuredContent = {
|
|
31539
|
+
workflow_id: workflowId,
|
|
31540
|
+
path: loadedWorkflow.targetPath,
|
|
31541
|
+
source_format: loadedWorkflow.sourceFormat,
|
|
31542
|
+
editable_inputs: editableInputs
|
|
31543
|
+
};
|
|
31544
|
+
if (args.include_prompt) {
|
|
31545
|
+
const filteredPrompt = {};
|
|
31546
|
+
for (const [nodeId, node] of Object.entries(
|
|
31547
|
+
loadedWorkflow.prompt
|
|
31548
|
+
)) {
|
|
31549
|
+
if (isRecord(node)) {
|
|
31550
|
+
const classType = typeof node.class_type === "string" ? node.class_type : "";
|
|
31551
|
+
if (classType && !["Note", "MarkdownNote", "PrimitiveNode"].includes(
|
|
31552
|
+
classType
|
|
31553
|
+
) && nodeId !== "meta") {
|
|
31554
|
+
filteredPrompt[nodeId] = node;
|
|
31239
31555
|
}
|
|
31240
|
-
if (found) break;
|
|
31241
|
-
} catch (_e) {
|
|
31242
31556
|
}
|
|
31243
31557
|
}
|
|
31244
|
-
|
|
31245
|
-
|
|
31246
|
-
|
|
31558
|
+
structuredContent.prompt = filteredPrompt;
|
|
31559
|
+
}
|
|
31560
|
+
return {
|
|
31561
|
+
content: [{ type: "text", text }],
|
|
31562
|
+
structuredContent
|
|
31563
|
+
};
|
|
31564
|
+
} catch (error48) {
|
|
31565
|
+
return handleComfyError(error48);
|
|
31566
|
+
}
|
|
31567
|
+
}
|
|
31568
|
+
async function handleSaveWorkflow(args) {
|
|
31569
|
+
try {
|
|
31570
|
+
let prompt;
|
|
31571
|
+
let sourcePath;
|
|
31572
|
+
if (args.workflow !== void 0) {
|
|
31573
|
+
const normalized = await normalizeWorkflowPrompt(args.workflow);
|
|
31574
|
+
prompt = normalized.prompt;
|
|
31247
31575
|
} else {
|
|
31248
|
-
|
|
31249
|
-
|
|
31576
|
+
if (!args.workflow_file_path && !args.workflow_id) {
|
|
31577
|
+
return createValidationError(
|
|
31578
|
+
"workflow",
|
|
31579
|
+
"Provide workflow JSON or an existing workflow reference."
|
|
31580
|
+
);
|
|
31581
|
+
}
|
|
31582
|
+
const loadedWorkflow = await loadWorkflow(args);
|
|
31583
|
+
prompt = loadedWorkflow.prompt;
|
|
31584
|
+
sourcePath = loadedWorkflow.targetPath;
|
|
31585
|
+
}
|
|
31586
|
+
if (args.overrides) {
|
|
31587
|
+
applyOverrides(prompt, args.overrides);
|
|
31588
|
+
}
|
|
31589
|
+
const outputFileName = deriveSavedWorkflowFileName(
|
|
31590
|
+
sourcePath,
|
|
31591
|
+
args.output_file_name
|
|
31592
|
+
);
|
|
31593
|
+
if (outputFileName.includes("..")) {
|
|
31594
|
+
return createValidationError(
|
|
31595
|
+
"output_file_name",
|
|
31596
|
+
"Directory traversal sequences (..) are not allowed."
|
|
31250
31597
|
);
|
|
31251
31598
|
}
|
|
31252
|
-
|
|
31599
|
+
const outputPath = path.resolve(process.cwd(), outputFileName);
|
|
31600
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
31601
|
+
let existedBeforeSave = false;
|
|
31253
31602
|
try {
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
);
|
|
31603
|
+
await fs.access(outputPath);
|
|
31604
|
+
existedBeforeSave = true;
|
|
31605
|
+
} catch {
|
|
31606
|
+
existedBeforeSave = false;
|
|
31259
31607
|
}
|
|
31260
|
-
if (
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
} else {
|
|
31265
|
-
objectInfo = await fetchComfyJson(
|
|
31266
|
-
"/object_info"
|
|
31267
|
-
);
|
|
31268
|
-
}
|
|
31269
|
-
promptObj = convertWebUIToAPI(
|
|
31270
|
-
promptObj,
|
|
31271
|
-
objectInfo
|
|
31608
|
+
if (existedBeforeSave && !args.overwrite) {
|
|
31609
|
+
return createValidationError(
|
|
31610
|
+
"output_file_name",
|
|
31611
|
+
`File already exists: ${outputFileName}. Set overwrite=true to replace it.`
|
|
31272
31612
|
);
|
|
31273
31613
|
}
|
|
31274
|
-
|
|
31275
|
-
|
|
31614
|
+
await fs.writeFile(
|
|
31615
|
+
outputPath,
|
|
31616
|
+
`${JSON.stringify(prompt, null, 2)}
|
|
31617
|
+
`,
|
|
31618
|
+
"utf8"
|
|
31619
|
+
);
|
|
31620
|
+
const editableInputs = analyzeEditableInputs(prompt);
|
|
31621
|
+
const workflowId = getWorkflowId(outputPath);
|
|
31622
|
+
const structuredContent = {
|
|
31623
|
+
workflow_id: workflowId,
|
|
31624
|
+
path: outputPath,
|
|
31625
|
+
source_path: sourcePath,
|
|
31626
|
+
overwritten: existedBeforeSave,
|
|
31627
|
+
editable_inputs: editableInputs
|
|
31628
|
+
};
|
|
31629
|
+
if (args.include_prompt) {
|
|
31630
|
+
const filteredPrompt = {};
|
|
31631
|
+
for (const [nodeId, node] of Object.entries(prompt)) {
|
|
31632
|
+
if (isRecord(node)) {
|
|
31633
|
+
const classType = typeof node.class_type === "string" ? node.class_type : "";
|
|
31634
|
+
if (classType && !["Note", "MarkdownNote", "PrimitiveNode"].includes(
|
|
31635
|
+
classType
|
|
31636
|
+
) && nodeId !== "meta") {
|
|
31637
|
+
filteredPrompt[nodeId] = node;
|
|
31638
|
+
}
|
|
31639
|
+
}
|
|
31640
|
+
}
|
|
31641
|
+
structuredContent.prompt = filteredPrompt;
|
|
31276
31642
|
}
|
|
31643
|
+
const persistedChanges = args.overrides && Object.keys(args.overrides).length > 0 ? `Persisted ${Object.keys(args.overrides).length} override(s).` : "No overrides were applied.";
|
|
31644
|
+
const text = [
|
|
31645
|
+
`Saved workflow ${workflowId} to ${outputPath}.`,
|
|
31646
|
+
persistedChanges,
|
|
31647
|
+
`Editable inputs (${editableInputs.length}):`,
|
|
31648
|
+
formatEditableInputsText(editableInputs)
|
|
31649
|
+
].join("\n");
|
|
31650
|
+
return {
|
|
31651
|
+
content: [{ type: "text", text }],
|
|
31652
|
+
structuredContent
|
|
31653
|
+
};
|
|
31654
|
+
} catch (error48) {
|
|
31655
|
+
return handleComfyError(error48);
|
|
31656
|
+
}
|
|
31657
|
+
}
|
|
31658
|
+
async function handleWorkflowRun(args) {
|
|
31659
|
+
try {
|
|
31660
|
+
const { prompt } = await loadWorkflow(args);
|
|
31661
|
+
const promptObj = { ...prompt };
|
|
31277
31662
|
if (args.overrides) {
|
|
31278
31663
|
applyOverrides(promptObj, args.overrides);
|
|
31279
31664
|
}
|
|
@@ -31307,7 +31692,9 @@ async function handleWorkflowRun(args) {
|
|
|
31307
31692
|
content: [
|
|
31308
31693
|
{
|
|
31309
31694
|
type: "text",
|
|
31310
|
-
text: `Workflow submitted. prompt_id: ${result.prompt_id}, queue: ${result.number}
|
|
31695
|
+
text: `Workflow submitted. prompt_id: ${result.prompt_id}, queue: ${result.number}.
|
|
31696
|
+
|
|
31697
|
+
To retrieve the final results or check its status later, use the 'comfyui_wait_for_workflow' tool with this prompt_id.`
|
|
31311
31698
|
}
|
|
31312
31699
|
],
|
|
31313
31700
|
structuredContent: result
|
|
@@ -31339,6 +31726,18 @@ function createServer() {
|
|
|
31339
31726
|
WorkflowRunSchema.shape,
|
|
31340
31727
|
handleWorkflowRun
|
|
31341
31728
|
);
|
|
31729
|
+
server.tool(
|
|
31730
|
+
"comfyui_get_workflow",
|
|
31731
|
+
"Inspect a stored workflow and list editable inputs.",
|
|
31732
|
+
GetWorkflowSchema.shape,
|
|
31733
|
+
handleGetWorkflow
|
|
31734
|
+
);
|
|
31735
|
+
server.tool(
|
|
31736
|
+
"comfyui_save_workflow",
|
|
31737
|
+
"Save a workflow into ./workflows with optional edits.",
|
|
31738
|
+
SaveWorkflowSchema.shape,
|
|
31739
|
+
handleSaveWorkflow
|
|
31740
|
+
);
|
|
31342
31741
|
server.tool(
|
|
31343
31742
|
"comfyui_wait_for_workflow",
|
|
31344
31743
|
"Wait for a submitted workflow to finish and fetch its images.",
|