hydra-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hydra-mcp Start a kaleidoscope feedback loop and crossfade to it"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hydra-mcp
An MCP server that lets an AI agent live-code Hydra, the browser video synth, alongside a person, and send the result to other software as a real video source.
Two targets. The agent can drive Hydra in its own local performer page, or in an ordinary Hydra tab such as hydra.ojack.xyz.
Dry-run first. Every piece of code runs in a hidden second Hydra instance before it touches the live canvas. A misspelled function or broken shader comes back as a real error, with the live output untouched.
Shared piece, safe ownership. The agent and a person write into one shared composition, and neither can silently overwrite the other.
Sees its own output. The agent takes screenshots, so it can check what it made.
Performance tools. Crossfades, custom GLSL functions, image and video sources, camera and microphone input, a sketch library, and share links.
Native video out. Syphon (macOS) and NDI, for TouchDesigner, OBS, Resolume and other hosts.
MCP client ──stdio──> server/hydra_mcp.py ──HTTP──> deck/hydra_deck.py ──WebSocket──> Hydra tab(s)
(your agent) (thin MCP proxy) (long-lived, 127.0.0.1:8140) │
└─ frames ─> out/hydra_out.py ─> Syphon / NDIContents
Related MCP server: touchdesigner-mcp
Requirements
Needed for | What |
Everything | uv. It installs the right Python for you: 3.10+ for the deck, 3.12 for video out |
The Hydra page | A Chromium-based browser (Chrome, Edge, Brave, Arc). Firefox and Safari can run the page, but not video out |
Syphon out | macOS |
NDI out | macOS, Windows or Linux. Receivers need NDI support (TouchDesigner, OBS with the NDI plugin, NDI Tools, …) |
Driving hydra.ojack.xyz | Chrome, with the unpacked extension in |
No Node.js is needed to run anything. The browser dependencies are vendored.
Install
git clone https://github.com/brandon-stargrave/hydra-mcp.git
cd hydra-mcp
uv sync # deck + MCP serverVideo out is optional and has its own environment. Install only the outputs you want:
cd out
uv sync --extra syphon --extra ndi # macOS: both
uv sync --extra ndi # Windows / Linux: NDI only
cd ..Run it
1. Start the deck and leave it running. It is the process that holds the piece.
uv run deck/hydra_deck.py2. Open the performer page in a browser window you can see: http://127.0.0.1:8140/. Keep it visible, because browsers throttle background tabs (see Troubleshooting).
3. Connect your MCP client (next section). The client starts
server/hydra_mcp.py itself.
4. (Optional) Start video out:
cd out && uv run --extra syphon --extra ndi hydra_out.py --ndiThen have the agent call hydra_video_out(action="start").
Connect your MCP client
The MCP server is a stdio process. Every MCP client wants the same three things: a command, its
arguments, and optional environment variables. Use absolute paths, because clients don't run
your shell and inherit no PATH. Find uv with which uv.
Most MCP clients take an mcpServers entry in this shape. Your client's docs say where its
config file lives:
{
"mcpServers": {
"hydra": {
"command": "/absolute/path/to/uv",
"args": ["--directory", "/absolute/path/to/hydra-mcp", "run", "server/hydra_mcp.py"]
}
}
}If your client has an mcp add command instead, give it the same command and arguments.
Restart the client afterwards. It should list 19 hydra_* tools. The server finds the deck at
http://127.0.0.1:8140 unless told otherwise (see Configuration). If the deck
isn't running, every tool returns {"ok": false, "error": "unreachable"} with the command to
start it.
The performer page
URL | What |
| Performer view: the canvas, a code editor over it, and a status line |
| Clean full-screen output for a second screen, projector or browser source. Follows the performer page by default |
| Choose which tab answers the agent (see Tabs and roles) |
| Turn on microphone input so |
| Render size (default 1280×720) |
| Show the status line on |
The editor shows the shared composition as // @block <name> sections.
Keys | Action |
Ctrl+Enter (or Cmd+Enter) | Run the block under the cursor. It is dry-run first and saved to the document as yours |
Shift+Ctrl+Enter | Run every block |
Shift+Ctrl+H | Hide or show the editor and status line |
Shift+Ctrl+R | Pull in changes the agent made while you had unsent edits. The editor never types over you |
Tools
All tools return JSON (and hydra_look also returns an image). Failures come back as
{"ok": false, "error": "...", "say": "...", "hint": "..."} rather than exceptions.
Reading
Tool | Arguments | Returns |
| — | Connected tabs (role, adapter, visible, fps, size, speed/bpm, mic permission, live sources), blocks with authors and the outputs they draw into, recent runtime warnings |
| — | Every block (name, code, author) and the whole piece as text. For an existing Hydra tab, also |
|
| A PNG screenshot of what is on screen. Refuses honestly if the tab is hidden |
|
| Hydra's transform functions read live from the engine: all names by type plus the primer, one function's inputs and defaults, or every function of a type |
Writing the piece
Tool | Arguments | Does |
|
| Writes, dry-runs and runs one named block. Other blocks keep playing |
|
| Like |
|
| Removes a block. Blanks outputs only it drew into, and re-runs any other block on the same output |
|
| Replaces the whole piece. Split it with |
|
| Adds a custom GLSL transform (Hydra's |
The instrument
Tool | Arguments | Does |
|
| Shows |
|
| Global clock and canvas size |
|
| Loads an external source. Reports |
|
| Reads or tunes |
|
| Streams the canvas to Syphon/NDI and reports the fps measured at both ends |
| — | Panic: blank everything and reset speed. The document is archived first |
Library and sharing
Tool | Arguments | Does |
|
| Saves the piece, with authors and a thumbnail, to |
|
| Loads a saved piece, keeping each block's original author |
|
| Lists saved pieces, filtered by name or tag |
| — | A hydra.ojack.xyz link that opens the piece in the official editor. Built locally; nothing is uploaded |
Each tool's own description (what your client shows the agent) goes into more detail. A primer
on Hydra idioms is at knowledge/hydra-primer.md and is returned
by hydra_reference().
Working with the tools (for agents)
The loop that works:
hydra_state: is a tab connected andvisible? Is anything erroring?hydra_document: what is already playing, and who wrote it? Extend the piece; don't talk over it.hydra_block: write one named block. If it's refused, readerrors, fix, try again. Don't passvalidate=falsejust to get past an error.hydra_look: look before you describe anything. Code that runs cleanly can still render black, blown out or frozen.
Conventions:
Outputs and blocks. There are four outputs,
o0–o3. A bare.out()means.out(o0). Give each output its own block (base→ o0,texture→ o1,mix→ o2), and use ashowblock holdingrender(o2).Values. Plain numbers are compiled into the shader. For motion, use functions (
() => Math.sin(time)) or arrays ([1, 2, 4].fast(0.5)).Feedback. Read an output back into itself:
src(o0).scale(1.01).blend(o1, 0.1).out(o0).Big changes mid-set. Use
hydra_crossfade.hydra_hushis for emergencies only.Permissions. Never assume someone clicked Allow. Camera and microphone report
pending_permissionuntil they do.take=true. It exists only for when the person playing asked you to take over their work.
Example session:
hydra_state() → one primary tab, visible, 60 fps, no blocks
hydra_sketch("// @block base\nosc(14, 0.04, 1.3).kaleid(6).out(o0)\n// @block show\nrender(o0)")
hydra_look() → check it
hydra_block("texture", "noise(2.5, 0.1).modulateScale(osc(4), 0.5).out(o1)")
hydra_block("mix", "src(o0).modulate(o1, 0.12).out(o2)")
hydra_block("show", "render(o2)")
hydra_crossfade("base", "voronoi(8, 0.3, 0.2).color(1, 0.4, 0.8).out(o0)", seconds=6)
hydra_save("first-set", tags=["kaleid", "modulate"])Driving an existing Hydra tab
The agent can also play inside a normal Hydra editor tab, such as
hydra.ojack.xyz. The agent's blocks are written into that tab's own
editor as // @block sections, and the tab's own code stays in charge.
Chrome won't let a public HTTPS page reach 127.0.0.1 without a prompt, and Safari blocks it
outright. So the reliable route is the small unpacked Chrome extension in link/: its
background worker holds the connection to the deck.
Install the extension (once):
In Chrome, open
chrome://extensionsand turn on Developer mode (top right).Click Load unpacked (top left) and select the repository's
linkfolder. Select the folder itself, not a file inside it.Copy the ID shown on the new "Hydra Deck Link" card.
Add it to
~/.hydra-deck/config.json, which allows the extension to connect:{ "extension_ids": ["<the id you copied>"] }Restart the deck.
Use it:
Open (or reload)
https://hydra.ojack.xyz. The extension attaches automatically; on any other Hydra page, click the extension's toolbar icon. Close the site's intro panel so you can see the editor.Make that tab the one the agent drives. Either close the deck's own page, or open the deck page as
http://127.0.0.1:8140/?role=mirror.hydra_statenow shows a primary tab withadapter: "ojack".
What changes for this kind of tab:
hydra_documentincludestab_editor, that tab's live editor text.hydra_sketchrefuses (tab_has_other_code) if the editor holds code the agent didn't write.hydra_droprestores outputs from the tab's own editor sections.
After changing anything in deck/page/, run tools/sync-link.sh. Then press ↻ on the
extension card and reload the Hydra tab. Tabs that were already open keep the old scripts.
A zero-install alternative (experimental) is to paste this into the Hydra tab's DevTools console. Chrome asks once for local network access; Safari can't do this.
(s => { s.src = 'http://127.0.0.1:8140/bridge.js'; document.head.appendChild(s); })(document.createElement('script'))Video out: Syphon and NDI
hydra_video_out(action="start") streams the primary tab's canvas, frame by frame, to
out/hydra_out.py. That process publishes it as:
Output | Platform | Receivers see it as | Notes |
Syphon | macOS |
| Published flipped for GL receivers. Use |
NDI | macOS / Windows / Linux |
| Pass |
cd out
uv run --extra syphon --extra ndi hydra_out.py --ndi # both
uv run --extra ndi hydra_out.py --ndi --no-syphon # NDI only
uv run hydra_out.py --help # --name, --no-flip, …hydra_video_out reports measurements, not the fps you asked for:
page_after_2s.sent_fpsanddropped: what the tab actually sent.out_after_2s.fps: what was published.syphon_clientsandndi_connections: whether anything is receiving.
Frames are dropped, never queued, when the receiver falls behind, so latency stays low.
In TouchDesigner:
Syphon. Add a Syphon Spout In TOP and pick
Python:Hydra. PlainHydramatches nothing and leaves the checkerboard.NDI. Add an NDI In TOP and choose the source in its Name menu.
Throughput. Measured on an Apple Silicon Mac at 1280×720, both paths delivered 30 fps to TouchDesigner.
Video out requires the primary tab to be in a Chromium browser and visible.
Configuration
~/.hydra-deck/config.json is read by the deck, the MCP server and video out. Environment
variables override it. Every key is optional:
Key | Env | Default | What |
|
|
| Deck port |
|
| — | If set, the control API requires |
— |
|
| Where the MCP server looks for the deck |
| — |
| Extra browser origins allowed to connect, e.g. a self-hosted Hydra editor |
| — | — | IDs of the |
|
|
| Video out port |
— |
|
| Where config, the live document and hush archives live |
State on disk:
~/.hydra-deck/document.jsonholds the current piece. A restarted deck replays it into a newly opened page.~/.hydra-deck/hushed-*.jsonholds whathydra_hushcleared.sketches/holds the saved library. It is git-ignored.
How it works
Why two processes. An MCP stdio server lives only as long as one client session, and a performance must not end when a chat restarts. So the long-lived deck holds the document and the browser connections. The MCP server only forwards requests.
Tabs and roles. Every connected tab is a primary or a mirror:
The primary validates, runs code, takes screenshots and streams video.
Mirrors run the same code and never answer.
Who is primary: an explicit
?role=primarywins. Otherwise it's the longest-connected tab that didn't ask to be a mirror.
Adapters.
bridge-core.jsis the same in every tab; a small adapter says where Hydra lives:deckis this project's page.ojackis the official editor, CodeMirror 5 or 6.genericis any page with a running Hydra, such as Strudel'sinitHydra.
Validation.
Why shaders need a separate check. Hydra's shader failures are silent: its bundled renderer never checks compile status, so a broken shader just renders black.
The dry run. Code runs inside
with (synth) { … }against a second, non-global Hydra on a small offscreen canvas.The shader check. Every fragment shader generated along the way is compiled again with raw WebGL, which returns the real error log.
Custom functions. Functions defined with
setFunctionare replayed into the validator, so blocks that use them validate too.
Ownership. Blocks record
author: "agent"(anything through MCP) orauthor: "human"(typed in the deck page's editor). Refusal codes arehuman_blockandhuman_blocks. Nothing arriving over HTTP can write ashuman.
Security model
Running code in the tab is arbitrary JavaScript in someone's browser, so the deck is strict:
Loopback only. Everything binds to
127.0.0.1.Control API.
/deck/*refuses any request that carries anOriginheader, which means every web page. Every route requiresHost: 127.0.0.1:<port>(or localhost), which defeats DNS rebinding. An optional token adds a shared secret.WebSocket. It accepts only allow-listed browser origins: the deck itself,
https://hydra.ojack.xyz, yourallowed_origins, and yourextension_ids.Tabs only report. A connected tab can never make another tab run code. The one exception is the deck page's own editor, on the deck's own origin.
Extension scope. The extension's worker only connects to
ws://127.0.0.1:8140. A page can't choose where it connects.
Troubleshooting
Symptom | Cause and fix |
Every tool returns | The deck isn't running: |
| No Hydra tab is connected. Open |
| Background tabs and minimized windows are throttled. Bring the primary tab's window to the front |
Screenshots come out black where you expected content | Empty outputs are transparent, and screenshots flatten them onto black, as on screen. Check that the output you |
A block is refused with a GLSL log | That is the dry run working. Fix the shader. Note that |
| The block was written by the person at the page. Use another name, or |
| A fade borrows two outputs no other block uses. Free one with |
Camera shows | The browser is waiting for someone to click Allow |
Microphone | Allow the microphone for |
Image or video source | The URL must allow cross-origin use (CORS) |
The extension tab never appears in | Its ID isn't in |
Changes to page code don't take effect in the extension tab | Run |
TouchDesigner Syphon In shows a checkerboard | Choose sender |
Syphon image upside down in your receiver | Restart video out with |
| Start |
High | Lower the resolution ( |
Known limitations
Browsers. Video out needs a Chromium browser (it uses
MediaStreamTrackProcessor).Mirrors. Mirrors have their own clock and microphone, so audio-reactive visuals can differ between tabs. They also don't open their own camera or screen capture.
Screen capture. Browsers require a click inside the page, so an agent usually can't start it remotely.
Deleting in the editor. Deleting a block's text in the deck page's editor doesn't remove it from the document. Use
hydra_drop.Extensions. Code that calls
loadScript(Hydra extensions) skips the dry run, and says so.Paste route. The
/bridge.jsconsole route is experimental; the extension is the supported path.
Project layout and development
server/hydra_mcp.py MCP server (stdio)
deck/hydra_deck.py the deck: page server, WebSocket hub, control REST, document
deck/page/ browser code (AGPL): bridge-core.js, frames.js, adapters/, index.html, out.html
deck/page/vendor/ hydra-synth 1.4.0 (unmodified) and the CodeMirror 6 bundle
link/ Chrome extension (copies of the bridge files, kept in sync by tools/sync-link.sh)
out/hydra_out.py video out (own pyproject, Python 3.12)
knowledge/hydra-primer.md Hydra idioms, returned by hydra_reference()
tools/ build-cm6.sh (rebuild the editor bundle), sync-link.sh
sketches/ your saved library (git-ignored)Rebuild the editor bundle:
tools/build-cm6.sh. This needs Node.js and npm, and pins CodeMirror and esbuild versions.After editing
deck/page/: runtools/sync-link.sh, then reload the extension and its tabs.The block parser exists twice:
parse_blocks()indeck/hydra_deck.pyandblocksOf()indeck/page/adapters/deck.js. Change both or neither.Quick checks:
node --check deck/page/*.js deck/page/adapters/*.jsuv run python -m py_compile deck/hydra_deck.py server/hydra_mcp.py
Licence
The Python code is MIT (LICENSE). The browser code in deck/page/ and link/ is
AGPL-3.0-or-later, because it runs alongside hydra-synth, which is AGPL-3.0. See
THIRD_PARTY_LICENSES.md for everything bundled or installed,
including the NDI runtime's terms.
Hydra is by Olivia Jack and contributors. NDI® is a registered trademark of Vizrt NDI AB.
Available Tools
19 toolshydra_audioA
Check and tune audio reactivity (Hydra's a object), or switch it on.
With no arguments: the current a.fft band values, volume and microphone permission — a quick way to see whether sound is getting in at all.
enable=true Hydra only listens if it was started with the microphone, so this reloads the deck page with ?audio=1. The piece is replayed afterwards; the browser asks for the mic, and a.fft reads zeros until someone clicks Allow. Video out has to be started again after the reload. bins number of frequency bands in a.fft (1–64; default 4). smooth 0–1: how slowly band values fall (higher = smoother). cutoff noise floor subtracted before scaling. scale how much raw loudness it takes to reach 1. show true draws the band meters on the canvas while you tune; false hides them.
Use it in code inside a function, or it is read once and frozen: shape(4, () => 0.2 + a.fft[0] * 0.5).out(o2)
Inputs: bins, show, scale, cutoff, +2 more. Returns: result
hydra_blockA
Write one named block of Hydra code and run it, leaving every other block playing.
name: letters, digits, _ and -, starting with a letter (e.g. "base", "feedback").
Writing an existing name replaces that block — a hard cut; hydra_crossfade fades.
code is ordinary Hydra JavaScript.
DRY RUN FIRST. With validate=true (the default) the code runs in a hidden second
Hydra instance before it touches the live canvas. A misspelled function comes back as
a ReferenceError; a broken shader comes back as the GPU's own compile log. Both are
refused with live_untouched: true. Fix and call again — don't disable validation to
get past an error. validate=false exists for code that loads extensions.
Human-written blocks are refused (error human_block) unless take=true, which you pass
only when the person playing asked you to take that block over.
Things that trip people up:
Each output shows one chain:
.out()means.out(o0). Four outputs, o0–o3;render(o1)shows one,render()shows all four.Plain numbers are baked into the shader. For motion, pass a function —
() => Math.sin(time)— or an array:[1, 2, 4].fast(0.5).Feedback is reading an output back into itself:
src(o0).sum()does not compile in hydra-synth 1.4.0. Useadd.
Examples: osc(20, 0.1, 0.8).kaleid(4).out() noise(3).modulate(osc(10), 0.2).color(0.9, 0.3, 1).out(o1) src(o0).scale(1.01).rotate(0.01).blend(osc(8), 0.1).out(o0) shape(4, () => 0.3 + a.fft[0] * 0.4).out(o2)
Inputs: code (required), name (required), take, validate. Returns: result
hydra_crossfadeA
Like hydra_block, but fade from what is on screen to the new code over seconds
(0.5–30) instead of cutting — the move for a big change mid-set.
How: the new code is dry-run, then a copy runs into a spare output while the old chain keeps playing; a third output blends old into new and is shown; at the end the visible output switches to the new code and both borrowed outputs are blanked.
Needs: one visible output (not the 4-up grid), and two outputs that no other block
draws into or reads — otherwise it refuses with no_spare_outputs and says which are in
use. The new code should draw into the visible output (.out() / .out(o0) when o0 is
shown). The call returns when the fade has finished; mirrors cut instead of fading.
Example (o0 on screen, o2 and o3 free): hydra_crossfade("base", "voronoi(8, 0.3, 0.2).color(1, 0.4, 0.8).out(o0)", seconds=6)
Inputs: code (required), name (required), take, seconds, +1 more. Returns: result
hydra_defineA
Add a custom transform written in GLSL (Hydra's setFunction), usable in any chain afterwards like a built-in.
type and what the body receives / must return:
src _st (vec2 coords) -> vec4 colour
coord _st -> vec2 coords
color _c0 (vec4 incoming colour) -> vec4
combine _c0, _c1 (two colours) -> vec4
combineCoord _st, _c0 -> vec2
glsl is the function BODY only. inputs are its parameters, in order:
[{"name": "amount", "type": "float", "default": 0.5}] (types: float, vec4, sampler2D).
For combine types, declare the incoming chain first: {"name": "tex", "type": "vec4"}.
It is compiled inside a throwaway chain before anything goes live, so a GLSL mistake
comes back as the compiler's own error. On success it is stored as block fn_<name>
at the top of the document (so it is defined again before use when a page reloads) and
use shows a call to start from.
Example: hydra_define("stripes", "src", "return vec4(vec3(step(0.5, fract(_st.x * count))), 1.0);", inputs=[{"name": "count", "type": "float", "default": 10}]) then: stripes(20).rotate(0.3).out(o1)
Inputs: glsl (required), name (required), type (required), take, +1 more. Returns: result
hydra_documentARead-only
The shared composition: ordered named blocks, each with its code and author, plus
the whole thing as one text with // @block <name> separators. When the primary is an
existing Hydra tab, tab_editor is that tab's own editor text — the truth about what
plays there, including anything typed by hand.
Read this before writing, so you extend the piece instead of talking over it — and so you can see which outputs (o0–o3) are already taken.
Inputs: no named parameters. Returns: result
hydra_dropADestructive
Remove one block from the composition and blank the outputs it drew into (unless another block still draws there) — Hydra keeps showing whatever an output was last given, so forgetting the code alone would leave its picture frozen on screen.
Returns blanked (outputs that went black) and restored (blocks run again because
they also draw into one of the same outputs — otherwise the dropped chain, having run
last, would stay on screen there). Human-written blocks need take=true. The code is gone
afterwards; hydra_save first if you might want it back.
Inputs: name (required), take. Returns: result
hydra_hushADestructive
The panic button: blank every output, clear update(), reset speed to 1, and empty the document — every block, human-written ones included, in every connected tab.
The document is archived first (the path comes back as archived), so nothing is
lost, but the screen goes black. To change one part, replace that block instead.
Inputs: no named parameters. Returns: result
hydra_loadADestructive
Replace the composition with a saved sketch — the same dry run, hush and run as hydra_sketch, keeping each block's original author.
Refused if the current document holds human-written blocks, unless take=true. Save the current piece first if it is worth keeping. An unknown name returns the list of saved sketches.
Inputs: name (required), take, validate. Returns: result
hydra_lookARead-only
A screenshot of the primary tab's canvas, as an image, downscaled to max_width
pixels wide (64–1920).
This is the only evidence of what the visuals look like. A block that evaluated cleanly can still be black, blown out or static, so look after writing and before describing anything. It shows whatever render() currently shows — one output, or the 4-up grid.
A hidden tab can't be photographed (its frames are throttled, and a minimized window's canvas has no size); the error says so.
Inputs: max_width.
hydra_referenceARead-only
Hydra's transform functions, read live from the running engine — so it lists exactly what this tab's hydra-synth has, including anything added with setFunction.
no arguments: names grouped by type, plus the primer (idioms and gotchas)
name="modulate": that function's inputs and defaults (also fuzzy: name="repeat")
type="combineCoord": every function of one type
Types, and what they chain onto: src start a chain: osc, noise, voronoi, shape, gradient, solid, src, prev coord move pixels: rotate, scale, repeat, kaleid, scroll… color change colour: color, brightness, contrast, hue, colorama, luma… combine mix another source in: add, blend, mult, diff, layer, mask combineCoord warp by another source: modulate, modulateScale, modulateRotate…
Check a name here before using one you are not sure of — an invented function is refused by the dry run anyway, but this is cheaper.
Inputs: name, type. Returns: result
hydra_renderA
Show one output full-screen ("o0", "o1", "o2", "o3") or all four in a grid ("all").
Every output keeps rendering either way, so a chain on o1 can feed o0 while only o0
is visible. This does not change the document — to make the choice permanent, put
render(o1) in a block.
Inputs: output. Returns: result
hydra_saveA
Save the current composition — every block, with its author — to the sketch library (hydra-mcp/sketches/.json), plus a 480-px thumbnail of what is on screen.
name: letters, digits, _ and -, up to 60. tags: a few words to find it by later ("feedback", "audio", "slow"). An existing name is refused unless overwrite=true.
Inputs: name (required), tags, overwrite. Returns: result
hydra_sketchADestructive
Replace the entire composition with a new sketch: dry-run all of it, hush, then run.
Use this to start a fresh idea, not to add to one — hydra_block adds. Split the sketch into blocks with marker lines so each part can be revised on its own later:
// @block base
osc(30, 0.05, 1.2).out(o0)
// @block warp
src(o0).modulate(noise(2), 0.1).out(o1)
render(o1)Code with no markers becomes one block called main.
If the document holds any human-written blocks this is refused (human_blocks),
because a sketch would erase them. Pass take=true only if the person playing asked for a
fresh start.
Inputs: code (required), take, validate. Returns: result
hydra_sketchesARead-only
List saved sketches: name, tags, when saved, block names, and the thumbnail path.
query filters by a word in the name or tags.
Inputs: query. Returns: result
hydra_sourceA
Load an external image stream into s0–s3, then read it in any chain with src(s0).
kind: "image" target = an http(s) URL. It must allow cross-origin use (CORS) or WebGL cannot read it — the error says so. "video" target = an http(s) URL, same CORS rule; loops muted. "cam" target = camera index ("0"). The browser asks for permission: expect state "pending_permission" until someone at the machine clicks Allow, and check hydra_state rather than assuming. "screen" screen capture. Browsers only allow it from a click inside the page, so a remote call usually cannot start it; the error explains. "clear" release the slot.
Returns state: "live" (with the media's size), "pending_permission", or an error. It
waits up to four seconds for the source to actually deliver frames. Images and video
are loaded on mirrors too; cameras and screen capture are not (each would prompt).
Then: src(s0).kaleid(4).out(o1)
Inputs: kind (required), slot (required), target. Returns: result
hydra_stateARead-only
Which Hydra tabs are connected and whether they can be trusted right now.
Returns every tab with its role (exactly one primary answers; mirrors just follow),
its adapter (deck = our page, ojack = the official editor, generic = any other
Hydra tab), fps, resolution, speed/bpm, which sources are live, the block names with
their authors and the outputs they draw into, and the primary's recent runtime warnings.
Two fields are not formalities: visible false means the tab is in the background. Browsers throttle its frames, so screenshots stall and what you see may be minutes old. Ask for it to be brought to the front rather than working blind. mic 'prompt' means the browser is still waiting for someone to click Allow. Audio-reactive code (a.fft) reads zeros until they do — and nobody may be at the machine. Never assume the click happened.
Inputs: no named parameters. Returns: result
hydra_transportA
Set the global clock and canvas size. Pass only what you want to change.
speed multiplies time for everything (1 is normal, 0 freezes motion; 0–20).
bpm the tempo arrays step to: [1, 2, 3].fast(1) changes value once per beat.
width, height render resolution, set together (16–4096). Larger costs fps, and
video out sends this size.
This changes the whole piece, human-written blocks included — say so if someone is playing along.
Inputs: bpm, speed, width, height. Returns: result
hydra_video_outA
Send the primary tab's canvas out of the browser as a native video source: a Syphon server named "Hydra" (macOS). TouchDesigner receives it with a Syphon Spout In TOP. TouchDesigner names Syphon senders App:Server, and the publisher runs as Python, so the sender to pick is "Python:Hydra" — plain "Hydra" matches nothing and leaves the TOP on its checkerboard.
With hydra_out.py started with --ndi, the same
frames also go out as an NDI source, which receivers list as " (Hydra)". NDI
reaches other machines on the network; ndi_connections in
the status says whether anything is receiving it.
action: "start", "stop" or "status". fps: 1–60, the capture rate to request.
Needs out/hydra_out.py running — a separate process with its own Python 3.12, because
the Syphon library only supports up to 3.12 (see the README). Start waits two
seconds, then returns both ends:
page_after_2s sent_fps, dropped — what the tab actually sent
out_after_2s fps, syphon_clients — what was published, and whether anything is
receiving
Report those numbers, not the fps you asked for. Frames are dropped rather than queued
when the receiver lags, so a high dropped means lower the resolution
(hydra_transport) or the fps.
Chromium tabs only (MediaStreamTrackProcessor). The primary must stay visible: a hidden tab stops producing frames.
Inputs: fps, action. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 19 tools
Most tools map cleanly to a distinct action in the Hydra workflow, and the descriptions are unusually precise. A few close pairs exist — hydra_sketch vs hydra_load both replace the whole composition, and hydra_block vs hydra_crossfade are write operations differing only in transition — but their purposes are clearly separated in the docs.
All tools share the hydra_ prefix and lowercase snake_case, which makes the set feel cohesive. However, the names mix nouns (hydra_state, hydra_document, hydra_reference) with verbs (hydra_drop, hydra_render, hydra_save), so the pattern is predictable but not a strict verb_noun convention.
At 19 tools, the server is on the heavy side of the ideal range. The count is defensible given the broad domain — composition editing, state inspection, rendering, media sources, audio, persistence, and video output — but a few tools could plausibly be consolidated without losing clarity.
The core composition lifecycle is well covered: read, create, replace, fade, delete, panic, save, load, list, and share. Minor gaps remain, such as no direct way to delete a saved sketch, reorder blocks, or undo changes, but an agent can accomplish the main Hydra workflows without hitting dead ends.
Maintenance
Related MCP Connectors
AI video editor your agent can drive: plan scenes, edit a multi-track timeline, and render MP4s.
Video production studio for AI agents: AI media, motion graphics as code, timeline and export.
Live, editable canvas for AI agents — author graphs via MCP, then watch and reroute runs live.
Generate and edit images, video, voice, lip-sync and 3D models from your AI agent.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables an LLM to drive a live Bespoke Synth, including module graph editing, control modulation, sequencing, playback control, and audio analysis feedback with visualizations.AGPL 3.0
- AlicenseAqualityDmaintenanceLets Codex inspect, build, wire, tune, sample, and visually verify running TouchDesigner projects for generative visuals, audio-reactive systems, and fast agent-assisted prototyping.151MIT
- AlicenseNot gradedqualityCmaintenanceA local MCP bridge that lets AI agents build and edit real TouchDesigner networks by creating and wiring operators, setting parameters and expressions, validating the network, and viewing rendered image previews to iterate on results.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI coding agents to generate cinematic video and image assets and perform autonomous timeline assembly, multi-track audio editing, chroma-key compositing, and local draft manipulation from conversational prompts.3MIT