diffusers-workflow 0.4.0__py3-none-any.whl

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 (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
dw/docs/MCP.md ADDED
@@ -0,0 +1,517 @@
1
+ # MCP Server
2
+
3
+ A fourth way to drive the engine, alongside `dw.run`, `dw.repl`, and
4
+ `dw.serve`: a stdio [MCP](https://modelcontextprotocol.io) server that lets
5
+ an MCP client — Claude Code first — author, validate, save, run and diagnose
6
+ workflows without shell access or a repo checkout.
7
+
8
+ `dw_mcp/` is an HTTP client of a **running** `dw.serve`. It owns no job
9
+ state and no GPU worker of its own; every tool call is a REST request
10
+ against the server described in [Server & Web UI](SERVER.md). If `dw.serve`
11
+ is not running, every tool fails with a message telling you to start it.
12
+
13
+ ## Install and run
14
+
15
+ ```bash
16
+ pip install -e ".[server,mcp]"
17
+ ```
18
+
19
+ The MCP server needs a running `dw.serve`, so two processes are involved:
20
+
21
+ ```bash
22
+ # terminal 1 - the engine. Leave it running.
23
+ dw-serve
24
+ ```
25
+
26
+ You do not start `dw-mcp` yourself. Your MCP client launches it on demand,
27
+ which is why the client needs a command it can actually find (see below).
28
+
29
+ `dw-mcp` (equivalently `python -m dw_mcp`) speaks MCP over stdio. Flags:
30
+
31
+ | Flag | Default | Meaning |
32
+ | --- | --- | --- |
33
+ | `--url` | `$DW_MCP_URL`, else `http://127.0.0.1:8765` | Base URL of the running `dw.serve` |
34
+ | `--token` | `$DW_API_TOKEN`, else none | Bearer token, when `dw.serve` was started with `--token` / `DW_API_TOKEN` - the same variable, so one export configures both ends |
35
+ | `--workspace` | `$DW_MCP_WORKSPACE`, else the server's default | Which of the server's workspaces the session works in. A *name* on the server, not a directory here - `DW_WORKSPACE` means something else to the engine. `use_workspace` switches it mid-session |
36
+ | `--timeout` | `30` | Seconds to wait on any one API request |
37
+ | `--no-probe` | off | Skip the startup `GET /api/health` that confirms the server is reachable and the token is accepted |
38
+
39
+ The `DW_MCP_URL` environment variable sets the same default the `--url` flag
40
+ overrides.
41
+
42
+ A non-loopback `--url` requires a token: `dw-mcp` exits 2 rather than start
43
+ without one. At startup it makes one `GET /api/health`, so a wrong URL or
44
+ token is reported once with a message instead of as a 401 on every tool
45
+ call; that probe is fatal for a remote URL and only a warning for a
46
+ loopback one (where it usually means `dw.serve` is not up yet).
47
+ [REMOTE.md](REMOTE.md) covers the remote setup end to end.
48
+
49
+ Claude Code users can add the composition skills as well:
50
+ `/plugin marketplace add dkackman/diffusers-workflow` then
51
+ `/plugin install dw@diffusers-workflow`. The plugin ships one skill per model
52
+ family (MiniMax H3, MiniMax Music 3, LTX-2.5) that picks a template for a
53
+ request's shape and states the family's rules - see
54
+ [plugins/dw/README.md](../plugins/dw/README.md), which also gives the
55
+ optional `npx skills add` lines for MiniMax's own prompt skills. It is
56
+ optional; every tool below works without it.
57
+
58
+ ### Use the absolute path to `dw-mcp`
59
+
60
+ **This is the one setup detail that reliably goes wrong.** If you installed
61
+ the way `install.sh` does, `dw-mcp` lives in the project's virtualenv and is
62
+ only on `PATH` while that venv is activated. Your MCP client is launched by
63
+ your shell, your desktop app, or your editor — usually *without* the venv
64
+ activated — so a bare `dw-mcp` fails to spawn:
65
+
66
+ ```
67
+ Failed to reconnect to dw: ENOENT
68
+ ```
69
+
70
+ Registering it once from an activated terminal hides this: that session
71
+ works, and the next one, started somewhere else, does not.
72
+
73
+ Always register the venv's absolute path. Console scripts have the
74
+ interpreter baked into their shebang, so they run correctly with no venv
75
+ activated — which is exactly why the absolute path is more robust than
76
+ telling people to activate first:
77
+
78
+ ```bash
79
+ echo "$(pwd)/venv/bin/dw-mcp" # the value to register
80
+ ```
81
+
82
+ ## Client configuration
83
+
84
+ ### Remote server, no local install
85
+
86
+ If `dw.serve` runs on another machine with `--mcp` (see
87
+ [REMOTE.md](REMOTE.md)), Claude Code connects to it directly:
88
+
89
+ claude mcp add --transport http dw http://<box>:8765/mcp \
90
+ --header "Authorization: Bearer <token>"
91
+
92
+ The same token fetches generated files: see step 7 of `The loop` in [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#the-loop).
93
+
94
+ Nothing from this repository is installed on the client. The stdio setup
95
+ below is for a machine that has its own `dw` install, and also works
96
+ against a remote `--url` with `--token`.
97
+
98
+ ### Claude Code
99
+
100
+ The CLI is the shortest path. From the project directory:
101
+
102
+ ```bash
103
+ claude mcp add dw -- "$(pwd)/venv/bin/dw-mcp"
104
+ ```
105
+
106
+ `--` separates Claude Code's own flags from the command it will spawn. Add
107
+ subprocess flags after it:
108
+
109
+ ```bash
110
+ claude mcp add dw -- "$(pwd)/venv/bin/dw-mcp" --url http://127.0.0.1:8791
111
+ ```
112
+
113
+ Pick the scope deliberately with `-s`:
114
+
115
+ | Scope | Stored in | Use when |
116
+ | --- | --- | --- |
117
+ | `local` (default) | `~/.claude.json`, keyed to this project | Just you, just this checkout |
118
+ | `user` | `~/.claude.json`, global | You want it in every project. The absolute path makes this work |
119
+ | `project` | `.mcp.json`, **committed to the repo** | You intend every clone to get it. Note an absolute path is machine-specific and will not port |
120
+
121
+ Equivalent hand-written `.mcp.json`, if you prefer a file:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "dw": {
127
+ "command": "/absolute/path/to/venv/bin/dw-mcp",
128
+ "args": ["--url", "http://127.0.0.1:8765"]
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ **A running session does not pick up a registration change** - start a new
135
+ one after adding or editing the server.
136
+
137
+ ### Claude Desktop
138
+
139
+ `claude_desktop_config.json`, same shape - and the same absolute-path rule,
140
+ which matters more here because a desktop app never inherits a shell's
141
+ `PATH`:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "dw": {
147
+ "command": "/absolute/path/to/venv/bin/dw-mcp",
148
+ "args": []
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ `DW_MCP_URL` can be set instead of `--url` via an `"env"` object alongside
155
+ `"command"`/`"args"` in either config.
156
+
157
+ ## Verify the setup
158
+
159
+ Three checks, in order. Each isolates a different failure, so run them in
160
+ sequence rather than jumping to the last one.
161
+
162
+ **1. The command launches without a venv.** This reproduces the environment
163
+ your client actually spawns it in, and is the check that catches ENOENT:
164
+
165
+ ```bash
166
+ env -i PATH=/usr/bin:/bin HOME="$HOME" /path/to/venv/bin/dw-mcp --help
167
+ ```
168
+
169
+ Prints usage and exits 0. If it does not, the path is wrong or the package
170
+ is not installed into that venv.
171
+
172
+ **2. The client sees the server.** Start a new Claude Code session and run
173
+ `/mcp`; `dw` should be listed and connected. From the shell,
174
+ `claude mcp list` and `claude mcp get dw` show the same thing.
175
+
176
+ Note that a "connected" status only means the process launched - it says
177
+ nothing about whether `dw.serve` is reachable.
178
+
179
+ **3. The tools reach the engine.** With `dw-serve` running, ask the client
180
+ something free, such as "list my diffusers workflows" (`list_workflows`) or
181
+ "check the diffusers-workflow server health" (`get_health`). A real answer
182
+ means the whole chain works. "Cannot reach diffusers-workflow at ..." means
183
+ step 3 failed while steps 1 and 2 passed - the client is fine and the engine
184
+ is not running.
185
+
186
+ Nothing in this sequence costs GPU time.
187
+
188
+ ## Tool reference
189
+
190
+ 59 tools in six groups. Names and arguments below are transcribed from
191
+ `dw_mcp/server.py` — nothing here is renamed or reshaped for the docs.
192
+
193
+ ### Catalog (read-only)
194
+
195
+ The catalog is large, so the server's instructions point a client at
196
+ `list_workflows` first: its listing carries enough about each workflow - a
197
+ one-line `summary`, its `shape` and `traits`, its measured `cost`, output
198
+ kinds and variable names - to pick one and know what to pass it, without
199
+ fetching every candidate's definition. Reusing a stored workflow is a
200
+ preference, not a rule; `run_workflow` still takes an `inline_workflow` for
201
+ a request nothing on disk covers.
202
+
203
+ A request usually names a *subject* ("a lego movie trailer set in the marvel
204
+ universe") while the catalog is written in *shapes* - a single image, an image
205
+ set, one shot, a multi-shot cut sequence, video with speech. Nothing in a
206
+ catalog entry will match the subject, so the shape is what has to be decided
207
+ first and matched against. `list_guides` indexes the engine's documentation by
208
+ section for exactly that, and `list_tasks` is what a shape gets composed from
209
+ when no single workflow covers it.
210
+
211
+ | Tool | Arguments | Purpose |
212
+ | --- | --- | --- |
213
+ | `list_guides()` | — | List the documentation the engine serves: each guide's name, what it covers, and its section headings. The index is the routing table - match a request's shape against a heading rather than guessing |
214
+ | `get_guide(name, section=None)` | `name`, `section` | Get one section of a guide - name it: a guide runs to thousands of lines, and WORKFLOW_GUIDE.md whole is ~19.6k tokens, more in one call than the whole tool surface costs to connect. Called with no `section` the answer is the guide's index: its opening, its first section, and `sections`/`withheld` naming the rest, with a `note` saying how to fetch one. Section names match loosely, so a heading copied approximately still resolves |
215
+ | `list_workflows(shape=None, traits=None, configures=None, include_models=False)` | `shape`, `traits`, `configures`, `include_models` | List stored workflows. Called with no filter at all each entry is cut to its `summary` and `shape` (`view: "summary"`, with a `note`), because the whole catalog in full detail is ~6.8k tokens for a question that is really "which shape do I want"; pass `shape` and the entries come back whole. Filtered, it is the server's compact view: each entry carries `summary`, `shape`, `traits`, `cost`, `kinds`, `variable_names`, `lists`, and `configures` only when set - `get_workflow` has the full description and definition. `lists`, present for a list-driven workflow, names the fields an entry of each list takes, the steps over it and the default's length; `cost` may carry `per_entry`, the measured cost of one entry so a run over a different-length list can be priced from it. `shape` keeps one of `image`, `image-set`, `image-edit`, `shot`, `sequence`, `audio`, `text`, `utility`; `traits` is comma-separated and every one listed must match (`has-audio`, `chained`, `image-conditioned`, `identity-referenced`, `needs-input-media`, `composes-workflows`); an unknown value in either is a 400 listing the vocabulary. Templates only by default - `configures=<template>` lists the checkpoint configs tuned for one, `include_models=true` lists them all. The first call to make for a request an existing workflow might cover |
216
+ | `get_workflow(name, variables_only=False)` | `name` | Get one stored workflow's full JSON definition. `variables_only=true` answers with just its variables and their defaults (long strings cut to 200 characters, the cut ones named in `truncated`, including strings inside a list default, named like `shots[0].prompt`) — the cheap way to confirm what a variable defaults to |
217
+ | `get_schema(section=None)` | `section` | Get the JSON schema every workflow definition must satisfy. Whole it is ~8.6k tokens, so name the part you need: `section` takes `steps`, `pipelines`, `tasks`, `result`, `variables` or `configuration` and answers `{section, sections, elsewhere, schema}` - `elsewhere` says which section holds each definition the fragment still `$ref`s. An unknown section is a 404 naming the ones there are |
218
+ | `list_pipelines()` | — | List every diffusers pipeline class this installation provides |
219
+ | `get_pipeline_signature(name)` | `name` | Get a pipeline's real call arguments |
220
+ | `list_classes(kind)` | `kind` | List class names of one kind: pipelines, models, schedulers, or quantization |
221
+ | `get_class(name, target="init")` | `name`, `target` (`init`\|`call`\|`load`) | Get a class's argument schema from the entry point a workflow reaches it by: `init` the constructor (quantization configs, schedulers), `call` a pipeline's `__call__`, `load` `from_pretrained` plus the curated loading knobs |
222
+ | `list_tasks()` | — | List every task command a workflow's task step can name |
223
+ | `get_task(command)` | `command` | Get a task command's argument schema |
224
+ | `list_models()` | — | List what the Hugging Face model cache holds, largest first |
225
+ | `get_memory()` | — | Get the worker's VRAM and RAM statistics. `gpu_*` is the card; `host_memory_rss_mb` / `host_memory_peak_rss_mb` are the worker process's resident and high-water host memory, beside the machine's `host_memory_total_mb` / `host_memory_available_mb` - read both, since an offloading workflow keeps its weights in host RAM and the card says little about what it holds (a host field is absent, not null, where the platform cannot measure it). `host_pinned_reserved_mb` / `host_pinned_allocated_mb`, when present, are torch's pinned-host cache - the staging buffers group offloading moves weights through, part of `host_memory_rss_mb` and invisible in every `gpu_*` figure, which is what a worker holding GB after releasing every model is usually holding (#98). `live: true` means `info` was measured now and is the worker's own memory; only live readings are comparable with each other. `live: false` with `info: null` (and `stale: false`) means nothing has been measured because nothing is resident; `live: false` with a populated `info` is a cached earlier reading, with `reason` (`job_running`, `worker_stopped`, `worker_busy`, `worker_unreachable`) and `age_seconds` - one cached while a job loads a model understates what is resident, so ask again when the server is idle rather than comparing it with a live figure |
226
+ | `get_health()` | — | Check that the server is alive, and which machine answered: `version`, `device`, whether a model process is currently resident (`worker_alive`), the job running now and the queue depth. `worker_alive: false` is the normal idle state on a server that has not run a job since startup or the last memory clear - not a fault - the on-demand worker starts with the next job (#206) |
227
+ | `get_server_info()` | — | What this installation can do and where it keeps things: `device` (the accelerator a run will use), `version`, the `workspace` this session is working in and the workflow/asset/output/prompt `directories` of *that* workspace, the bind address and port, whether a token is required, and whether MCP is mounted. Check the device before authoring - a CUDA-only choice (bitsandbytes, `torch.compile`, flash attention) is not available on an `mps` or `cpu` server. `runtime` (#222) reports the Python version, torch version and the CUDA version torch was built against, the NVIDIA driver version (when `nvidia-smi` is reachable), and the installed versions of diffusers, transformers, accelerate, bitsandbytes, peft, safetensors and sentencepiece (`null` for one not installed) - for diagnosing an environment mismatch between boxes without shelling in |
228
+ | `list_jobs(limit=20, status=None, workspace=None)` | optional `limit` (newest N), `status` (one state or a comma-separated set of `queued`, `running`, `succeeded`, `failed`, `cancelled`), `workspace` | List queued, running and recent jobs, **newest first**. Bounded by default: the unbounded listing was over a client's tool-result limit on a server with a few months of history, which made it a tool that could not be called at all. `total` says how many matched and `truncated`/`next` say so when the answer was cut - raise `limit` or narrow with `status`. Without `workspace`, a named workspace lists its own jobs and the default one lists every job the server holds |
229
+ | `list_gallery(limit=50, subfolder=None, only_orphans=False, workspace=None, folder=None, version=None)` | `limit`, `subfolder`, `only_orphans`, `workspace`, `folder`, `version` | List generated output files, newest first. A name is `<workflow>/<run id>/<file>`, where `<file>` may sit in the subfolder the step chose (`final/episode.mp4`); each entry carries `folder` (the workflow) and `subfolder` (by convention `final` or `intermediate`, `''` when the step chose none, any path the workflow wrote otherwise), and `subfolder="final"` lists only deliverables. Each entry carries `run_id` and `version` - that run's ordinal among the workflow's runs, which is how one of several runs that wrote the same basename is named to a person: the web UI labels the same file `v5`. The number is assigned when the run opens and never renumbered, so deleting a run leaves a gap rather than sliding the rest down (a failed run, or a rerun that reused every step, leaves one too - it took a number and may have nothing to list), and it is `null` under the flat output layout, which has no runs. `folder` with `version` lists that one run's files, and `output:<folder>/v5/<file>` names one in a workflow; every other tool takes `name`. Each entry also carries a ready-made `url`, already scoped to the workspace that made it - a hand-built `/outputs/<name>` URL 404s for anything but the default workspace. `only_orphans=True` inverts the call: instead of files, it returns run directories with no media anywhere under them (`runs`, each `{name, mtime}`) - a run whose output was deleted before `delete_output` could remove it by name, or one that failed before writing anything; `subfolder` does not apply in this mode, and `name` is exactly what `delete_output` accepts (#170). `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
230
+ | `get_gallery_metadata(name, envelope=False, workspace=None)` | `name`, `workspace` | Get the metadata embedded in a generated file — or, when `name` is an `asset:` reference, what an *input* asset holds (`source` says which; `job` is null for an asset). Reading an input's duration, frame count, fps and sample rate before a run is how a caller learns the `total_frames`, `fps` and `sample_rate` a workflow expects it to supply: the exact workflow and arguments that produced it, and, for audio/video, a `media` block (duration, rate, channels, fps, size, peak/mean dBFS). Only an image (PNG/JPEG/WebP) embeds `metadata` this way — it is always null for audio and video, and `next` then points at `get_job_workflow(job_id)` when `job` is known, or says a kept asset carries no provenance at all when it isn't. `envelope=true` adds `media.envelope` — `rms_dbfs` and `peak_dbfs` one entry per second — which is what locates something in a track rather than measuring the whole of it. `media.shots` is set on an output joined from shots: one `{name, start_frame, num_frames, start_sample, num_samples}` per shot, as the join measured them (see the run manifest in docs/WORKFLOW_GUIDE.md), else null. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
231
+
232
+ ### Media
233
+
234
+ | Tool | Arguments | Purpose |
235
+ | --- | --- | --- |
236
+ | `get_output_image(name, max_dimension=768, workspace=None, crop=None)` | `name`, `max_dimension`, `workspace`, `crop` | Look at a generated image, downscaled to `max_dimension` on its longest side. Returns the image plus a text part reporting `original_size`, `returned_size` and `bytes`, so a downscale is never silent. `crop` is `[x, y, width, height]` in the original's pixels, cut before the downscale and reported back clamped - the way to see a region of a 2K still at 100%, where the whole would be shrunk past what a small element or a decode-tiling seam can be judged at. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
237
+ | `get_output_audio(name, start=None, duration=None, workspace=None)` | `name`, `start`, `duration`, `workspace` | Listen to a generated soundtrack as base64 - an audio output, or the track muxed into a video (#193) - in its own encoding when served whole, WAV when extracted or excerpted. No downscale exists for audio, so a whole clip over the 4MB budget is refused rather than cut (#204); ask for the part instead with `start` and `duration` in seconds, and the text part names what was cut (`excerpt: 2.0s from 10.0s of 240.0s`) so a slice is never mistaken for the whole. `get_gallery_metadata`'s envelope says where in a track to look. `workspace` names the workspace for this one call without switching the session to it |
238
+ | `get_output_frames(name, at=None, seams=None, count=None, boundaries=None, names=None, max_dimension=512, hear=None, workspace=None)` | `name`, `at`, `seams`, `count`, `boundaries`, `names`, `max_dimension`, `hear`, `workspace` | See a generated video as frames, since there is no video content type over MCP (#193). One selector per call: `count` for an evenly spaced contact sheet, `at` for moments (seconds or `"frame:N"`), `seams` (true, or seam numbers from 1) for the last frame before and first frame after each join side by side (each pair carries `difference`, the mean pixel change across the join, 0-255 - rank seams by it and look at the worst). On an output joined from shots (`concat_videos`, `dissolve_videos`, a chained step) `seams` alone is enough: the seams and their names are the `media.shots` its run recorded. For any other file pass `boundaries` - each later shot's first frame, the running sum of the shots' `frame_count` from `get_gallery_metadata` on their own `intermediate/` files - and `names` to name them; either one given overrides the recorded value. Tiles are fitted to `max_dimension` and, when the set would exceed the 4MB budget, shrunk together rather than dropped; the text part lists each tile and says so. `hear=N` also returns N seconds of soundtrack centred on each `at` moment, after its image - the way to check a hit point or lip-sync without reconciling two clocks; a mute clip keeps its frames and says `no soundtrack` |
239
+ | `get_output_text(name, max_characters=20000, workspace=None)` | `name`, `max_characters`, `workspace` | Read a text output — a prompt enhancement, or any step whose result is `text/plain` or JSON. Reports the file's real length and whether it was truncated. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
240
+ | `assess_output(name, probe=None, detail=False, workspace=None)` | `name` (a gallery name or `asset:`), `probe`, `detail`, `workspace` | Measure a finished cut on the server, without queueing: the assessment probes (`analyze_shots`, `analyze_seams`, `analyze_sync_drift`) run in the server process over one decode of the file, beside whatever job holds the GPU. Shot boundaries are the ones its run recorded (the manifest for an output, the `keep_output` sidecar for an asset). The answer merges every applicable probe's `findings` (`{rule, severity, at, value, threshold, says}`) with `rules_applied`, `rules_skipped` and `not_applicable` (`{probe: why}` - a still, a mute file, a file with no recorded shots); `detail=true` adds each probe's full body under `probes`, and `probe=` returns that one probe's full body. `probe` is checked against the three names before anything else is read, and an unknown one is refused naming them. Findings are places to look, not verdicts: drill in with `get_output_frames(seams=[n])` / `get_output_audio(start, duration)`. See *Assessing a run's output* in docs/WORKFLOW_GUIDE.md |
241
+ | `download_output(name, destination=None, overwrite=False, workspace=None)` | `name`, `destination`, `overwrite`, `workspace` | Save one output file to local disk, of any content type. `destination` may be a full path or a directory; `~` expands and missing parent directories are created. `overwrite=True` is required to replace a file already at the resolved path. Over the stdio `dw-mcp`, omitting `destination` saves under the output's own name in the current working directory. Over a `dw.serve --mcp` endpoint the file lands on the server confined to that workspace (a relative path is joined onto it), and `destination` is required there - an omitted one is refused rather than dropped loose in the workspace root, where nothing can find or delete it later (#353); use the `url` `list_gallery` reports, `get_output_image`/`get_output_audio`/`get_output_frames` for inline content, or `keep_output` to make it a named asset instead. Returns nothing to the conversation but where the file landed — unlike the other media tools, the point is a file on disk, not a payload in context. Writes on the machine running the MCP server - over `dw.serve --mcp` that is the GPU box. A write that fails there (a path that exists only on the client, for instance) comes back as an error naming the server-side write and the client-side alternatives, not as an anonymous tool failure. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
242
+ | `delete_output(name=None, workspace=None, job_id=None)` | exactly one of `name` / `job_id`, `workspace` | Permanently remove one generated file from the output directory, or - with `name` a `<workflow>/<run id>` run directory, or with `job_id` - a whole run. By `job_id` the run directory is read from the job record (`run_dir`) and the reply adds `job_id` and the resolved `run_dir` to the usual `name` / `deleted` / `run_swept`; a job that never wrote a run directory, or an unknown one, is an error. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it; a `job_id` delete with no `workspace` goes to the workspace the job ran in |
243
+
244
+ ### Authoring, assets and workspaces
245
+
246
+ Authoring happens inside one workspace. A server can hold several - each with
247
+ its own `workflows/`, `assets/` and `outputs/`, all sharing one prompt library
248
+ - and `use_workspace` picks the one this session reads and writes for the rest
249
+ of its life. That is how two agents work against one GPU without saving over
250
+ each other; see [Workspaces](WORKSPACES.md#several-workspaces-on-one-server).
251
+ The session starts in `default` and stays there unless it is told otherwise.
252
+
253
+ The pin above is per-*process* state on `client.py`'s `DwClient`, which is
254
+ one session for the local stdio `dw-mcp`, but `dw.serve --mcp` (see
255
+ [REMOTE.md](REMOTE.md#claude-code-no-local-install)) builds a single client
256
+ for every agent it serves - so on that mounted surface the pin is shared by
257
+ every concurrently connected caller, not scoped to any one of them (#298).
258
+ A second client's `use_workspace`/`create_workspace(use=true)` can switch
259
+ what your session reads and writes without your session calling either. This
260
+ server is single-user by design, so nothing tracks distinct MCP sessions to
261
+ fix that; `use_workspace`/`create_workspace` add a `warning` field to their
262
+ result when the pin they are about to overwrite was already pointed
263
+ somewhere other than `default` (a sign another client may depend on it), but
264
+ they cannot warn the *other* session whose pin just moved out from under it.
265
+ A caller that needs real isolation on a mounted server - the tester and
266
+ regression harnesses among them - must pass `workspace=` on every call that
267
+ accepts it (`validate_workflow`, `run_workflow`, and most of the read/media
268
+ tools) rather than relying on the session pin.
269
+
270
+ | Tool | Arguments | Purpose |
271
+ | --- | --- | --- |
272
+ | `validate_workflow(workflow=None, name=None, workspace=None, arguments=None)` | exactly one of `workflow` (inline definition) or `name` (a stored workflow, as `list_workflows` reports it), optional `workspace`, optional `arguments` | Check a workflow against the schema and against real pipeline signatures. Free and instant. Validating by name uses the workflow file's own directory as the base directory, so it sees what a run would. Returns every schema violation in `errors`, each with the JSON path it sits at, so a draft is fixed in one pass, and a `previous_result:` that names no earlier step is one of them. `warnings` covers what still runs but is probably wrong - a signature mismatch, and, for a list-driven variable, an entry key no step reads, at the entry's path. `workspace` names the workspace for this one call without switching the session to it - use it to pin a job whose `output:` or `asset:` references live in a workspace other than the session's. Pass the same `arguments` you will pass to `run_workflow` and they are checked too - an undeclared or renamed variable name, a value that will not coerce to the declared type, and an `asset:`, `prompt:` or `output:` reference that names nothing this workspace can reach, each reported at `arguments.<name>`. `checked_arguments` lists what was covered, so a `valid: true` about the stored defaults cannot be mistaken for one about your values. A reference set the model would refuse - too many images, videos or audio clips, or, for MiniMax-H3, audio as the only reference - is an error here too, rather than a failure minutes into a run you acknowledged. `run_workflow` makes the same check and refuses a bad argument rather than queuing a job that fails on its first step. A valid answer carries `plan` - the fingerprint, step count, list lengths, `downloads_required`, `estimate` (with `basis`) and `elided_steps` for the arguments given; quote from it. `steps` counts what will run: a step nothing reads and which saves no file does not run, and is named in `elided_steps` instead |
273
+ | `list_workspaces()` | — | The server's workspaces and which one this session is using. Each has its own workflows, assets and outputs; the prompt library is shared by all of them |
274
+ | `use_workspace(name)` | `name` | Work in that workspace for the rest of the session - every later call reads and writes there. This is how to keep your work out of another agent's namespace rather than sharing the default one. Checked against the server, so a typo fails here rather than scoping every later call to nothing. On a `dw.serve --mcp` endpoint the pin is shared by every connected client (#298, see the note above the table) - a `warning` field appears when this call overwrote a pin already pointed away from `default` |
275
+ | `create_workspace(name, use=False)` | `name`, `use` | Create a workspace. Pass use=true to switch this session to it as well; otherwise the session stays where it was and the result says so. `use=true` carries the same mounted-server caveat and `warning` field as `use_workspace` |
276
+ | `delete_workspace(name, acknowledged_cost=False)` | `name`, `acknowledged_cost` | Permanently delete a workspace and everything in it. Refuses without the acknowledgement, reporting what it would remove |
277
+ | `list_assets()` | — | The input media on the server, each with the `asset:` reference a workflow argument carries. Look here before asking for a file - what a workflow needs may already be there. `libraries` names the roots searched and which are writable; `shadowed` lists names a nearer library hides |
278
+ | `keep_output(name, asset_name=None, overwrite=False, shared=False, workspace=None)` | `name`, optional `asset_name`, `overwrite`, `shared`, `workspace` | Keep a generated file as an input asset under a stable `asset:` name, so a later workflow can rely on it. The copy happens on the server: nothing is downloaded or re-uploaded. `asset_name` may name a folder and takes the kept file's extension when it has none; `shared=true` keeps it in the library every workspace shares, which is where a recurring cast belongs. `workspace` names the workspace for this one call without switching the session to it - the same pin `run_workflow` takes, so a job run into another workspace stays reachable from the session that queued it |
279
+ | `upload_asset(file_path=None, content=None, asset_name=None, shared=False)` | exactly one of `file_path` or `content` (base64), optional `asset_name`, `shared` | Push an image, video or audio file into the server's asset library and get back its `asset:` reference. `file_path` is read from the machine the MCP server runs on, so this is how an input reaches a dw.serve running somewhere else - but only when the caller shares a filesystem with that machine. `content` is the alternative for an agent that does not: the bytes travel inline, base64-encoded, in the tool call itself, capped at 4MB (well under `file_path`'s 200MB) because inline bytes compete with the calling agent's own context budget rather than being a bulk-transfer path (#203). `asset_name` is required with `content` (there is no file name to infer one from) and, either way, stores it under a readable name (`cast/priya-voice.wav`) instead of a random one; `shared=true` puts it in the library every workspace shares |
280
+ | `delete_asset(name)` | `name` | Permanently remove one file from the asset library, by the name `list_assets` reports. Deletes from whichever library holds it - this workspace's own before the shared one; one from a read-only examples library is refused. Any workflow still carrying that `asset:` reference stops loading |
281
+ | `save_workflow(name, workflow)` | `name`, `workflow` | Save a workflow into the server's writable workflow directory, overwriting any existing workflow of that name there. A name that currently resolves to a read-only source (an examples directory) is not overwritten - the copy lands in the writable directory and shadows it |
282
+ | `delete_workflow(name)` | `name` | Permanently delete a stored workflow |
283
+
284
+ ### Prompts
285
+
286
+ The stored prompt library is the other half of authoring: a workflow
287
+ argument written as `"prompt:name"` or `"prompt:folder/name"` resolves
288
+ against it at load time, so a workflow can be authored and the text it
289
+ references written in the same session.
290
+
291
+ | Tool | Arguments | Purpose |
292
+ | --- | --- | --- |
293
+ | `list_prompts(tag=None, intended_model=None, include_text=False)` | optional `tag`, `intended_model`, `include_text` | List the stored prompts - description, intended model, tags and `text_chars`, bodies left out; `get_prompt` for one body |
294
+ | `get_prompt(name)` | `name` | Get one stored prompt's full definition |
295
+ | `get_prompt_schema()` | — | Get the JSON schema every stored prompt must satisfy. Its own route rather than a name under `/api/prompts`, so a prompt called `schema` cannot shadow it |
296
+ | `save_prompt(name, prompt)` | `name`, `prompt` | Save a prompt, overwriting any prompt of that name. The server validates first, and refuses a `text` that itself begins with a reference prefix (`variable:`, `previous_result:`, `constant:`, `asset:`, `output:`, `prompt:`) |
297
+ | `delete_prompt(name)` | `name` | Permanently delete a stored prompt. A workflow still referencing it will fail to load |
298
+ | `list_enhancers()` | — | List the enhancer presets `enhance_prompt` accepts |
299
+ | `enhance_prompt(idea, preset="h3", model_name=None, device=None, acknowledged_cost=False)` | `idea`, `preset`, optional `model_name` and `device`, `acknowledged_cost` | Expand a short idea into a full prompt with a language model. Queued as an ordinary job, so it passes the gate; the enhanced text is the text file in the finished manifest, readable with `get_output_text` |
300
+
301
+ ### Diagnose
302
+
303
+ | Tool | Arguments | Purpose |
304
+ | --- | --- | --- |
305
+ | `run_workflow(workflow_path=None, inline_workflow=None, arguments=None, acknowledged_cost=False, workspace=None, wait_seconds=0)` | exactly one of `workflow_path` (a catalog name from `list_workflows`, with or without `.json`, or a path to a workflow file on the server) or `inline_workflow`, optional `arguments`, `acknowledged_cost`, `workspace`, `wait_seconds` | Queue a workflow for generation. Returns as soon as the job is queued - unless `wait_seconds` is above 0, in which case the call then waits on the queued job exactly as `wait_for_job(job_id, timeout_seconds=wait_seconds)` would (same 55 s cap per call, clamped not honoured) and the result carries the queued-job fields plus that wait's (`status`, `still_running`, `waited_seconds`, `timeout_requested_seconds`, `timeout_applied_seconds`, `timeout_capped`, the slim `job`); when the cap covers the job's runtime one call is the run and the wait, and a `still_running: true` result is followed with `wait_for_job` as before. `workspace` names the workspace for this one call without switching the session to it - use it to pin a job whose `output:` or `asset:` references live in a workspace other than the session's - `acknowledged_cost` is `true` or the bound `{fingerprint, minutes, downloads}` from the validate plan; a 409 means the plan changed and the message carries the new estimate, and nothing is waited on |
306
+ | `get_job(job_id)` | `job_id` | Get a job's status, warnings, output manifest, error and traceback; each manifest entry's `subfolder` is the in-run subfolder the step declared - by convention `final` for the deliverable, `intermediate` for scratch, `''` for none. A running job also carries `progress` (below) |
307
+ | `get_job_workflow(job_id)` | `job_id` | The REST equivalent is `GET /api/jobs/{id}/workflow` (see [SERVER.md](SERVER.md#jobs-api)). The workflow the job actually ran. `realized: true` means every mutable input is pinned (arguments, seed, prompts, `output:latest`); `false` means the job predates run tracking and this is the definition as submitted. Pass it to `save_workflow` to keep it under a name |
308
+ | `export_job(job_id, overwrite=False)` | `job_id`, `overwrite` | Gather one finished job into `<workspace>/exports/<job id>/` on the server: the realized workflow, the run's manifest, the job row, a README, and copies of the assets, earlier-run inputs and outputs. Returns the directory, a zip URL, the file list with sizes and the total. The three JSON files are in the zip, not repeated here - get_job_workflow and get_job serve them individually. **The directory is on the machine running the server**, like `download_output`'s destination. `auth_required` says whether opening the zip needs this server's bearer token, which this agent cannot attach to someone else's fetch (#353): when false, fetch `open_url` yourself and unpack it into `exports/` under the session's working directory (a deliverable, not a temp file) - the archive already unpacks into one folder named after the job id, so do not create that folder first; when true, hand `open_url` to the person instead of fetching it |
309
+ | `get_job_events(job_id, after=-1, limit=200)` | `job_id`, `after`, `limit` | Get a page of a job's progress events |
310
+ | `wait_for_job(job_id, timeout_seconds=20)` | `job_id`, `timeout_seconds` | Block until a job reaches a terminal status, or `timeout_seconds` elapses. **One call blocks for at most 55 seconds** — a larger `timeout_seconds` is clamped, not honoured, because no MCP client holds a tool call open for a generation's real runtime, so budget one call per ~55s of the job. Every reply carries `waited_seconds`, `timeout_requested_seconds`, `timeout_applied_seconds` and `timeout_capped`, so a capped return is distinguishable from an elapsed one. Use instead of hand-polling `get_job`/`get_job_events` in a loop; if it returns `still_running: true`, call it again. Returns a slim job - status, warnings, error, `run_id` and `run_version` (the run's `v5`, as the gallery labels it), and the manifest once finished - without the arguments; `get_job` has those. A running job also carries `progress` (below) |
311
+ | `cancel_job(job_id)` | `job_id` | Ask a queued or running job to stop |
312
+ | `clear_memory()` | — | Drop every loaded pipeline and the step cache, freeing VRAM/RAM immediately instead of waiting for the next job to evict one model for another. Also drops the step cache, so a seeded workflow that would otherwise reuse cached results regenerates on its next run. Refused with a 409 while a job is running or queued - the queue is FIFO, so wait for it to finish and retry rather than expecting this call to block until it does (#221) |
313
+ | `rerun_job(job_id, acknowledged_cost=False, new_seed=False)` | `job_id`, `acknowledged_cost`, `new_seed` | Queue a fresh job from a previous job's stored specification. Costs GPU time, so it passes the same gate as `run_workflow`. `new_seed=true` draws a fresh seed into the workflow's seed variable — without it a seeded workflow's rerun repeats its arguments exactly and the step cache serves the whole run from the earlier one's files (`reused: true`), generating nothing. `get_job_workflow`'s `seed_variable` says whether there is one - `acknowledged_cost` is `true` or the bound `{fingerprint, minutes, downloads}` from the validate plan; a 409 means the plan changed and the message carries the new estimate |
314
+ | `move_job(job_id, direction)` | `job_id`, `direction` (`up`\|`down`\|`front`\|`back`) | Reorder a queued job |
315
+
316
+ ### Models
317
+
318
+ | Tool | Arguments | Purpose |
319
+ | --- | --- | --- |
320
+ | `list_models()` | — | (Catalog) List what the Hugging Face cache holds, largest first |
321
+ | `download_model(repo_id, acknowledged_cost=False)` | `repo_id`, `acknowledged_cost` | Fetch a model repo into the cache. Costs disk and bandwidth, so it passes the gate. Returns as soon as the download starts |
322
+ | `list_downloads()` | — | List downloads the server is running or recently ran |
323
+ | `cancel_download(download_id)` | `download_id` | Stop a running download. Partial files stay cached and resume on a retry |
324
+ | `delete_model(repo, acknowledged_cost=False)` | `repo`, `acknowledged_cost` | Delete every cached revision of one repo. Not recoverable locally |
325
+ | `get_diffusers_state()` | — | Installed diffusers version, its git commit, and any update in flight |
326
+ | `update_diffusers(acknowledged_cost=False)` | `acknowledged_cost` | Upgrade diffusers to GitHub HEAD in the background |
327
+
328
+ The server refuses `delete_model` and `update_diffusers` while a job is
329
+ running or queued, and `delete_model` while a download is active — pulling
330
+ files or package contents out from under a loaded pipeline is the same
331
+ hazard twice. That refusal arrives as the server's own explanation.
332
+
333
+ ## The cost gate
334
+
335
+ Seven tools refuse unless `acknowledged_cost=true` is passed. Each commits
336
+ the machine to something the user would want to have been asked about first,
337
+ and each says so in its own words — a single shared refusal would be wrong
338
+ for each of them in a different way, and a gate the user learns to wave
339
+ through is not a gate.
340
+
341
+ | Tool | What it commits |
342
+ | --- | --- |
343
+ | `run_workflow` | Minutes of GPU time; the engine runs one job at a time |
344
+ | `rerun_job` | The same run, from a stored spec |
345
+ | `download_model` | Tens of gigabytes of network and disk |
346
+ | `delete_model` | Cached weights, unrecoverably — getting them back means downloading again |
347
+ | `update_diffusers` | Replacing the installed library with an untagged development build |
348
+ | `enhance_prompt` | A real job on the one-at-a-time engine, delaying any generation behind it |
349
+ | `delete_workspace` | Every workflow, asset and generated file in a workspace, unrecoverably |
350
+
351
+ `rerun_job` is gated for the same reason as `run_workflow`: it queues the
352
+ identical work, so leaving it open would make the gate worth nothing — any
353
+ job id from `list_jobs` would buy a way around it. `cancel_job` and
354
+ `cancel_download` are deliberately *not* gated: they end a cost rather than
355
+ starting one, and gating them would make the safe direction the harder one.
356
+
357
+ The acknowledgement can be bound to what was quoted. `validate_workflow`
358
+ answers with a `plan`; pass `acknowledged_cost={"fingerprint":
359
+ plan.fingerprint, "minutes": plan.estimate.minutes, "downloads": [...]}` and
360
+ the server refuses with 409 if the run's shape changed between the quote and
361
+ the call - a longer list, a stored prompt edited meanwhile, weights that now
362
+ have to be downloaded - naming the new plan so the agent re-quotes. Bare
363
+ `true` still works and is for a `plan` that came back null; the job records
364
+ which form it got (`acknowledged: none | boolean | bound`).
365
+
366
+ Passing the flag does not make a tool wait. The five that start work return
367
+ as soon as it is queued or started, the same way queuing a job from the web UI
368
+ does not block the browser tab; `delete_model` and `delete_workspace` are
369
+ deletions rather than queued work and complete before they answer.
370
+
371
+ The intended loop:
372
+
373
+ 1. `validate_workflow` — free, checks schema and pipeline signatures, no GPU
374
+ time spent. Pass the `arguments` you intend to run with: without them the
375
+ verdict covers the stored definition and its stock defaults, not the
376
+ values you wrote, and its `plan` is the number to say out loud:
377
+ `estimate.minutes` with its `basis`, plus each `downloads_required`
378
+ entry as a line item of its own
379
+ 2. `run_workflow` with `acknowledged_cost` bound to the plan (`{fingerprint,
380
+ minutes, downloads}`), or `true` when there was no plan — pass a name straight from
381
+ `list_workflows` as `workflow_path`; queues the job and returns
382
+ immediately with a `job_id`
383
+ 3. `wait_for_job(job_id)` to block for a bounded interval instead of
384
+ hand-polling — one call covers at most 55 seconds however large
385
+ `timeout_seconds` is, so a minutes-long render takes several; call it
386
+ again if it comes back `still_running: true` — or
387
+ `get_job_events(job_id)` repeatedly, passing back the previous call's
388
+ `last_seq` as `after`, for incremental progress instead of just a
389
+ terminal/not-terminal status. Each event carries `at`, seconds since the
390
+ job started, so where a step's time went is a subtraction between two
391
+ events - `step_start` to `generating` is the lead-in a reused pipeline
392
+ still pays, `generating` to the first `pipeline_step` the encoding
393
+ 4. `get_job(job_id)` for the finished manifest (or the error and traceback,
394
+ if it failed)
395
+ 5. `get_output_image(name)` to look at a result image
396
+ 6. `get_output_frames(name, count=12)` to look at a result video, and
397
+ `get_output_audio(name, start, duration)` to hear it
398
+
399
+ While a job runs, `get_job` and `wait_for_job` carry a `progress` block -
400
+ the step being run, the phase (`loading`, `generating`, `decoding`,
401
+ `saving`) with the model in `phase_detail`, `seconds_in_phase`,
402
+ `seconds_since_event`, and `denoise_step`/`denoise_total_steps`, which are
403
+ null until the denoise loop starts. A single-step generation is minutes of
404
+ one phase, so two polls otherwise come back identical: read `denoise_step`
405
+ moving (slow but healthy) against a `denoise_step` that is a number and
406
+ stays put while `seconds_since_event` climbs (nothing is happening). A null
407
+ `denoise_step` under `generating` is neither - it is the lead-in the
408
+ pipeline runs before the loop, encoding the prompt and every reference, with
409
+ nothing emitted, so silence there is expected. Its length follows what it
410
+ encodes: ~90 s on MiniMax H3 for a prompt with an image or audio reference,
411
+ ~10 min once a *video* reference is among them (measured: 629 s for one 5 s
412
+ 960x544 clip on an RTX 3090). `get_job_events` names the block it is in
413
+ while that runs - a `log` line per top-level block of a modular pipeline
414
+ (`MiniMaxAI/MiniMax-H3: vae_encoder`), which is the difference between
415
+ silence and knowing it is encoding the reference. And once the counter is a number the gaps
416
+ between steps are uneven wherever a transformer block cache is configured -
417
+ cheap cached steps, then a full one - so a 140 s gap on H3 is a healthy run;
418
+ read liveness as the counter moving between polls minutes apart rather than
419
+ as silence under a threshold. `cancel_job` stops at the next denoise or
420
+ step boundary, which `denoise_step` is also the measure of.
421
+
422
+ The other frozen-counter stretch is at the end: under `saving`, `denoise_step`
423
+ sits at a completed-looking `8/8` and cannot move again, because the step is
424
+ writing files. `get_job_events` carries a `log` per file there too - named as
425
+ the write starts (`writing shot.mp4 (121 frames)`) and costed as it finishes
426
+ (`wrote shot.mp4 in 1.3s (1.4 MB)`) - so that stretch is attributable rather
427
+ than silent (#97).
428
+
429
+ ## Security
430
+
431
+ The MCP server adds no authentication of its own — it inherits the REST
432
+ API's posture exactly, described in full in [Security](SECURITY.md):
433
+ localhost binding, no auth, `Origin` header checks, and path confinement in
434
+ `dw/security.py` for every workflow, gallery and prompt path a tool touches.
435
+ Nothing under `dw_mcp/` re-implements or loosens that confinement; it is
436
+ purely a client of the same validated endpoints the web UI uses - except for
437
+ `download_output`, the one tool that writes a local file for the MCP client
438
+ rather than only reading through the API. Over a stdio `dw-mcp` it may write
439
+ anywhere the client's own filesystem lets it (a full path, a directory, or
440
+ the current working directory by default, `~` expanded), the way a shell
441
+ redirect would for the same user; a `..` path segment in `destination` is
442
+ refused, and an existing file is left alone unless the caller passes
443
+ `overwrite=True`.
444
+
445
+ Over `dw.serve --mcp` the write happens **on the server**, and there the
446
+ destination is confined to that workspace: an absolute or `~` path outside
447
+ it is refused, and a relative one is joined onto the workspace rather than
448
+ onto whatever the server process's working directory happens to be.
449
+ `destination` is required over this transport - an omitted one is refused
450
+ rather than dropped loose in the workspace root, where nothing can find or
451
+ delete it later (#353). The
452
+ transport is what distinguishes the two - on stdio "local disk" is genuinely
453
+ the caller's own machine, over HTTP it is the operator's. Confinement is on
454
+ the resolved real path, not a substring test, because an absolute path needs
455
+ no `..` to reach anywhere the server can write.
456
+
457
+ `dw-mcp` may be pointed at a `dw.serve` on another machine only when that
458
+ server was started with a token, and the same token is passed here
459
+ (`--token` / `DW_API_TOKEN`); it refuses to start otherwise. The token is
460
+ the only authentication, and the connection is plaintext HTTP - use it on
461
+ a network you control, or through Tailscale or a TLS proxy beyond that.
462
+ [REMOTE.md](REMOTE.md) has the full setup. The same applies to
463
+ `dw.serve --mcp`, which serves this tool surface itself at `/mcp` behind
464
+ the same token; in that setup `download_output` writes on the server
465
+ machine, not the client's.
466
+
467
+ `save_workflow` and `run_workflow` together let an MCP client write and
468
+ then execute a workflow it authored - and a workflow JSON file can execute
469
+ arbitrary Python (see [Trust model](SECURITY.md#trust-model)). What
470
+ protects a `dw.serve` an MCP client talks to is the server's own
471
+ `--trust-workflows` flag, off by default: run `dw-serve` without it (the
472
+ default) for any server an MCP client can reach.
473
+
474
+ ## Known limits
475
+
476
+ - **Event history is bounded.** `get_job_events` serves at most the last 200
477
+ events of a finished job (`MAX_PERSISTED_EVENTS` in the job history store).
478
+ A job that ran before this feature existed returns an empty event list
479
+ with a `note` explaining why.
480
+ - **Images, sound and frames.** `get_output_image` returns an image,
481
+ `get_output_audio` a soundtrack (an audio file's, or the one muxed into a
482
+ video) whole or as a named excerpt, and `get_output_frames` frames of a
483
+ video as images - there is no video content type over MCP, so a video is
484
+ seen as frames and heard as its track. `get_output_audio` refuses a whole
485
+ clip whose base64 size would exceed the same 4MB budget; ask for an
486
+ excerpt instead.
487
+ - **Uploads read the MCP server's disk, unless sent inline.**
488
+ `upload_asset(file_path)` pushes a local file into the asset library, but
489
+ "local" means the machine `dw-mcp` runs on. Over `dw.serve --mcp` that is
490
+ the GPU box, so a file sitting on the client's laptop is not reachable that
491
+ way - put it on the server, give the workflow a URL (the arguments that
492
+ take a path take a URL too), or use `upload_asset(content=..., asset_name=...)`
493
+ to send the bytes inline instead, base64-encoded in the call itself, capped
494
+ at 4MB (#203). On a `--mcp` endpoint `file_path` is also *confined* to the
495
+ directories the server works in (its workspace, workflows, assets, outputs
496
+ and prompts), and the refusal comes before the file is looked for, so the
497
+ tool cannot be used to probe which paths exist on the box (#138); `content`
498
+ reads no path and is not subject to this confinement, since no file on
499
+ either machine is ever named.
500
+ `download_output` has the same asymmetry in the other direction: on a
501
+ `--mcp` endpoint it writes on the GPU box, not the client's machine, and is
502
+ confined to the workspace there.
503
+ - **Prompts are not per-workspace.** Switching workspaces changes which
504
+ workflows, assets and outputs the session sees; the prompt library is one
505
+ library shared by all of them, because `prompt:` is shared by reference.
506
+
507
+ ## Troubleshooting
508
+
509
+ | Symptom | Likely cause |
510
+ | --- | --- |
511
+ | `Failed to reconnect to dw: ENOENT` (or the client cannot start the server) | The client cannot find the command. Register the venv's **absolute** path to `dw-mcp`, not the bare name - see [Use the absolute path](#use-the-absolute-path-to-dw-mcp). A bare name works only when the client was launched from an activated venv, so this often appears in a second terminal after the first one worked |
512
+ | The server shows connected, but every tool fails | "Connected" means the `dw-mcp` process launched, not that the engine is reachable. Check `dw.serve` is running |
513
+ | "Cannot reach diffusers-workflow at …" | `dw.serve` is not running. Start it with `dw-serve` (or `python -m dw.serve`) and try again |
514
+ | A config change seems to have no effect | A running session holds the old config. Start a new session |
515
+ | It worked, then broke after rebuilding the venv | Re-run `pip install -e ".[server,mcp]"`. If the repo moved or was renamed, re-register the server with the new absolute path |
516
+ | A tool call times out | Usually a model loading into VRAM/RAM for the first time; retry, or raise `--timeout` |
517
+ | `run_workflow` or `rerun_job` refuses with a cost message | Not an error — it is the `acknowledged_cost` gate. Confirm with the user and call again with `acknowledged_cost` bound to the plan (or `true`); a 409 "shape changed" answer means the run grew since the quote - re-validate, re-quote, pass the new plan |
@@ -0,0 +1,78 @@
1
+ # Prompt Weighting
2
+
3
+ Enable A1111-style prompt weighting to control emphasis on individual words or phrases. Also supports prompts longer than the standard 77-token CLIP limit.
4
+
5
+ ## Enabling
6
+
7
+ Set `prompt_weighting` to `true` in the pipeline configuration:
8
+
9
+ ```json
10
+ {
11
+ "pipeline": {
12
+ "configuration": {
13
+ "component_type": "FluxPipeline",
14
+ "prompt_weighting": true
15
+ },
16
+ "from_pretrained_arguments": {
17
+ "model_name": "black-forest-labs/FLUX.1-schnell",
18
+ "torch_dtype": "torch.bfloat16"
19
+ },
20
+ "arguments": {
21
+ "prompt": "a (photorealistic:1.4) portrait with (bright red hair:1.3) and [freckles]"
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Syntax
28
+
29
+ | Syntax | Effect | Example |
30
+ | ------ | ------ | ------- |
31
+ | `(word:1.5)` | Set weight to 1.5 | `(beautiful:1.5) landscape` |
32
+ | `(word)` | Multiply weight by 1.1 | `(beautiful) landscape` |
33
+ | `((word))` | Multiply by 1.1 twice (1.21) | `((beautiful)) landscape` |
34
+ | `[word]` | Reduce weight (divide by 1.1) | `forest with [clouds]` |
35
+ | `[[word]]` | Reduce twice (0.826) | `forest with [[clouds]]` |
36
+ | `\(` `\)` | Literal parentheses | `\(actual parens\)` |
37
+
38
+ Weights are multiplicative when nested: `(((word:1.3)))` = 1.3 x 1.1 x 1.1 = 1.573.
39
+
40
+ ## How It Works
41
+
42
+ When enabled, prompts containing weighting syntax are intercepted before pipeline execution:
43
+
44
+ 1. The prompt string is parsed for weight tokens
45
+ 2. Tokens are run through the pipeline's text encoders with per-token weights applied
46
+ 3. The resulting embedding tensors replace the `prompt` string argument
47
+ 4. The pipeline receives `prompt_embeds` and `pooled_prompt_embeds` instead
48
+
49
+ Prompts without any weighting syntax (`(`, `[`) pass through unchanged as plain strings.
50
+
51
+ An optional `prompt_2` argument (Flux's T5 prompt, normally defaulting to `prompt`) is also consumed and weighted separately when present.
52
+
53
+ Because diffusers rejects mixing a string `negative_prompt` with `prompt_embeds`, any `negative_prompt` argument is silently dropped once weighting kicks in.
54
+
55
+ ## Supported Pipelines
56
+
57
+ Explicitly supports:
58
+
59
+ - FluxPipeline
60
+ - FluxImg2ImgPipeline
61
+ - FluxInpaintPipeline
62
+ - FluxControlNetPipeline
63
+
64
+ Any other pipeline whose class name starts with `Flux` (e.g. `FluxKontextPipeline`, `FluxFillPipeline`, a custom subclass) is also supported automatically, provided it carries the same CLIP + T5 encoder stack (`tokenizer`, `tokenizer_2`, `text_encoder`, `text_encoder_2`). Non-Flux pipelines are not currently supported — the prompt is left as a plain string and a warning is logged.
65
+
66
+ ## Requirements
67
+
68
+ - The pipeline's text encoders must be loaded (not set to `null`)
69
+ - Cannot be used with `remote_text_encoder` (mutually exclusive — `remote_text_encoder` takes precedence if both are set)
70
+
71
+ ## Example
72
+
73
+ ```bash
74
+ python -m dw.run workflows/templates/prompt-weighting.json \
75
+ prompt="a (cinematic:1.5) shot of a (dragon:1.3) breathing [smoke] over a (medieval:0.8) castle"
76
+ ```
77
+
78
+ See [prompt-weighting.json](../workflows/templates/prompt-weighting.json).