Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
75df496
fix(engine): #82 #83 #84 - run-time warnings reach the caller, carrie…
dkackman Sep 12, 2026
2d87005
Merge branch 'mcp-fixes-2026-09-12c' into develop
dkackman Sep 12, 2026
80baba7
fix(engine): #88 #89 #90 #92 - sub-workflow paths, one save per compo…
dkackman Sep 13, 2026
6306f09
docs(catalog): #91 - say what a 'cost' is, and propose deriving one f…
dkackman Sep 13, 2026
57d801b
fix(realize): #90 - digest a sub-workflow the run could actually load…
dkackman Sep 13, 2026
7e5a42b
Merge branch 'mcp-fixes-2026-09-13' into develop
dkackman Sep 13, 2026
2878c1e
fix(engine): #90 - a missing candidate is reported as looked-at, not …
dkackman Sep 13, 2026
2dbd22e
fix(engine): #90 - each candidate path named once in the not-found me…
dkackman Sep 13, 2026
e4f64b1
fix(ui): match a run's step names to the flow graph's nodes
dkackman Sep 13, 2026
0053814
chore(todos): remove outdated todos document
dkackman Sep 13, 2026
45fb908
fix(ui): keep a flow node's labels inside its box
dkackman Sep 13, 2026
485a045
Merge pull request #94 from dkackman/ui-run-state-and-unsaved-steps
dkackman Sep 13, 2026
7d8994f
docs(mcp): #95 - the pre-denoise lead-in scales with the references, …
dkackman Sep 13, 2026
6dbeef4
feat(engine): #95 - name each modular block as it starts, so the lead…
dkackman Sep 13, 2026
b39d03c
Merge branch 'mcp-fixes-2026-09-13b' into develop
dkackman Sep 13, 2026
8f9409e
fix(mcp): #99 - the output-side tools take the same per-call workspac…
dkackman Sep 13, 2026
bfdac09
fix(engine): #98 - a full cleanup gives back the pinned-host cache an…
dkackman Sep 13, 2026
934e97d
docs(proposals): #96 - where a model's argument bounds should live, s…
dkackman Sep 13, 2026
0bbeae2
Merge branch 'mcp-fixes-2026-09-14' into develop
dkackman Sep 13, 2026
deab0db
fix(engine): #97 - narrate the saving phase, and stop paying 16 s to …
dkackman Sep 13, 2026
ae4bf9b
docs(mcp): #97 - the saving phase is narrated, and the LTX-2.5 cost i…
dkackman Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@ workflows/*/assets/
/output/
/*.mp4
/*.wav

# Claude Code worktrees
.claude/worktrees/
38 changes: 27 additions & 11 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,21 +222,21 @@ when no single workflow covers it.
| `list_tasks()` | — | List every task command a workflow's task step can name |
| `get_task(command)` | `command` | Get a task command's argument schema |
| `list_models()` | — | List what the Hugging Face model cache holds, largest first |
| `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). `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 |
| `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 |
| `get_health()` | — | Check that the server is alive, and which machine answered: `version`, `device`, whether the worker process is up, the job running now and the queue depth |
| `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 |
| `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 |
| `list_gallery(limit=50, subfolder=None)` | `limit`, `subfolder` | 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 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 |
| `get_gallery_metadata(name, envelope=False)` | `name` | Get the metadata embedded in a generated file: the exact workflow and arguments that produced it, and, for audio/video, a `media` block (duration, rate, channels, fps, size, peak/mean dBFS). `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 |
| `list_gallery(limit=50, subfolder=None, workspace=None)` | `limit`, `subfolder`, `workspace` | 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 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. `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 |
| `get_gallery_metadata(name, envelope=False, workspace=None)` | `name`, `workspace` | Get the metadata embedded in a generated file: the exact workflow and arguments that produced it, and, for audio/video, a `media` block (duration, rate, channels, fps, size, peak/mean dBFS). `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. `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 |

### Media

| Tool | Arguments | Purpose |
| --- | --- | --- |
| `get_output_image(name, max_dimension=768)` | `name`, `max_dimension` | 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 |
| `get_output_text(name, max_characters=20000)` | `name`, `max_characters` | 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 |
| `download_output(name, destination=None, overwrite=False)` | `name`, `destination`, `overwrite` | Save one output file to local disk, of any content type. `destination` may be a full path, a directory, or omitted to save under the output's own name in the current working directory; `~` expands and missing parent directories are created. `overwrite=True` is required to replace a file already at the resolved path. 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 |
| `delete_output(name)` | `name` | Permanently remove one generated file from the output directory |
| `get_output_image(name, max_dimension=768, workspace=None)` | `name`, `max_dimension`, `workspace` | 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. `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 |
| `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 |
| `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, a directory, or omitted to save under the output's own name in the current working directory; `~` expands and missing parent directories are created. `overwrite=True` is required to replace a file already at the resolved path. 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 |
| `delete_output(name, workspace=None)` | `name`, `workspace` | Permanently remove one generated file from the output directory. `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 |

### Authoring, assets and workspaces

Expand All @@ -255,7 +255,7 @@ The session starts in `default` and stays there unless it is told otherwise.
| `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 |
| `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 |
| `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 |
| `keep_output(name, asset_name=None, overwrite=False, shared=False)` | `name`, optional `asset_name`, `overwrite`, `shared` | 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 |
| `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 |
| `upload_asset(file_path, asset_name=None, shared=False)` | `file_path` | Push a local image, video or audio file into the server's asset library and get back its `asset:` reference. The file is read from the machine the MCP server runs on, so this is how an input reaches a dw.serve running somewhere else. `asset_name` 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 |
| `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 |
| `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 |
Expand Down Expand Up @@ -370,11 +370,27 @@ one phase, so two polls otherwise come back identical: read `denoise_step`
moving (slow but healthy) against a `denoise_step` that is a number and
stays put while `seconds_since_event` climbs (nothing is happening). A null
`denoise_step` under `generating` is neither - it is the lead-in the
pipeline runs before the loop, encoding the prompt and any reference image
or audio, ~90 s on MiniMax H3 with nothing emitted, so silence there is
expected. `cancel_job` stops at the next denoise or
pipeline runs before the loop, encoding the prompt and every reference, with
nothing emitted, so silence there is expected. Its length follows what it
encodes: ~90 s on MiniMax H3 for a prompt with an image or audio reference,
~10 min once a *video* reference is among them (measured: 629 s for one 5 s
960x544 clip on an RTX 3090). `get_job_events` names the block it is in
while that runs - a `log` line per top-level block of a modular pipeline
(`MiniMaxAI/MiniMax-H3: vae_encoder`), which is the difference between
silence and knowing it is encoding the reference. And once the counter is a number the gaps
between steps are uneven wherever a transformer block cache is configured -
cheap cached steps, then a full one - so a 140 s gap on H3 is a healthy run;
read liveness as the counter moving between polls minutes apart rather than
as silence under a threshold. `cancel_job` stops at the next denoise or
step boundary, which `denoise_step` is also the measure of.

The other frozen-counter stretch is at the end: under `saving`, `denoise_step`
sits at a completed-looking `8/8` and cannot move again, because the step is
writing files. `get_job_events` carries a `log` per file there too - named as
the write starts (`writing shot.mp4 (121 frames)`) and costed as it finishes
(`wrote shot.mp4 in 1.3s (1.4 MB)`) - so that stretch is attributable rather
than silent (#97).

## Security

The MCP server adds no authentication of its own — it inherits the REST
Expand Down
Loading