Skip to content

Native interaction & safe editor actions

tdmcp can now ask for a destructive decision inside TouchDesigner without holding the original HTTP request open. Wave 1 adds a small, authenticated interaction broker and structured editor actions that keep working when TDMCP_BRIDGE_ALLOW_EXEC=0.

The native decision inbox

The runtime bridge has an Interactions custom-parameter page. A delete or overwrite request is queued, returned as an opaque request ID, and presented on the following TouchDesigner frame. Choose an item in Choice, then pulse Apply Choice. Safe Close cancels safely: destructive/file prompts resolve to Keep, while OAuth consent resolves to Deny.

This inbox is deliberately non-modal. It does not call ui.messageBox from the Web Server DAT callback, so a prompt never keeps the original request open past the client's normal HTTP timeout. Only one prompt is presented at a time; the queue, lifetime and retained records are bounded.

Every unsafe terminal path means Keep: timeout, close/cancel, lost client, missing UI, Perform Mode, headless operation, scheduling failure or duplicate consumption. A prompt can authorize its exact target once. It cannot execute Python, carry a callable, or approve another operator or file.

Panic, blackout and other emergency paths do not use or wait for this broker.

Delete / Bypass / Keep

delete_td_node with mode: "delete" presents exactly:

  • Delete — destroy the operator after consent;
  • Bypass — set the operator's bypass flag and preserve it;
  • Keep — leave the operator unchanged.

The prompt identifies the operator path, type and name and summarizes the local impact. The result reports the decision, original/final path, applied action, confirmation policy, request ID, and the per-request TouchDesigner undo label when the live build exposes one.

Migration from the old delete behavior

Existing calls with { path, mode } remain valid, but the safe default changed:

  • mode: "delete" now requires native consent and returns Keep when the UI cannot decide;
  • mode: "bypass" remains immediate and reversible;
  • TDMCP_YOLO=1 is the only tool-level skip policy. It is explicit in the structured result as confirmation_policy: "yolo"; missing UI is never interpreted as approval.

Reinstall or reload the runtime bridge with this source before using the new flow. An older bridge that lacks the broker fails rather than falling back to an unconfirmed legacy delete.

Batch delete cannot pause a batch for UI consent. A legacy batch delete now returns Keep with ok: false. Use the standalone delete_td_node flow for native consent, or make the batch policy explicit with mode: "bypass" or confirmation_policy: "yolo".

Delete is not inherently “impossible to undo.” Its final REST mutation already runs inside the bridge's ui.undo wrapper. A live build probe confirmed that ordinary nested blocks can collapse into one outer-named undo item, but also found that creating an annotateCOMP closes/replaces the caller's outer block. One undo entry spanning an arbitrary high-level MCP tool is therefore not safe to enable yet.

Save and Save As

save_td_project saves the current .toe, or accepts an absolute .toe path for Save As. An existing different target requires an Overwrite / Keep decision from the broker. No file dialog, project load, project quit, or raw Python fallback is exposed.

Success is returned only after the target exists on disk. Untitled projects require an explicit Save As path. A consent ticket is bound to the normalized target path and cannot authorize a different file.

Editor-aware primitives

  • get_editor_context returns a compact project/build snapshot, Perform Mode, panes, the explicitly active Network Editor, owner/current/selected nodes, rollover operator/parameter and viewport. Missing UI fields are omitted or null with warnings; topology is not dumped.
  • pulse_td_parameter resolves an operator and parameter, verifies that its style is Pulse, calls .pulse(), and reports typed errors for an invalid operator, missing parameter or wrong style.
  • edit_td_node_metadata edits name, parent, exact position, color, comment and supported writable flags. It reads changes back and rolls back a partial failure. Parent moves copy and validate the destination before destroying the source.
  • create_td_node accepts placement: "auto" | "explicit", exact node_x / node_y, and viewer. Omitting placement preserves the previous TD drop behavior; reused idempotent nodes keep their existing layout.

Undo status

The bridge keeps its existing one undo block per mutating REST request. New single-request metadata, Pulse, create and final delete mutations receive useful receipts. The receipt now reports the actual newest artist-visible native item when exactly one item was added; when TouchDesigner substitutes a built-in name such as Delete Node or Change Bypass Flag, the requested wrapper name is reported separately by the raw bridge response.

Live validation on TouchDesigner 2025.32820 proved that an ordinary outer block can undo and redo two nested edits as one item, and that finally closes a block after an exception. It also reproduced the blocker: creating an annotateCOMP inside the outer block created its own Add Annotate entry, closed/replaced the outer block, and made the caller's endBlock() fail. Cross-request, whole-MCP-tool transactions remain held until a TD-safe ownership protocol can survive operator-specific undo behavior, cancellation and timeouts without orphaning a transaction.

Automated one-item undo/redo is also HELD. A two-item live probe established that this build exposes stacks newest-first (stack[0]) and that native undo and redo work, but a follow-up identity probe showed that every stack read returns a fresh plain string. Two unrelated actions can both be named Delete Node or Change Bypass Flag; after an intervening artist edit, a label-only check can still match and undo the wrong action (same-label ABA). Count checks reduce but cannot eliminate that ambiguity. No public tool or route is registered until TD exposes a stable item identity or a stronger artist-edit-aware protocol is proved.

Wave 7: action-aware, transactional editor workflows

Wave 7 extends existing tools instead of adding aliases. focus_network_editor, manage_component, make_portable_tox, manage_packages and add_custom_parameters now share bounded structured bridge primitives, and the new insert_operator_at_selection tool adds the missing editor mutation. The routes remain authenticated and work with TDMCP_BRIDGE_ALLOW_EXEC=0; the legacy manage_component load path is unchanged and still uses its existing exec-gated implementation.

Evidence status

AreaStatusWhat the evidence proves
Action-aware Network Editor followPASS — TD 2025.32820 routeThe authenticated route reused visible/compatible panes, replaced current/selection exactly, ran six generation-checked viewport frames, cancelled rapid stale generations and suppressed Perform Mode. A real headless process, other TD builds and unusual multi/floating-pane layouts remain UNVERIFIED.
Insert at active selectionPASS — authenticated route, TD 2025.32820With auth enabled and bridge exec disabled, single-chain, fan-out, multi-input, deterministic placement, replay/conflict, induced rollback and one-item route undo/redo passed without a thread conflict. Live TD connector proxies required structural owner/path/index identity rather than Python object identity.
Transacted .tox exportPASS — authenticated route, TD 2025.32820as_is/portable, exact Overwrite / Keep, response-loss recovery by idempotency key, deduplicated retry, cancellation, hashing and cleanup passed with bridge exec disabled. Filesystem artifacts remain outside TD graph undo.
Package namespace reconciliationPASS — authenticated TD + local storageKeep, native Bypass/Delete, explicit audited YOLO, stale-plan rejection, quarantine/registry commit or restore, and TD undo/redo passed end to end. The live fix aligns broker fingerprints on OPType; TD undo restores only TD state, not committed registry/filesystem state.
Custom-parameter lifecyclePASS — authenticated route, TD 2025.32820Add/edit/delete/sort/rename/delete-page, exact undo/redo, replay/conflict, induced rollback and rollback undo safety passed. Live fixes cover imported-module ParMode resolution, structural ParGroup identity, style-safe rollback and deterministic clamp/value ordering. EXPORT remains HELD.
Automated one-item native undo/redoREJECTEDNative undo/redo works, but the live stack exposes only repeatable string labels with no stable item identity. Same-label ABA can target an intervening artist action, so a generic route/tool is permanently rejected.
One-request structured operationOFFLINE PASS / LIVE ROUTE UNVERIFIEDWave 15 adds token-required preview, one-callback commit and capability-authorized receipt observation outside the generic wrapper. The adapter previously passed live journal undo/redo and rollback, but the new public route could not be exercised because the disposable second TD process never opened a listener. No MCP tool is registered.
Whole-tool undo across REST requestsFAIL / REJECTED DESIGNTD terminates a dangling undo block when each Web Server DAT callback returns. A later request cannot safely join it; one undo removed only the first probe mutation. The bridge therefore keeps one named undo block per legacy mutating REST request and never carries a block across requests.
Colour highlightHELDFraming/current/selection shipped. Transient colour restoration under overlapping actions and artist edits has no accepted compare-and-swap contract, so Wave 7 does not change node colours.

PASS is scoped to the evidence named in the row. It does not promote actual headless TouchDesigner, TouchPlayer, other builds, external filesystem behavior, or an unrun authenticated end-to-end path.

Wave 15 operation boundary

POST /api/operations/preview, /commit and /receipt are guarded source-tree bridge primitives, not MCP tools. They require a configured bearer token before the request body is parsed and remain usable with TDMCP_BRIDGE_ALLOW_EXEC=0. Preview is read-only. Commit owns one synchronous callback-journal transaction, and receipt recovery requires the opaque operation ID, a separate 256-bit capability, the same authenticated principal and the same bridge instance. Capabilities stay in POST bodies and are never undo labels or query parameters. The idempotency key only deduplicates an identical commit and is deliberately absent from terminal receipts.

v0.13.2 does not expose generic undo/redo, receipt-bound revert, selection-to-component or plan/preview/commit agent orchestration yet. Revert requires a direction-aware compensation journal plus native Apply/Keep consent; selection collapse requires live proof of exact topology/reference rollback. The Wave 15 disposable process did not reach a bridge listener, so those paths remain UNVERIFIED instead of borrowing evidence from the artist's unsaved project.

Action-aware Network Editor follow

The legacy { paths, animate } call remains valid. New optional fields make the receipt and UI policy explicit:

  • action: create, edit, inspect, view, layout or delete;
  • framing: auto, selection, owner or none;
  • enabled: an explicit opt-out that returns a typed suppression.

Targets must belong to one parent network. The bridge prefers the active or already-owning compatible Network Editor, never creates a pane, replaces stale selection, assigns an explicit current operator and reads back the final owner, selection and viewport. animate:true uses six bounded next-frame ease-out steps; every step revalidates the pane generation and only the sixth publishes final readback. A newer generation cancels stale scheduled steps. Disabled follow, Perform Mode and UI-unavailable sessions do not move the editor. Actual headless behavior remains UNVERIFIED, and colour highlight remains HELD.

Insert at the active selection

insert_operator_at_selection requires the exact active Network Editor owner, single selected operator and current operator returned by get_editor_context, plus an opaque idempotency key. Any context drift fails before creation. The bridge accepts only a live-creatable same-family operator type, assigns explicit deterministic non-overlapping coordinates immediately, forces the new viewer off, and replaces one stable downstream edge while preserving fan-out siblings and other downstream inputs.

Creation, parameters, placement, connector changes and final readback happen in one authenticated REST mutation with one request-level undo label. A failure disconnects/destroys only the new node and verifies the exact original edge snapshot; retries replay a sanitized receipt, while a reused key with changed input fails closed. Mouse-interactive placeOPs, selected-wire inference, multi-node insertion and raw Python are not exposed. The final main-thread-only harness passed the authenticated route on 2025.32820 with bridge exec disabled, including exact rollback, replay/conflict and route-level undo/redo.

Transactional TOX export

manage_component action:"save" now uses the shared as_is transaction, and make_portable_tox uses portable. Both validate an absolute .tox target, queue at most one active export, write a unique same-directory temporary file, verify size/hash/build, atomically promote it and retain a bounded status receipt for polling and idempotent retry.

Overwrite now defaults to overwrite_policy:"refuse". Use "ask" to request an exact target-bound Overwrite / Keep ticket; missing UI is never consent. Portable mode snapshots Text/Table DAT file links/content and COMP externaltox state, restores them in finally, and is fail-closed outside the live-proven build unless the operator explicitly opts into a separately tested build. A .tox can succeed while README/manifest sidecars fail; that is reported as partial_failure, never as full package success.

With bridge exec disabled, the structured portable export still runs. The existing README introspection helper is best-effort and may be skipped with a warning because that sidecar path remains legacy/exec-gated.

A filesystem write is not TouchDesigner graph undo. No claim is made that ui.undo can remove or restore an exported artifact. Client timeout/disconnect after dispatch can also be ambiguous; poll the opaque operation ID before retrying.

Package namespace reconciliation

manage_packages action:"reconcile" is dry-run-first. The plan scans a bounded project namespace and acts only on a unique marker whose package ID, source fingerprint, ref and scope match the local install record. Foreign, unreadable, mismatched, markerless and duplicate candidates are non-actionable.

Apply requires the unexpired plan_id and revalidates ownership. Keep is a no-op, Bypass preserves the live COMP, and Delete requires native consent unless explicit YOLO policy is active. Local staged files are quarantined before the registry record changes; a failure restores the quarantine when possible, and an incomplete cleanup returns partial_failure plus remediation. A legacy uninstall that still has a live TD target now returns this safe reconciliation plan instead of deleting local state first.

This workflow never runs third-party scripts, installs Python dependencies, downloads models or configures external applications.

Custom-parameter lifecycle

Existing { comp_path, page, params } calls still mean transactional add. operations adds bounded add, edit_parameter, delete_parameter, sort_page, rename_page and delete_page actions in the same tool. Supported styles are Float, Int, Toggle, Str, Menu, Pulse, Header, OP, TOP, File, Folder, XYZW and RGBA; legacy RGB/XYZ inputs remain accepted. EXPRESSION and BIND modes are supported. EXPORT mode is HELD and fails before mutation because a reversible export-source contract was not proved.

The migration intentionally tightens old partial-add behavior: an exact existing definition is unchanged, a conflicting definition fails before replacement, and a later failure restores the complete custom-page snapshot or reports partial_failure. Built-ins are never editable. Sort must name every ParGroup exactly once and passes only par.parGroup objects to TD, preserving XYZW/RGBA components. Results are per operation and field; values, expressions and bind expressions are omitted from idempotency receipts.

Environment-specific external plugin warning

The macOS alert about being unable to open OS encryption services was diagnosed on this workstation as a FAIL of local install trust, not a tdmcp bridge failure: the current TouchDesigner bundle has an invalid sealed-resource code requirement, while the installed FreenectTOP plugin is ad-hoc signed and rejected by Gatekeeper. The binary being mapped does not prove that the operator registers or cooks; functional activation is UNVERIFIED.

Do not delete keychain items, strip quarantine or re-sign the main app as a shortcut. The safe next experiment is a pristine official TouchDesigner install verified before launch, first without external plugins and then with a vendor-signed/notarized plugin in an isolated project.

Wave 8 result examples

PASS — verified transaction

json
{
  "status": "succeeded",
  "operation_id": "opaque-export-id",
  "verification": { "level": "load_independent" },
  "cleanup": { "pending": false }
}

FAIL — ownership could not be proved

json
{
  "status": "failed",
  "code": "package_not_recorded",
  "storage": { "quarantined": false, "recordRemoved": false }
}

UNVERIFIED — evidence boundary

json
{
  "status": "UNVERIFIED",
  "checks": [
    "actual headless TouchDesigner",
    "other TouchDesigner builds",
    "external plugin registration and cooking"
  ]
}

Wave 9: trusted portable components and annotation-aware layout

Wave 9 extends existing tools; it does not add duplicate annotation, layout or package tools. All new TouchDesigner operations use authenticated structured routes and remain available with TDMCP_BRIDGE_ALLOW_EXEC=0.

manage_annotation action:"edit" edits an existing annotateCOMP title, body, RGBA colour and exact x, y, w, h bounds. The bridge resolves every writable alias before mutation, snapshots the complete supported state, applies and reads back each requested field, and restores the snapshot after a partial failure. Text and comment values are redacted from bridge logs and receipts. Text DAT fallbacks created by the legacy create path are intentionally not accepted by edit.

arrange_network keeps its legacy behavior unless annotation_aware:true is set. The opt-in path reads one bounded geometry snapshot, rejects ambiguous overlapping annotation membership, plans groups without raw Python, and applies positions with a snapshot fingerprint. A stale editor context fails before mutation. Docked DATs move by their host delta; resize_annotations:true fits non-empty boxes to their contents plus annotation_padding (default 80). An identical second run reports zero moved nodes.

For portable artifacts, validate_library_asset accepts validation_mode:"deep_roundtrip" only with an authenticated, explicitly quarantined bridge on a port other than 9980. It loads the .tox into a unique scratch holder, waits bounded frames, compares the declared component contract, captures bounded errors/external references, and always attempts cleanup. Missing runtime or missing proof returns UNVERIFIED, never PASS.

make_portable_tox now records a versioned provenance sidecar by default. It binds the final TOX hash to a canonical package-manifest hash, source COMP identity, TD/tdmcp build and only the Git commit/dirty state. It never records tokens, environment values, diffs, project contents or repository roots. Use provenance_policy:"require_clean" (and optionally expected_git_commit) for a strict release preflight; unavailable, dirty or mismatched Git state fails before export. Existing targets still require explicit native overwrite consent, and TOX plus provenance are promoted as one recoverable pair.

The optional help_snapshot inventories bounded operator types and explicitly named TD Python APIs, reads only the installed exact-build OfflineHelp corpus, writes a deterministic docs/td-help index/README, and reruns the quarantine round-trip after attachment. Caps, missing installed pages or build mismatch are reported honestly as UNVERIFIED. attach_docs_as_assets can refresh the same snapshot later and atomically updates an existing provenance manifest hash.

Wave 9 result examples

PASS — exact-build artifact contract and cleanup verified

json
{
  "validation_mode": "deep_roundtrip",
  "roundtrip": {
    "verdict": "PASS",
    "runtime": { "td_build": "2025.32820" },
    "cleanup": { "verified": true }
  }
}

FAIL — strict provenance policy rejects before export

json
{
  "status": "FAIL",
  "code": "git_worktree_dirty",
  "export_started": false
}

UNVERIFIED — bounded help inventory cannot prove every entry

json
{
  "status": "UNVERIFIED",
  "reason": "operator_type_cap",
  "available": 1,
  "truncated": 7
}

Honest result examples

PASS — offline contract and state confirmed

json
{
  "status": "PASS",
  "decision": "Keep",
  "action_applied": "keep",
  "applied": false,
  "final_path": "/project1/noise1"
}

FAIL — an invalid structured operation

json
{
  "status": "FAIL",
  "error": {
    "code": "invalid_parameter_type",
    "message": "pulse: parameter Gain has style Float, expected Pulse"
  }
}

UNVERIFIED — outside the tested runtime

json
{
  "status": "UNVERIFIED",
  "reason": "runtime not exercised",
  "checks": ["actual headless TouchDesigner", "builds other than 2025.32820"]
}

UNVERIFIED is not a pass. The graphical 2025.32820 matrix passed; run separate live validation before depending on the same semantics in headless mode, another build or a show-critical external filesystem.

Deferred follow-ups

OAuth/PKCE arrived as a later opt-in HTTP authorization wave; see OAuth, PKCE & TouchDesigner consent. These waves do not add remote skill catalogs/installers, workspace snapshot/restore, selection-to-component, animated global highlight, broad bridge refactoring, or migration of every destructive command to the broker. Those remain separate follow-ups. Curated bundled skills and build-aware local docs arrived in the next source-tree wave; see Build-aware agent & runtime readiness.