@metabrain-labs/comfyui-mcp-server 1.0.0

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 (88) hide show
  1. package/.env +179 -0
  2. package/.github/workflows/ci.yml +63 -0
  3. package/.husky/pre-commit +11 -0
  4. package/LICENSE +21 -0
  5. package/README-en.md +635 -0
  6. package/README.md +656 -0
  7. package/__tests__/services/task/execution.test.ts +272 -0
  8. package/__tests__/services/task/wait.test.ts +220 -0
  9. package/__tests__/types/result.test.ts +227 -0
  10. package/__tests__/utils/mcp-helpers.test.ts +137 -0
  11. package/__tests__/utils/special-node-handler.test.ts +186 -0
  12. package/comfyui-mcp-server-skill/SKILL.md +43 -0
  13. package/comfyui-mcp-server-skill/references/api-json.md +323 -0
  14. package/comfyui-mcp-server-skill/references/catalog.md +251 -0
  15. package/comfyui-mcp-server-skill/rules/api-json.md +94 -0
  16. package/comfyui-mcp-server-skill/rules/catalog.md +93 -0
  17. package/comfyui-mcp-server-skill/rules/comfyui.md +158 -0
  18. package/configs/inspector/dev.json +11 -0
  19. package/configs/inspector/prod.json +9 -0
  20. package/docs/en/content/public/inspector-example.png +0 -0
  21. package/docs/en/content/public/workflow_name_example.png +0 -0
  22. package/docs/en/content/public/workflow_parameter_example.png +0 -0
  23. package/docs/en/md/Project-Advantages.md +75 -0
  24. package/docs/zh-CN/content/public/inspector-example.png +0 -0
  25. package/docs/zh-CN/content/public/workflow_name_example.png +0 -0
  26. package/docs/zh-CN/content/public/workflow_parameter_example.png +0 -0
  27. package/docs/zh-CN/md/why-us.md +51 -0
  28. package/example.json +26 -0
  29. package/jest.config.js +45 -0
  30. package/locales/en.json +150 -0
  31. package/locales/zh.json +150 -0
  32. package/package.json +68 -0
  33. package/src/api/api.ts +123 -0
  34. package/src/api/http.ts +70 -0
  35. package/src/constants/common.ts +38 -0
  36. package/src/constants/index.ts +1 -0
  37. package/src/hooks/websocket.ts +93 -0
  38. package/src/i18n.ts +40 -0
  39. package/src/index.ts +268 -0
  40. package/src/scripts/comfy-ui/list-history.ts +31 -0
  41. package/src/scripts/comfy-ui/run-ws.ts +23 -0
  42. package/src/scripts/ws/send-feature-flags.ts +23 -0
  43. package/src/server-stdio.ts +23 -0
  44. package/src/services/business.ts +194 -0
  45. package/src/services/dynamic-tool.ts +303 -0
  46. package/src/services/index.ts +19 -0
  47. package/src/services/storage/asset-storage.ts +48 -0
  48. package/src/services/storage/index.ts +2 -0
  49. package/src/services/storage/workflow-storage.ts +192 -0
  50. package/src/services/task/execution.ts +69 -0
  51. package/src/services/task/fetch.ts +131 -0
  52. package/src/services/task/index.ts +3 -0
  53. package/src/services/task/wait.ts +294 -0
  54. package/src/services/workflow/executor.ts +134 -0
  55. package/src/services/workflow/index.ts +1 -0
  56. package/src/tools/handlers/core-manual.ts +28 -0
  57. package/src/tools/handlers/index.ts +22 -0
  58. package/src/tools/handlers/interrupt-prompt.ts +35 -0
  59. package/src/tools/handlers/list-models.ts +51 -0
  60. package/src/tools/handlers/mount-workflow.ts +88 -0
  61. package/src/tools/handlers/prompt-result.ts +35 -0
  62. package/src/tools/handlers/prompts.ts +61 -0
  63. package/src/tools/handlers/queue-custom-prompt.ts +164 -0
  64. package/src/tools/handlers/queue-prompt.ts +170 -0
  65. package/src/tools/handlers/save-custom-workflow.ts +67 -0
  66. package/src/tools/handlers/save-task-assets.ts +82 -0
  67. package/src/tools/handlers/system-status.ts +30 -0
  68. package/src/tools/handlers/task-detail.ts +35 -0
  69. package/src/tools/handlers/tool-status-map.ts +33 -0
  70. package/src/tools/handlers/upload-assets.ts +82 -0
  71. package/src/tools/handlers/workflow-api.ts +46 -0
  72. package/src/tools/handlers/workflows-catalog.ts +79 -0
  73. package/src/tools/index.ts +101 -0
  74. package/src/types/common.ts +74 -0
  75. package/src/types/dynamic-tool.ts +85 -0
  76. package/src/types/enums/result.ts +29 -0
  77. package/src/types/execute.ts +24 -0
  78. package/src/types/object-info.ts +43 -0
  79. package/src/types/result.ts +126 -0
  80. package/src/types/task.ts +118 -0
  81. package/src/types/workflow.ts +111 -0
  82. package/src/types/ws.ts +80 -0
  83. package/src/utils/format.ts +134 -0
  84. package/src/utils/mcp-helpers.ts +110 -0
  85. package/src/utils/special-node-handler.ts +36 -0
  86. package/src/utils/workflow-converter.ts +140 -0
  87. package/src/utils/ws.ts +219 -0
  88. package/tsconfig.json +18 -0
@@ -0,0 +1,170 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import i18n from "../../i18n";
5
+ import {
6
+ deleteDynamicTool,
7
+ executeDynamicWorkflowTool,
8
+ getAllDynamicTools,
9
+ getDynamicTool,
10
+ } from "../../services/dynamic-tool";
11
+ import {
12
+ executeWorkflowTaskByPrompts,
13
+ ExecutionProgress,
14
+ } from "../../services/task/execution";
15
+ import { waitForExecutionCompletion } from "../../services/task/wait";
16
+ import { error, errorWithDetail, ok } from "../../types/result";
17
+ import { buildComfyViewUrls } from "../../utils/mcp-helpers";
18
+ import { ResultToMcpResponse, withMcpErrorHandling } from "../../utils/mcp-helpers";
19
+ import { ComfyClient } from "../../utils/ws";
20
+
21
+ export function registerQueuePrompt(server: McpServer, client: ComfyClient) {
22
+ server.registerTool(
23
+ "queue_prompt",
24
+ {
25
+ title: i18n.t("tool.queue_prompt.title"),
26
+ description: i18n.t("tool.queue_prompt.description"),
27
+ inputSchema: {
28
+ workflowName: z
29
+ .string()
30
+ .describe(i18n.t("tool.queue_prompt.inputSchema.workflowName")),
31
+ isAsync: z
32
+ .boolean()
33
+ .default(false)
34
+ .describe(i18n.t("tool.queue_prompt.inputSchema.isAsync")),
35
+ params: z
36
+ .record(z.string(), z.any())
37
+ .optional()
38
+ .describe(i18n.t("tool.queue_prompt.inputSchema.params")),
39
+ },
40
+ },
41
+ withMcpErrorHandling(
42
+ async ({ workflowName, isAsync, params = {} }, extra) => {
43
+ const startTime = Date.now();
44
+
45
+ const tool = getDynamicTool(workflowName);
46
+ if (!tool) {
47
+ return ResultToMcpResponse(
48
+ errorWithDetail(
49
+ i18n.t("error.toolNotAlreadyExistError", { workflowName }),
50
+ `availableTools: ${getAllDynamicTools().map((t) => t.name)}`,
51
+ ),
52
+ );
53
+ }
54
+
55
+ if (!client.isConnected()) {
56
+ await client.connect();
57
+ }
58
+ if (!client.isConnected()) {
59
+ throw new McpError(ErrorCode.InternalError, "WebSocket NOT CONNECTED");
60
+ }
61
+
62
+ const execResult = await executeDynamicWorkflowTool(
63
+ workflowName,
64
+ params,
65
+ client,
66
+ );
67
+
68
+ const clientId = client.getClientId();
69
+ const submitResult = await executeWorkflowTaskByPrompts({
70
+ prompts: execResult,
71
+ clientId,
72
+ });
73
+
74
+ console.error(`[工作流已提交] Prompt ID: ${submitResult.prompt_id}`);
75
+
76
+ if (!isAsync) {
77
+ const progressToken = extra?._meta?.progressToken as
78
+ | number
79
+ | string
80
+ | undefined;
81
+
82
+ if (progressToken) {
83
+ console.error(`[进度通知] 已启用,Token: ${progressToken}`);
84
+ } else {
85
+ console.error(`[进度通知] 未启用(AGENT 未传入 progressToken)`);
86
+ }
87
+
88
+ const progressInterval = 2000;
89
+ let lastProgressTime = 0;
90
+
91
+ const onProgress = async (progress: ExecutionProgress) => {
92
+ const now = Date.now();
93
+ if (progressToken && now - lastProgressTime >= progressInterval) {
94
+ lastProgressTime = now;
95
+ const progressValue =
96
+ progress.percent !== undefined
97
+ ? progress.percent / 100
98
+ : progress.stage === "completed"
99
+ ? 1
100
+ : 0.5;
101
+
102
+ try {
103
+ await extra?.sendNotification?.({
104
+ method: "notifications/progress",
105
+ params: {
106
+ progressToken,
107
+ progress: progressValue,
108
+ total: 1,
109
+ message: progress.message,
110
+ },
111
+ });
112
+ console.error(
113
+ `[进度通知] Token: ${progressToken}, Progress: ${(progressValue * 100).toFixed(1)}%`,
114
+ );
115
+ } catch (err) {
116
+ console.error(`[进度通知] 发送失败:`, err);
117
+ }
118
+ }
119
+ };
120
+
121
+ const executionResult = await waitForExecutionCompletion({
122
+ client,
123
+ promptId: submitResult.prompt_id,
124
+ timeout: 10 * 60 * 1000,
125
+ onProgress,
126
+ });
127
+
128
+ if (!executionResult.success) {
129
+ deleteDynamicTool(workflowName);
130
+ return ResultToMcpResponse(
131
+ errorWithDetail(
132
+ i18n.t("error.workflowExecuteFail", {
133
+ error: executionResult.error,
134
+ }),
135
+ `Prompt ID: ${submitResult.prompt_id}`,
136
+ ),
137
+ );
138
+ }
139
+
140
+ const executionTime = Date.now() - startTime;
141
+ return ResultToMcpResponse(
142
+ ok(
143
+ i18n.t("tool.queue_prompt.success"),
144
+ {
145
+ promptId: submitResult.prompt_id,
146
+ img: buildComfyViewUrls(executionResult),
147
+ outputs: executionResult.outputs,
148
+ },
149
+ { action: "queue_prompt" },
150
+ executionTime,
151
+ ),
152
+ );
153
+ }
154
+
155
+ const executionTime = Date.now() - startTime;
156
+ return ResultToMcpResponse(
157
+ ok(
158
+ i18n.t("tool.queue_prompt.success"),
159
+ {
160
+ promptId: submitResult.prompt_id,
161
+ description: i18n.t("tool.queue_prompt.asyncSupplement"),
162
+ },
163
+ { action: "queue_prompt" },
164
+ executionTime,
165
+ ),
166
+ );
167
+ },
168
+ ),
169
+ );
170
+ }
@@ -0,0 +1,67 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import i18n from "../../i18n";
4
+ import { collectAndSaveFormatTaskFromExternal } from "../../services";
5
+ import { error, ok } from "../../types/result";
6
+ import {
7
+ ResultToMcpResponse,
8
+ withMcpErrorHandling,
9
+ } from "../../utils/mcp-helpers";
10
+ import { ComfyClient } from "../../utils/ws";
11
+
12
+ export function registerSaveCustomWorkflow(
13
+ server: McpServer,
14
+ client: ComfyClient,
15
+ ) {
16
+ server.registerTool(
17
+ "save_custom_workflow",
18
+ {
19
+ title: i18n.t("tool.save_custom_workflow.title"),
20
+ description: i18n.t("tool.save_custom_workflow.description"),
21
+ inputSchema: {
22
+ filename: z
23
+ .string()
24
+ .describe(i18n.t("tool.save_custom_workflow.inputSchema.filename")),
25
+ apiJson: z
26
+ .record(z.string(), z.any())
27
+ .describe(i18n.t("tool.save_custom_workflow.inputSchema.apiJson")),
28
+ },
29
+ },
30
+ withMcpErrorHandling(async ({ filename, apiJson }) => {
31
+ const startTime = Date.now();
32
+
33
+ const fileInfo = filename.split(".");
34
+ if (fileInfo.length > 1 && !filename.endsWith(".json")) {
35
+ return ResultToMcpResponse(
36
+ error(
37
+ i18n.t("error.hasExtendName", {
38
+ current: "." + fileInfo[1],
39
+ need: ".json",
40
+ }),
41
+ ),
42
+ );
43
+ }
44
+
45
+ if (!filename.endsWith(".json")) {
46
+ filename += ".json";
47
+ }
48
+
49
+ const { filePath, promptId } = await collectAndSaveFormatTaskFromExternal(
50
+ filename,
51
+ apiJson,
52
+ client,
53
+ );
54
+
55
+ const executionTime = Date.now() - startTime;
56
+
57
+ return ResultToMcpResponse(
58
+ ok(
59
+ i18n.t("tool.save_custom_workflow.success", { filePath }),
60
+ { filePath, promptId },
61
+ { action: "save_custom_workflow" },
62
+ executionTime,
63
+ ),
64
+ );
65
+ }),
66
+ );
67
+ }
@@ -0,0 +1,82 @@
1
+ import fs from "fs";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { COMMON } from "../../constants";
5
+ import i18n from "../../i18n";
6
+ import { saveAssetsByPromptId } from "../../services";
7
+ import { error, ok } from "../../types/result";
8
+ import {
9
+ ResultToMcpResponse,
10
+ withMcpErrorHandling,
11
+ } from "../../utils/mcp-helpers";
12
+
13
+ export function registerSaveTaskAssets(server: McpServer) {
14
+ server.registerTool(
15
+ "save_task_assets",
16
+ {
17
+ title: i18n.t("tool.save_task_assets.title"),
18
+ description: i18n.t("tool.save_task_assets.description"),
19
+ inputSchema: {
20
+ promptId: z
21
+ .string()
22
+ .describe(i18n.t("tool.save_task_assets.inputSchema.promptId")),
23
+ destinationDir: z
24
+ .string()
25
+ .optional()
26
+ .describe(i18n.t("tool.save_task_assets.inputSchema.destinationDir")),
27
+ overwrite: z
28
+ .boolean()
29
+ .optional()
30
+ .default(false)
31
+ .describe(i18n.t("tool.save_task_assets.inputSchema.overwrite")),
32
+ },
33
+ },
34
+ withMcpErrorHandling(async ({ promptId, destinationDir, overwrite }) => {
35
+ const startTime = Date.now();
36
+
37
+ if (destinationDir) {
38
+ if (!fs.existsSync(destinationDir)) {
39
+ return ResultToMcpResponse(
40
+ error(i18n.t("error.dirNotExist", { destinationDir })),
41
+ );
42
+ }
43
+
44
+ const stat = fs.statSync(destinationDir);
45
+ if (!stat.isDirectory()) {
46
+ return ResultToMcpResponse(
47
+ error(i18n.t("error.notDir", { destinationDir })),
48
+ );
49
+ }
50
+
51
+ try {
52
+ fs.accessSync(destinationDir, fs.constants.W_OK);
53
+ } catch {
54
+ return ResultToMcpResponse(
55
+ error(i18n.t("error.notWritableDir", { destinationDir })),
56
+ );
57
+ }
58
+ } else {
59
+ if (!fs.existsSync(COMMON.ASSETS_DIR)) {
60
+ fs.mkdirSync(COMMON.ASSETS_DIR, { recursive: true });
61
+ }
62
+ }
63
+
64
+ const { assetsNames, filePath } = await saveAssetsByPromptId(
65
+ promptId,
66
+ overwrite,
67
+ destinationDir,
68
+ );
69
+
70
+ const executionTime = Date.now() - startTime;
71
+
72
+ return ResultToMcpResponse(
73
+ ok(
74
+ i18n.t("tool.save_task_assets.success", { assetsNames, filePath }),
75
+ {},
76
+ { action: "save_task_assets" },
77
+ executionTime,
78
+ ),
79
+ );
80
+ }),
81
+ );
82
+ }
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { api } from "../../api/api";
3
+ import i18n from "../../i18n";
4
+ import { ok } from "../../types/result";
5
+ import { ResultToMcpResponse, withMcpErrorHandling } from "../../utils/mcp-helpers";
6
+
7
+ export function registerGetSystemStatus(server: McpServer) {
8
+ server.registerTool(
9
+ "get_system_status",
10
+ {
11
+ title: i18n.t("tool.get_system_status.title"),
12
+ description: i18n.t("tool.get_system_status.description"),
13
+ inputSchema: {},
14
+ },
15
+ withMcpErrorHandling(async () => {
16
+ const startTime = Date.now();
17
+ const result = await api.getSystemStatus();
18
+ const executionTime = Date.now() - startTime;
19
+
20
+ return ResultToMcpResponse(
21
+ ok(
22
+ i18n.t("tool.get_system_status.success"),
23
+ result,
24
+ { action: "get_system_status" },
25
+ executionTime,
26
+ ),
27
+ );
28
+ }),
29
+ );
30
+ }
@@ -0,0 +1,35 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import i18n from "../../i18n";
4
+ import { getTaskDetailByPromptId } from "../../services";
5
+ import { ok } from "../../types/result";
6
+ import { ResultToMcpResponse, withMcpErrorHandling } from "../../utils/mcp-helpers";
7
+
8
+ export function registerGetTaskDetail(server: McpServer) {
9
+ server.registerTool(
10
+ "cui_get_task_detail",
11
+ {
12
+ title: i18n.t("tool.cui_get_task_detail.title"),
13
+ description: i18n.t("tool.cui_get_task_detail.description"),
14
+ inputSchema: {
15
+ promptId: z
16
+ .string()
17
+ .describe(i18n.t("tool.cui_get_task_detail.inputSchema.promptId")),
18
+ },
19
+ },
20
+ withMcpErrorHandling(async ({ promptId }) => {
21
+ const startTime = Date.now();
22
+ const result = await getTaskDetailByPromptId({ promptId });
23
+ const executionTime = Date.now() - startTime;
24
+
25
+ return ResultToMcpResponse(
26
+ ok(
27
+ i18n.t("tool.cui_get_task_detail.success"),
28
+ result,
29
+ { action: "cui_get_task_detail" },
30
+ executionTime,
31
+ ),
32
+ );
33
+ }),
34
+ );
35
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 工具与 inspection_status 的映射关系
3
+ * key: ToolName
4
+ * value: 该Tool归属的inspection_status列表,一个Tool可归属多个inspection_status
5
+ */
6
+
7
+ import { SourceType } from "../../types/common";
8
+
9
+ export const toolInspectionStatusMap: Record<string, SourceType[]> = {
10
+ // 基础核心工具
11
+ get_core_manual: ["External", "InitialInspection", "CompleteInspection"],
12
+
13
+ // 工作流目录相关
14
+ get_workflows_catalog: ["InitialInspection", "CompleteInspection"],
15
+ mount_workflow: ["External", "InitialInspection", "CompleteInspection"],
16
+ get_workflow_api: ["External", "InitialInspection", "CompleteInspection"],
17
+
18
+ // 任务执行相关
19
+ queue_prompt: ["External", "InitialInspection", "CompleteInspection"],
20
+ queue_custom_prompt: [], // 不可被任何inspection_status使用
21
+ interrupt_prompt: ["External", "InitialInspection", "CompleteInspection"],
22
+ get_prompt_result: ["External", "InitialInspection", "CompleteInspection"],
23
+ get_task_detail: ["External", "InitialInspection", "CompleteInspection"],
24
+
25
+ // 资产与存储相关
26
+ save_custom_workflow: ["External"], // 只有External可用
27
+ save_task_assets: ["External", "InitialInspection", "CompleteInspection"],
28
+ upload_assets: ["External", "InitialInspection", "CompleteInspection"],
29
+
30
+ // 系统工具
31
+ get_system_status: ["External", "InitialInspection", "CompleteInspection"],
32
+ list_models: ["External", "InitialInspection", "CompleteInspection"],
33
+ };
@@ -0,0 +1,82 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { z } from "zod";
5
+ import { api } from "../../api/api";
6
+ import i18n from "../../i18n";
7
+ import { error, ok } from "../../types/result";
8
+ import { ResultToMcpResponse, withMcpErrorHandling } from "../../utils/mcp-helpers";
9
+
10
+ export function registerUploadAssets(server: McpServer) {
11
+ server.registerTool(
12
+ "upload_assets",
13
+ {
14
+ title: i18n.t("tool.upload_assets.title"),
15
+ description: i18n.t("tool.upload_assets.description"),
16
+ inputSchema: {
17
+ fileSource: z
18
+ .string()
19
+ .describe(i18n.t("tool.upload_assets.inputSchema.fileSource")),
20
+ mimeType: z
21
+ .string()
22
+ .optional()
23
+ .describe(i18n.t("tool.upload_assets.inputSchema.mimeType")),
24
+ },
25
+ },
26
+ withMcpErrorHandling(async ({ fileSource, mimeType }) => {
27
+ try {
28
+ const startTime = Date.now();
29
+ let buffer;
30
+ let finalFileName = "uploaded_image.png";
31
+ const isUrl = /^https?:\/\//i.test(fileSource);
32
+
33
+ if (isUrl) {
34
+ const res = await fetch(fileSource);
35
+ if (!res.ok) {
36
+ return ResultToMcpResponse(
37
+ error(
38
+ i18n.t("error.downloadAssetsFail", { status: res.status }),
39
+ ),
40
+ );
41
+ }
42
+ const arrayBuffer = await res.arrayBuffer();
43
+ buffer = Buffer.from(arrayBuffer);
44
+ const urlWithoutQuery = fileSource.split("?")[0];
45
+ finalFileName = urlWithoutQuery.split("/").pop() || "downloaded.png";
46
+ } else {
47
+ if (!fs.existsSync(fileSource)) {
48
+ return ResultToMcpResponse(
49
+ error(i18n.t("error.fileNotExistError", { fileSource })),
50
+ );
51
+ }
52
+ buffer = fs.readFileSync(fileSource);
53
+ finalFileName = path.basename(fileSource);
54
+ }
55
+
56
+ const form = new FormData();
57
+ const blob = new Blob([buffer], {
58
+ type: mimeType || "application/octet-stream",
59
+ });
60
+ form.append("image", blob, finalFileName);
61
+
62
+ const response = await api.uploadImg(form);
63
+ const executionTime = Date.now() - startTime;
64
+
65
+ return ResultToMcpResponse(
66
+ ok(
67
+ i18n.t("tool.upload_assets.success", { finalFileName }),
68
+ response,
69
+ { action: "cui_upload_file" },
70
+ executionTime,
71
+ ),
72
+ );
73
+ } catch (err: any) {
74
+ return ResultToMcpResponse(
75
+ error(
76
+ i18n.t("error.uploadFail", { message: err.message }),
77
+ ),
78
+ );
79
+ }
80
+ }),
81
+ );
82
+ }
@@ -0,0 +1,46 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { api } from "../../api/api";
4
+ import i18n from "../../i18n";
5
+ import { error, ok } from "../../types/result";
6
+ import {
7
+ ResultToMcpResponse,
8
+ withMcpErrorHandling,
9
+ } from "../../utils/mcp-helpers";
10
+
11
+ export function registerGetWorkflowAPI(server: McpServer) {
12
+ server.registerTool(
13
+ "get_workflow_API",
14
+ {
15
+ title: i18n.t("tool.get_workflow_API.title"),
16
+ description: i18n.t("tool.get_workflow_API.description"),
17
+ inputSchema: {
18
+ workflowPath: z
19
+ .string()
20
+ .describe(i18n.t("tool.get_workflow_API.inputSchema.workflowPath")),
21
+ },
22
+ },
23
+ withMcpErrorHandling(async ({ workflowPath }) => {
24
+ if (!workflowPath.endsWith(".json")) {
25
+ return ResultToMcpResponse(
26
+ error(i18n.t("error.getWorkflowFormatError", { workflowPath })),
27
+ );
28
+ }
29
+
30
+ const startTime = Date.now();
31
+ const result = await api.getDetailUserData(
32
+ encodeURIComponent(workflowPath),
33
+ );
34
+ const executionTime = Date.now() - startTime;
35
+
36
+ return ResultToMcpResponse(
37
+ ok(
38
+ i18n.t("tool.get_workflow_API.success"),
39
+ result,
40
+ { action: "get_workflow_API" },
41
+ executionTime,
42
+ ),
43
+ );
44
+ }),
45
+ );
46
+ }
@@ -0,0 +1,79 @@
1
+ import { readFile } from "fs/promises";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { COMMON } from "../../constants";
5
+ import i18n from "../../i18n";
6
+ import {
7
+ collectAndSaveFormatTask,
8
+ collectAndSaveFormatTaskFromWorkflows,
9
+ collectExternalWorkflowsFromDirectory,
10
+ saveWorkflow,
11
+ } from "../../services";
12
+ import { ComfyClient } from "../../utils/ws";
13
+ import { WorkflowConverter } from "../../utils/workflow-converter";
14
+ import {
15
+ ResultToMcpStringResponse,
16
+ withMcpErrorHandling,
17
+ } from "../../utils/mcp-helpers";
18
+
19
+ export function registerGetWorkflowsCatalog(
20
+ server: McpServer,
21
+ client: ComfyClient,
22
+ converter: WorkflowConverter,
23
+ ) {
24
+ server.registerTool(
25
+ "get_workflows_catalog",
26
+ {
27
+ title: i18n.t("tool.get_workflows_catalog.title"),
28
+ description: i18n.t("tool.get_workflows_catalog.description"),
29
+ inputSchema: {
30
+ maxItems: z
31
+ .number()
32
+ .min(1)
33
+ .max(10)
34
+ .optional()
35
+ .default(10)
36
+ .describe(i18n.t("tool.get_workflows_catalog.inputSchema.maxItems")),
37
+ useExistingCatalog: z
38
+ .boolean()
39
+ .optional()
40
+ .default(false)
41
+ .describe(i18n.t("tool.get_workflows_catalog.inputSchema.useExistingCatalog")),
42
+ },
43
+ },
44
+ withMcpErrorHandling(async ({ maxItems, useExistingCatalog }) => {
45
+ // 如果 useExistingCatalog 为 true,直接返回已有的 workflow.json
46
+ if (useExistingCatalog) {
47
+ const content = await readFile(COMMON.WORKFLOW_PATH, "utf-8");
48
+ return ResultToMcpStringResponse(content);
49
+ }
50
+
51
+ let hasMore: boolean = true;
52
+
53
+ // 1. 收集历史任务(CompleteInspection)
54
+ for (let i = 0; hasMore; i++) {
55
+ hasMore = await collectAndSaveFormatTask({
56
+ maxItems,
57
+ offset: i * maxItems,
58
+ append: i > 0,
59
+ });
60
+ }
61
+
62
+ // 2. 收集用户工作流(InitialInspection)
63
+ await collectAndSaveFormatTaskFromWorkflows(client, converter);
64
+
65
+ // 3. 收集本地 workflow 目录的 External 类型工作流
66
+ // 由于去重逻辑优先保留非 External 类型,External 只作为补充
67
+ const externalResult = await collectExternalWorkflowsFromDirectory();
68
+ const externalWorkflows = externalResult || [];
69
+
70
+ // 只有当有 External 工作流时才保存
71
+ if (externalWorkflows.length > 0) {
72
+ await saveWorkflow(externalWorkflows, { append: true });
73
+ }
74
+
75
+ const content = await readFile(COMMON.WORKFLOW_PATH, "utf-8");
76
+ return ResultToMcpStringResponse(content);
77
+ }),
78
+ );
79
+ }