dsh-comfyui-mcp 0.0.0-stage → 0.1.5
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/LICENSE +21 -0
- package/README.md +55 -2
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/assets/icon-prompt.txt +3 -0
- package/assets/icon.svg +1 -0
- package/client.js +1 -0
- package/configured-runtime.mjs +28 -0
- package/cordis.patch.yml +4 -0
- package/index.mjs +25 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +1 -6
- package/runtime.mjs +92 -0
- package/server.mjs +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-comfyui-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-comfyui-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
将 ComfyUI 接入 DSH,使用 DSH 官方 `@deepseek-ai/dsh-mcp-client` 注册 MCP 工具,并提供连接测试和可选的本地按需启动。
|
|
4
|
+
|
|
5
|
+
## 上游与职责
|
|
6
|
+
|
|
7
|
+
ComfyUI 工具来自 [artokun/comfyui-mcp](https://github.com/artokun/comfyui-mcp),本包固定使用 **0.52.206**。上游负责工具发现、工作流执行、图片读取等业务能力。本包负责 DSH 插件封装、配置界面、连接探测及自有进程的生命周期;不将上游实现视为本项目原创。
|
|
8
|
+
|
|
9
|
+
## 安装与配置
|
|
10
|
+
|
|
11
|
+
需要 Node.js 22 及兼容的 DSH(当前声明为 `>=0.2.1-rc.3 <0.3.0`)。在 DSH 插件管理器中安装 `dsh-comfyui-mcp`(指定版本可填 `dsh-comfyui-mcp@0.1.5`),然后启用组件。也支持安装本地目录或 `.tgz`。不要另外添加同名 MCP 桥。
|
|
12
|
+
|
|
13
|
+
打开「插件 → ComfyUI MCP」,点击「测试连接」探测现有服务。在同页下方的「运行配置」表单设置:
|
|
14
|
+
|
|
15
|
+
- `url`:默认 `http://127.0.0.1:8188`,支持已有 HTTP(S) 地址,不允许 URL 内嵌凭据。
|
|
16
|
+
- `autoStart`:默认关闭;开启后首次执行业务工具才允许启动本地 ComfyUI。
|
|
17
|
+
- `comfyuiDir`:包含 `main.py` 的绝对目录。
|
|
18
|
+
- `pythonPath`:已安装依赖的 Python 可执行文件绝对路径。
|
|
19
|
+
- `launchArgs`:额外参数数组,默认空。支持显存/精度选项、`--disable-xformers`、`--use-pytorch-cross-attention`、`--preview-method` 等白名单选项;拒绝监听地址、端口覆盖与参数缩写。
|
|
20
|
+
- `startupTimeoutMs`:默认 120000,范围 1000–600000 毫秒。
|
|
21
|
+
|
|
22
|
+
本地启动仅接受回环 HTTP 地址,固定禁用浏览器自动打开、自定义节点和 API 节点。已有远程实例不受这些启动参数影响。模型与 Python 环境需由用户事先准备,本插件不自动安装或更新它们。
|
|
23
|
+
|
|
24
|
+
## 使用
|
|
25
|
+
|
|
26
|
+
`list_tools` 和 `describe_tool` 只发现工具,不启动 ComfyUI 或加载模型。先读取工具 schema,再用 `call_tool` 执行业务工具。原生 Krea2 API 工作流使用上游 `enqueue_workflow`;模型名称和工作流须匹配现有 ComfyUI 安装。结果保留上游 MCP 内容,包括图片和本地保存路径。
|
|
27
|
+
|
|
28
|
+
`runtime` 提供 `status`、`test`、`start`、`stop`、`free`。状态查询和连接测试不启动进程。开启按需启动且服务未运行时,页面显示「未运行 · 按需启动待命」;执行生图工具时才会启动。生成完成后使用 `stop` 释放插件自有进程;`free` 请求当前连接实例卸载模型,可能影响该实例的其他使用者。
|
|
29
|
+
|
|
30
|
+
## 禁用、卸载与迁移
|
|
31
|
+
|
|
32
|
+
通过 DSH 插件管理器禁用、重新启用或卸载。禁用/卸载关闭本插件连接并停止本插件启动的 ComfyUI;不会停止外部进程,也不删除模型、工作流或生成图片。
|
|
33
|
+
|
|
34
|
+
旧部署迁移时,先停用旧 `mcp-local-comfyui`,再仅删除该条目及其停用覆盖,保留其他配置,然后安装本包。不要让两个 `serverName: comfyui` 桥同时运行。本包不自动改写用户配置。
|
|
35
|
+
|
|
36
|
+
## 开发与打包
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install --ignore-scripts --omit=optional --legacy-peer-deps
|
|
40
|
+
npm run build
|
|
41
|
+
npm test
|
|
42
|
+
npm run pack:check
|
|
43
|
+
npm pack
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
DSH 提供的 peer 包需在插件运行环境中可解析。安装发布包时由 DSH 管理依赖;任何待运行的依赖构建脚本都应先取得用户许可。包采用文件白名单,不包含模型、Python、工作流、生成图片、凭据或个人配置。图标是原创 SVG,设计提示词保存在 `assets/icon-prompt.txt`,使用 DSH 的 `./icon` 导出。
|
|
47
|
+
|
|
48
|
+
## 限制与安全
|
|
49
|
+
|
|
50
|
+
上游依赖审计发现 `undici` 的已知高风险通告;本次保留用户指定的上游版本,未自动升级。仅连接受信任的 ComfyUI 服务;回环默认值不能替代服务端访问控制。上游部分工具可以管理文件、模型或依赖,执行安装/更新类操作前必须取得用户明确许可。
|
|
51
|
+
|
|
52
|
+
自动测试覆盖配置保存、待命与故障状态、配置校验、不启动的探测、外部进程保护、并发启动与自有进程清理。已在 DSH Desktop 验证安装、重启后的配置页面、保存参数、本地启动及停止;Krea2 768 × 768 工作流执行成功,并通过 MCP 取回图片。插件管理器的卸载与重装操作已成功;真实运行期间禁用/卸载的完整清理验收尚未完成。真实进程测试需按测试文件中的环境变量显式启用,默认跳过。
|
|
53
|
+
|
|
54
|
+
## 许可证
|
|
55
|
+
|
|
56
|
+
本项目采用 MIT。上游 `comfyui-mcp` 为 MIT,版权归 Arthur R. Longbottom (artokun)。参见 `THIRD_PARTY_NOTICES.md`。依赖通过包管理器安装,不将上游依赖代码或模型整合进发布包;各依赖许可证继续适用。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## comfyui-mcp 0.52.206
|
|
4
|
+
|
|
5
|
+
Source: https://github.com/artokun/comfyui-mcp
|
|
6
|
+
|
|
7
|
+
MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Arthur R. Longbottom (artokun)
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
## Dependency distribution boundary
|
|
30
|
+
|
|
31
|
+
This tarball does not vendor node_modules, ComfyUI, Python, models or workflows.
|
|
32
|
+
Dependencies are resolved by the package manager and retain their own license
|
|
33
|
+
files. The MCP SDK is MIT. DSH integration packages are host-provided peers,
|
|
34
|
+
not copied DSH code. Repackaging those packages, Python environments or models
|
|
35
|
+
requires a separate review of the relevant licenses. Original SVG artwork in
|
|
36
|
+
this package uses this project's MIT license.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Original vector artwork, authored as SVG (not model-generated).
|
|
2
|
+
Design prompt: A compact ComfyUI-to-DSH bridge icon, rounded midnight-navy square, mint node path connecting two lavender endpoints, small white inference spark, clean legible geometry at 24px, no lettering, no upstream logo imitation.
|
|
3
|
+
License: MIT, same as this plugin. The canonical composition is embedded in design/plugin.html.
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#192b46"/><path d="M16 39V24h14v16h18V25" fill="none" stroke="#79e1d5" stroke-width="5" stroke-linejoin="round"/><circle cx="16" cy="24" r="5" fill="#a8b4ff"/><circle cx="48" cy="25" r="5" fill="#a8b4ff"/><path d="m30 19 4-7 4 7-4 7z" fill="#fff"/></svg>
|
package/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:'dsh-comfyui-mcp',factory(require){const React=require('react'),h=React.createElement;return {inject:['slots','locale'],apply(ctx){ctx.locale.register('dsh-comfyui-mcp','zh',{title:'连接与运行状态',test:'测试连接',loading:'正在测试…',idle:'尚未测试',online:'连接成功',offline:'未连接',standby:'未运行 · 按需启动待命',owned:'本插件进程',external:'外部进程',hint:'测试只探测地址,不启动 ComfyUI 或加载模型。请在下方运行配置中编辑参数。',safety:'禁用或卸载仅停止本插件进程;保留模型、工作流与图片。',credit:'工具由 artokun/comfyui-mcp 0.52.206 提供。'});ctx.locale.register('dsh-comfyui-mcp','en',{title:'Connection & runtime',test:'Test connection',loading:'Testing…',idle:'Not tested',online:'Connected',offline:'Offline',standby:'Not running · on-demand ready',owned:'Plugin-owned process',external:'External process',hint:'Testing never starts ComfyUI or loads models. Edit runtime parameters in the form below.',safety:'Disabling/uninstalling stops only plugin-owned processes; models, workflows and images are retained.',credit:'Tools powered by artokun/comfyui-mcp 0.52.206.'});const t=ctx.locale.bind('dsh-comfyui-mcp');function Status(){const [s,set]=React.useState(),[busy,setBusy]=React.useState(false);React.useSyncExternalStore(fn=>ctx.locale.subscribe(fn),()=>ctx.locale.getSnapshot());async function test(){setBusy(true);try{const r=await fetch('/api/dsh-comfyui/status',{cache:'no-store'});if(!r.ok)throw Error('HTTP '+r.status);set(await r.json());}catch(e){set({running:false,error:e.message});}finally{setBusy(false);}}const standby=!!s&&!s.running&&s.autoStart===true&&!s.owned&&!s.lastError&&s.error==='fetch failed';return h('section',{className:'dcm-status'},h('style',null,'.dcm-status{border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:20px;color:var(--dsw-alias-label-primary);font-size:14px}.dcm-status .dcm-row{display:flex;justify-content:space-between;align-items:center;gap:16px}.dcm-status .dcm-badge{font-size:12px;background:var(--dsw-alias-bg-layer-2);padding:6px 10px;border-radius:20px}.dcm-status h3{font-size:15px;margin:0 0 16px}.dcm-status p{line-height:1.7;color:var(--dsw-alias-label-secondary)}.dcm-status button{font:inherit;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:#536be8;color:white;cursor:pointer}.dcm-status pre{white-space:pre-wrap;overflow-wrap:anywhere}.dcm-status small{color:var(--dsw-alias-label-secondary)}'),h('div',{className:'dcm-row'},h('h3',null,t('title')),h('span',{className:'dcm-badge',role:'status'},busy?t('loading'):!s?t('idle'):s.running?t('online')+' · '+t(s.owned?'owned':'external'):t(standby?'standby':'offline'))),h('p',null,'工具发现不会启动 ComfyUI 或加载模型。'),s?.error&&!standby?h('pre',{role:'alert'},s.error):null,s?.lastError?h('pre',null,s.lastError):null,h('button',{onClick:test,disabled:busy},t(busy?'loading':'test')),h('p',null,t('hint')),h('p',null,t('safety')),h('small',null,t('credit')));}function ConfigPage({view,form}){const initial=form?.state?.value;const [draft,setDraft]=React.useState(null),[busy,setBusy]=React.useState(false),[message,setMessage]=React.useState('');const zh=ctx.locale.getLocale().id!=='en';const text=(a,b)=>zh?a:b;const fields=['url','autoStart','comfyuiDir','pythonPath','launchArgs','startupTimeoutMs'];const defaults={url:'http://127.0.0.1:8188',autoStart:false,comfyuiDir:'',pythonPath:'',launchArgs:[],startupTimeoutMs:120000};if(view==='summary')return text('ComfyUI 连接与本地按需启动参数','ComfyUI connection and local on-demand startup');if(!initial)return h('p',{role:'status'},text('配置尚未就绪或不可用','Configuration is loading or unavailable'));const values=draft||Object.fromEntries(fields.map(k=>[k,k==='launchArgs'?JSON.stringify(initial[k]??defaults[k]):initial[k]??defaults[k]]));async function save(e){e.preventDefault();setBusy(true);setMessage('');try{const v={...values,launchArgs:JSON.parse(values.launchArgs),startupTimeoutMs:Number(values.startupTimeoutMs)};if(!Array.isArray(v.launchArgs)||v.launchArgs.some(x=>typeof x!=='string'))throw Error('launchArgs must be a JSON string array');if(!Number.isFinite(v.startupTimeoutMs)||v.startupTimeoutMs<1000||v.startupTimeoutMs>600000)throw Error('startupTimeoutMs: 1000–600000');const ok=await form.mutate(fields.map(k=>({op:'set',path:[k],value:v[k]})),form.state.revision);setMessage(ok?text('配置已保存','Configuration saved'):text('保存失败或配置已变更,请重新打开页面后重试','Save failed or configuration changed; reopen this page and retry'));if(ok)setDraft(null);}catch(e){setMessage(e.message);}finally{setBusy(false);}}return h('form',{className:'dcm-config',onSubmit:save},h('style',null,'.dcm-config{max-width:760px;font-size:14px}.dcm-config label{display:block;margin:18px 0 6px}.dcm-config input,.dcm-config select,.dcm-config textarea{display:block;box-sizing:border-box;width:100%;padding:9px 12px;margin-top:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit}.dcm-config button{font:inherit;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:#536be8;color:white}'),...fields.map(k=>h('label',{key:k},text({url:'ComfyUI 地址',autoStart:'允许本地按需启动',comfyuiDir:'ComfyUI 目录',pythonPath:'Python 可执行文件',launchArgs:'额外启动参数',startupTimeoutMs:'启动超时(毫秒)'}[k],{url:'ComfyUI address',autoStart:'Allow local on-demand startup',comfyuiDir:'ComfyUI directory',pythonPath:'Python executable',launchArgs:'Extra launch arguments',startupTimeoutMs:'Startup timeout (ms)'}[k]),k==='autoStart'?h('select',{value:String(values[k]),disabled:busy||!form.state.writable,onChange:e=>setDraft({...values,[k]:e.target.value==='true'})},h('option',{value:'false'},text('关闭:仅连接现有服务','Off: existing service only')),h('option',{value:'true'},text('开启:首次执行工具时启动','On: start on first tool execution'))):h(k==='launchArgs'?'textarea':'input',{type:k==='startupTimeoutMs'?'number':'text',rows:k==='launchArgs'?3:undefined,placeholder:k==='comfyuiDir'?text('包含 main.py 的目录','Directory containing main.py'):k==='pythonPath'?text('Python 的完整路径','Full Python path'):undefined,value:values[k],disabled:busy||!form.state.writable,onChange:e=>setDraft({...values,[k]:e.target.value})}))),h('p',null,text('仅保存时更新配置;不会安装模型或 Python 依赖。启动参数为 JSON 数组。','Only Save updates configuration. No models or Python dependencies are installed. Launch arguments are a JSON array.')),h('button',{type:'submit',disabled:busy||!form.state.writable},busy?text('保存中…','Saving…'):text('保存配置','Save configuration')),h('p',{role:'status'},message));}function UnifiedPage({scope}){const state=React.useSyncExternalStore(fn=>scope.subscribe(fn),()=>scope.getSnapshot());return h('div',{className:'dcm-page',style:{maxWidth:760}},h(Status),h('section',{style:{border:'1px solid var(--dsw-alias-border-l1)',borderRadius:12,padding:20,marginTop:24}},h('h3',{style:{fontSize:15,margin:'0 0 16px'}},ctx.locale.getLocale().id==='en'?'Runtime configuration · DSH configuration form':'运行配置 · DSH 官方配置表单'),h('p',{style:{lineHeight:1.7,color:'var(--dsw-alias-label-secondary)'}},ctx.locale.getLocale().id==='en'?'This page uses DSH configuration snapshots, revisions and atomic saves.':'表单与连接状态位于同一插件详情页,使用 DSH 官方配置快照、修订号及原子保存操作。'),h(ConfigPage,{view:'page',form:{state,mutate:(ops,revision)=>scope.mutate(ops,revision)}})));}ctx.inject(['configForms'],c=>{const forms=c.configForms;c.effect(()=>forms.whileServed(['dsh-comfyui-mcp'],()=>c.slots.inject('plugins.bundle.config',()=>c.slots.register({name:'plugins.bundle.config',key:'dsh-comfyui-mcp'},()=>h(UnifiedPage,{scope:forms.get('dsh-comfyui-mcp')})))),'comfyui.config-page');});}};}});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRuntime, validateConfig } from './runtime.mjs';
|
|
2
|
+
|
|
3
|
+
export function createConfiguredRuntime(readConfig, factory = createRuntime) {
|
|
4
|
+
let runtime, signature, closed = false, pending = Promise.resolve();
|
|
5
|
+
function run(action) {
|
|
6
|
+
const task = pending.then(async () => {
|
|
7
|
+
if (closed) throw Error('Runtime is closing');
|
|
8
|
+
const config = validateConfig(readConfig());
|
|
9
|
+
const next = JSON.stringify(config);
|
|
10
|
+
if (signature !== next) {
|
|
11
|
+
await runtime?.close();
|
|
12
|
+
runtime = factory(config);
|
|
13
|
+
signature = next;
|
|
14
|
+
}
|
|
15
|
+
return action === 'config' ? runtime.config : runtime[action]();
|
|
16
|
+
});
|
|
17
|
+
pending = task.catch(() => {});
|
|
18
|
+
return task;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
run,
|
|
22
|
+
async close() {
|
|
23
|
+
closed = true;
|
|
24
|
+
await pending;
|
|
25
|
+
await runtime?.close();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/cordis.patch.yml
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import * as mcp from '@deepseek-ai/dsh-mcp-client';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createConfiguredRuntime } from './configured-runtime.mjs';
|
|
7
|
+
export const name='dsh-comfyui-mcp';
|
|
8
|
+
export const inject=['tools','webServer'];
|
|
9
|
+
export const Config=z.object({
|
|
10
|
+
url:z.string().default('http://127.0.0.1:8188').description('ComfyUI HTTP(S) address; default is loopback.'),
|
|
11
|
+
autoStart:z.boolean().default(false).description('Allow local on-demand startup. Discovery and connection tests never start ComfyUI.'),
|
|
12
|
+
comfyuiDir:z.string().default('').description('Absolute directory containing main.py. Required for local startup.'),
|
|
13
|
+
pythonPath:z.string().default('').description('Absolute Python executable path. No automatic dependency installation.'),
|
|
14
|
+
launchArgs:z.array(String).default([]).description('Additional ComfyUI arguments; network binding overrides are forbidden.'),
|
|
15
|
+
startupTimeoutMs:z.number().min(1000).max(600000).default(120000)
|
|
16
|
+
}).volatile();
|
|
17
|
+
export async function apply(ctx,config){
|
|
18
|
+
const runtime=createConfiguredRuntime(()=>config.get());const token=randomBytes(32).toString('hex');let disposed=false;
|
|
19
|
+
const control=createServer(async(req,res)=>{res.setHeader('Content-Type','application/json');try{if(req.method!=='POST'||req.headers.authorization!=='Bearer '+token){res.writeHead(403);return res.end('{}');}const action=req.url.slice(1);if(!['status','start','stop','free','config'].includes(action))throw Error('Unknown action');const value=await runtime.run(action);res.end(JSON.stringify(value));}catch(e){res.writeHead(500);res.end(JSON.stringify({error:e.message}));}});
|
|
20
|
+
await new Promise((resolve,reject)=>{control.once('error',reject);control.listen(0,'127.0.0.1',resolve);});
|
|
21
|
+
ctx.effect(()=>async()=>{disposed=true;await runtime.close();control.closeAllConnections();await new Promise(r=>control.close(r));},'comfyui.runtime');
|
|
22
|
+
ctx.effect(()=>ctx.webServer.register({kind:'exact',path:'/api/dsh-comfyui/status',handler:async(req,res)=>{res.setHeader('Content-Type','application/json');res.setHeader('Cache-Control','no-store');if(req.method!=='GET'){res.writeHead(405);return res.end('{}');}try{const s=await runtime.run('status');res.end(JSON.stringify(s));}catch(e){res.writeHead(500);res.end(JSON.stringify({error:e.message}));}}}),'comfyui.status-route');
|
|
23
|
+
await mcp.apply(ctx,mcp.Config({transport:'stdio',serverName:'comfyui',command:process.execPath,args:[fileURLToPath(new URL('./server.mjs',import.meta.url))],env:{ELECTRON_RUN_AS_NODE:'1',DSH_COMFY_CONTROL:'http://127.0.0.1:'+control.address().port,DSH_COMFY_TOKEN:token},toolCallTimeoutMs:1800000,failOnStartupError:true}));
|
|
24
|
+
if(disposed)throw Error('Plugin disposed during startup');
|
|
25
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"ComfyUI MCP","description":"Connect ComfyUI through the official MCP bridge with safe on-demand local startup. Tools powered by artokun/comfyui-mcp."}}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"ComfyUI MCP","description":"通过官方 MCP 桥连接 ComfyUI,支持安全的本地按需启动。工具由 artokun/comfyui-mcp 提供。"}}
|
package/package.json
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-comfyui-mcp",
|
|
3
|
-
"version": "0.0.0-stage",
|
|
4
|
-
"stub": true,
|
|
5
|
-
"description": "Temporary package placeholder for staged publishing"
|
|
6
|
-
}
|
|
1
|
+
{"name":"dsh-comfyui-mcp","version":"0.1.5","description":"ComfyUI connection and safe on-demand lifecycle for DSH; powered by artokun/comfyui-mcp.","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/Kaczev/dsh-comfyui-mcp.git"},"homepage":"https://github.com/Kaczev/dsh-comfyui-mcp#readme","bugs":{"url":"https://github.com/Kaczev/dsh-comfyui-mcp/issues"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"engines":{"node":">=22","dsh":">=0.2.1-rc.3 <0.3.0"},"exports":{".":"./index.mjs","./package.json":"./package.json","./client":"./client.js","./icon":"./assets/icon.svg","./locale/*.json":"./locale/*.json"},"files":["index.mjs","runtime.mjs","configured-runtime.mjs","server.mjs","client.js","cordis.patch.yml","assets","locale","LICENSE","THIRD_PARTY_NOTICES.md","README.md"],"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"platform":"web","immediately":true,"inject":["@deepseek-ai/dsh-client-ui-plugin-manager","@deepseek-ai/dsh-client-ui-settings"]}},"scripts":{"check":"node --check index.mjs && node --check runtime.mjs && node --check configured-runtime.mjs && node --check server.mjs && node --check client.js","test":"node --test test/*.test.mjs","build":"npm run check","pack:check":"npm pack --dry-run --json"},"dependencies":{"comfyui-mcp":"0.52.206","@modelcontextprotocol/sdk":"~1.32.1"},"peerDependencies":{"@deepseek-ai/dsh-mcp-client":"0.2.1-rc.3","@deepseek-ai/schemastery":"~3.18.5"}}
|
package/runtime.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
export function validateConfig(input = {}) {
|
|
7
|
+
const c = { url: 'http://127.0.0.1:8188', autoStart: false, comfyuiDir: '', pythonPath: '', launchArgs: [], startupTimeoutMs: 120000, ...input };
|
|
8
|
+
const u = new URL(c.url);
|
|
9
|
+
if (!['http:', 'https:'].includes(u.protocol) || u.username || u.password || u.search || u.hash) throw Error('Use an HTTP(S) address without credentials, query or fragment.');
|
|
10
|
+
c.url = u.href.endsWith('/') ? u.href.slice(0, -1) : u.href;
|
|
11
|
+
if (typeof c.autoStart !== 'boolean') throw Error('autoStart must be boolean');
|
|
12
|
+
if (!Array.isArray(c.launchArgs) || c.launchArgs.some(a => typeof a !== 'string')) throw Error('launchArgs must be an array of strings');
|
|
13
|
+
if (c.autoStart) {
|
|
14
|
+
if (!['127.0.0.1', 'localhost', '[::1]'].includes(u.hostname) || u.protocol !== 'http:' || u.pathname !== '/') throw Error('Local startup requires a loopback HTTP origin.');
|
|
15
|
+
if (!path.isAbsolute(c.comfyuiDir) || !path.isAbsolute(c.pythonPath)) throw Error('Local startup requires absolute ComfyUI and Python paths.');
|
|
16
|
+
}
|
|
17
|
+
// argparse accepts long-option abbreviations. Only explicitly supported non-network options are allowed.
|
|
18
|
+
const flags = new Set(['--use-pytorch-cross-attention', '--disable-xformers', '--lowvram', '--novram', '--highvram', '--normalvram', '--cpu', '--force-fp32', '--force-fp16', '--fp16-vae', '--fp32-vae', '--bf16-vae', '--cpu-vae', '--preview-method']);
|
|
19
|
+
for (let i = 0; i < c.launchArgs.length; i++) {
|
|
20
|
+
const a = c.launchArgs[i];
|
|
21
|
+
if (!flags.has(a)) throw Error('Unsupported launch argument: ' + a);
|
|
22
|
+
if (a === '--preview-method' && !['none', 'auto', 'latent2rgb', 'taesd'].includes(c.launchArgs[++i])) throw Error('Invalid preview method');
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isFinite(c.startupTimeoutMs) || c.startupTimeoutMs < 1000 || c.startupTimeoutMs > 600000) throw Error('Startup timeout must be 1000–600000 ms.');
|
|
25
|
+
return c;
|
|
26
|
+
}
|
|
27
|
+
export async function probe(url) {
|
|
28
|
+
try {
|
|
29
|
+
const r = await fetch(url + '/system_stats', { signal: AbortSignal.timeout(2500) });
|
|
30
|
+
if (!r.ok) throw Error('HTTP ' + r.status);
|
|
31
|
+
const body = await r.json();
|
|
32
|
+
if (!body.system && !body.devices) throw Error('Not a ComfyUI system_stats response');
|
|
33
|
+
return { running: true };
|
|
34
|
+
} catch (e) { return { running: false, error: e.message }; }
|
|
35
|
+
}
|
|
36
|
+
export function createRuntime(input, { spawnProcess = spawn, probeAddress = probe } = {}) {
|
|
37
|
+
const c = validateConfig(input);
|
|
38
|
+
let child, starting, closing = false, lastError;
|
|
39
|
+
const owned = () => !!child && child.exitCode === null && child.signalCode === null;
|
|
40
|
+
async function status() { return { ...await probeAddress(c.url), owned: owned(), pid: owned() ? child.pid : undefined, url: c.url, autoStart: c.autoStart, lastError }; }
|
|
41
|
+
async function waitExit(p, ms) {
|
|
42
|
+
if (p.exitCode !== null || p.signalCode !== null) return;
|
|
43
|
+
await new Promise(resolve => {
|
|
44
|
+
const done = () => { clearTimeout(timer); p.removeListener('exit', done); resolve(); };
|
|
45
|
+
const timer = setTimeout(done, ms);
|
|
46
|
+
p.once('exit', done);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function terminate() {
|
|
50
|
+
if (!owned()) return { stopped: false, reason: 'No owned process; external instances are never stopped.' };
|
|
51
|
+
const p = child;
|
|
52
|
+
p.kill(); await waitExit(p, 8000);
|
|
53
|
+
if (owned()) { p.kill('SIGKILL'); await waitExit(p, 2000); }
|
|
54
|
+
if (owned()) throw Error('Owned ComfyUI process could not be stopped: ' + p.pid);
|
|
55
|
+
return { stopped: true, pid: p.pid };
|
|
56
|
+
}
|
|
57
|
+
async function start() {
|
|
58
|
+
if (closing) throw Error('Runtime is closing');
|
|
59
|
+
if (starting) return starting;
|
|
60
|
+
starting = (async () => {
|
|
61
|
+
if ((await probeAddress(c.url)).running) return { running: true, started: false, owned: owned() };
|
|
62
|
+
if (!c.autoStart) throw Error('ComfyUI is offline; local on-demand startup is disabled.');
|
|
63
|
+
if (owned()) throw Error('Owned process exists but is not responding.');
|
|
64
|
+
const main = path.join(c.comfyuiDir, 'main.py');
|
|
65
|
+
fs.accessSync(main); fs.accessSync(c.pythonPath);
|
|
66
|
+
if (closing) throw Error('Runtime is closing');
|
|
67
|
+
const u = new URL(c.url);
|
|
68
|
+
child = spawnProcess(c.pythonPath, [main, '--listen', u.hostname === '[::1]' ? '::1' : '127.0.0.1', '--port', u.port || '80', '--disable-auto-launch', '--disable-all-custom-nodes', '--disable-api-nodes', ...c.launchArgs], { cwd: c.comfyuiDir, env: { ...process.env, PYTHONIOENCODING: 'utf-8' }, windowsHide: true, stdio: ['ignore', 'inherit', 'inherit'] });
|
|
69
|
+
let failure;
|
|
70
|
+
child.on('error', e => { failure = e; });
|
|
71
|
+
const end = Date.now() + c.startupTimeoutMs;
|
|
72
|
+
while (Date.now() < end) {
|
|
73
|
+
if (closing) throw Error('Runtime is closing');
|
|
74
|
+
if (failure) throw failure;
|
|
75
|
+
if (!owned()) throw Error('ComfyUI exited before becoming ready; inspect DSH stderr logs.');
|
|
76
|
+
if ((await probeAddress(c.url)).running) { lastError = undefined; return { running: true, started: true, owned: true, pid: child.pid }; }
|
|
77
|
+
await delay(500);
|
|
78
|
+
}
|
|
79
|
+
throw Error('ComfyUI startup timed out; inspect DSH stderr logs.');
|
|
80
|
+
})().catch(async e => { lastError = e.message; await terminate(); throw e; }).finally(() => { starting = undefined; });
|
|
81
|
+
return starting;
|
|
82
|
+
}
|
|
83
|
+
async function stop() { closing = true; try { await starting?.catch(() => {}); return await terminate(); } finally { closing = false; } }
|
|
84
|
+
async function close() { closing = true; await starting?.catch(() => {}); return terminate(); }
|
|
85
|
+
async function free() {
|
|
86
|
+
if (!(await status()).running) return { freed: false };
|
|
87
|
+
const r = await fetch(c.url + '/free', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ unload_models: true, free_memory: true }), signal: AbortSignal.timeout(10000) });
|
|
88
|
+
if (!r.ok) throw Error('Free failed: HTTP ' + r.status);
|
|
89
|
+
return { freed: true };
|
|
90
|
+
}
|
|
91
|
+
return { status, start, stop, close, free, config: c };
|
|
92
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
import { ListToolsRequestSchema,CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
const require=createRequire(import.meta.url);let backend,connecting,closing=false;
|
|
8
|
+
async function control(action){const r=await fetch(process.env.DSH_COMFY_CONTROL+'/'+action,{method:'POST',headers:{authorization:'Bearer '+process.env.DSH_COMFY_TOKEN},signal:AbortSignal.timeout(action==='start'?610000:15000)});const data=await r.json();if(!r.ok)throw Error(data.error);return data;}
|
|
9
|
+
let backendConfig;
|
|
10
|
+
async function connect(){const c=await control('config');const key=JSON.stringify(c);if(backend&&backendConfig!==key){await backend.close();backend=undefined;}if(backend)return backend;if(connecting)return connecting;return connecting=(async()=>{const client=new Client({name:'dsh-comfyui-upstream',version:'0.1.0'});const transport=new StdioClientTransport({command:process.execPath,args:[require.resolve('comfyui-mcp'),'--compact'],env:{...process.env,ELECTRON_RUN_AS_NODE:'1',COMFYUI_URL:c.url,COMFYUI_PATH:c.comfyuiDir||'',COMFYUI_PYTHON:c.pythonPath||''},stderr:'inherit'});try{await client.connect(transport);if(closing){await client.close();throw Error('Closing');}backend=client;backendConfig=key;return client;}catch(e){await transport.close();throw e;}})().finally(()=>connecting=undefined);}
|
|
11
|
+
const obj=properties=>({type:'object',properties,additionalProperties:false});const text=value=>({content:[{type:'text',text:JSON.stringify(value)}]});
|
|
12
|
+
const server=new Server({name:'dsh-comfyui-mcp',version:'0.1.0'},{capabilities:{tools:{}}});
|
|
13
|
+
server.setRequestHandler(ListToolsRequestSchema,async()=>({tools:[
|
|
14
|
+
{name:'runtime',description:'ComfyUI lifecycle and connection test. Status/test do not start ComfyUI; stop only terminates this plugin’s process. Stop after generation to release GPU/RAM.',inputSchema:obj({action:{type:'string',enum:['status','test','start','stop','free']}})},
|
|
15
|
+
{name:'list_tools',description:'Discover artokun/comfyui-mcp tools without starting ComfyUI or loading models.',inputSchema:obj({search:{type:'string'},category:{type:'string'}})},
|
|
16
|
+
{name:'describe_tool',description:'Read an upstream tool schema without starting ComfyUI.',inputSchema:{...obj({name:{type:'string'}}),required:['name']}},
|
|
17
|
+
{name:'call_tool',description:'Execute an upstream ComfyUI tool; starts local ComfyUI only when configured. Read describe_tool first. Use enqueue_workflow for native Krea2 API workflows. Never install/update models or Python dependencies without explicit user permission. Returns MCP content including images and saved paths.',inputSchema:{...obj({name:{type:'string'},args:{type:'object',additionalProperties:true}}),required:['name']}}
|
|
18
|
+
]}));
|
|
19
|
+
server.setRequestHandler(CallToolRequestSchema,async({params})=>{try{const a=params.arguments??{};if(params.name==='runtime')return text(await control(a.action==='test'?'status':a.action||'status'));if(!['list_tools','describe_tool','call_tool'].includes(params.name))throw Error('Unknown tool');if(params.name==='call_tool')await control('start');return await(await connect()).callTool({name:params.name,arguments:a},undefined,{timeout:1800000});}catch(e){return {isError:true,content:[{type:'text',text:e.message}]};}});
|
|
20
|
+
async function shutdown(){if(closing)return;closing=true;await connecting?.catch(()=>{});await backend?.close();await server.close();process.exit(0);}
|
|
21
|
+
process.on('SIGINT',shutdown);process.on('SIGTERM',shutdown);process.stdin.on('end',shutdown);
|
|
22
|
+
await server.connect(new StdioServerTransport());
|