td-atlas RU·EN Try it

Working on td-atlas

Перевод английской документации от 15.09·English

Guidance for agents contributing to this repository. plugin/skills/touchdesigner/SKILL.md covers using the connector to drive TouchDesigner.

What this project is

Three layers, separable:

Layer Needs TouchDesigner? Where
Layer Atom index — operators, parameters, docs, palette Needs TouchDesigner? No, once built Where src/td_atlas/atoms/
Layer Bridge — live control of a running instance Needs TouchDesigner? Yes Where src/td_atlas/bridge/ + component/
Layer Project reader — .toe/.tox from disk Needs TouchDesigner? No Where src/td_atlas/project/

mcp/server.py exposes all three, and cli.py is the same functionality for humans. Keep that parity. A capability added to one appears in the other, or the gap is declared in the parity section below, which tests/test_cli_mcp_parity.py holds to the code.

The MCP surface cannot aim at a chosen instance. That one gap is declared in prose, since it is about the global flags and not about any one capability. The CLI's global --port/--project have no MCP equivalent, and every bridge tool dials whatever BridgeClient.discover() picks. td_instances lists every running bridge and marks the one these tools reach, and _warn prefixes the ambiguity warning onto every bridge result whenever more than one is running, so an agent cannot edit the wrong project in silence. A tool-side selector would cost a guard at every bridge() call site, since discover() raises InstanceSelectionError when a flag names no bridge or several, and an MCP tool has to answer in text.

CLI ↔ MCP parity

tests/test_cli_mcp_parity.py compares the two surfaces capability by capability. It reads the four tables below, walks build_parser() and the @mcp.tool() definitions, and fails when the code and the tables disagree.

A capability is one MCP tool, or one CLI subcommand. project and project variant are counted by their actions (project read, project variant save, …), which is the level at which an MCP tool corresponds to anything.

Paired

Seventeen capabilities exist on both sides. Where the two spell an argument differently, the rename is declared here, and the test applies it before comparing the argument sets. The CLI's global --db/--port/--project are excluded as the declared gap above.

CLI MCP Renames
CLI instances MCP td_instances
CLI status MCP td_status
CLI doctor MCP td_doctor
CLI search MCP td_search_operators
CLI op MCP td_operator_schema Renames typeop_type
CLI exec MCP td_exec
CLI render MCP td_render
CLI log MCP td_log Renames numberlimit
CLI project read MCP td_project_read
CLI project text MCP td_project_text
CLI project write MCP td_project_write
CLI project grep MCP td_project_grep
CLI project diff MCP td_project_diff Renames filebefore, otherafter, movesshow_moves, no_textinclude_text
CLI project variant save MCP td_variant_save
CLI project variant list MCP td_variant_list
CLI project variant restore MCP td_variant_restore
CLI project variant diff MCP td_variant_diff Renames labelbefore, otherafter, movesshow_moves, no_textinclude_text

CLI only

Nine capabilities. In every row the reason has the same shape: something a person does to this machine once, or something that writes a directory to disk.

CLI Why a person runs it
CLI install Why a person runs it Stages the bridge and prints a line to paste into TouchDesigner. An agent that could install the bridge would need the bridge to do it.
CLI release-tox Why a person runs it Builds a distributable .tox of the bridge. Maintenance of this project, not use of it.
CLI build Why a person runs it Rebuilds the index from a TouchDesigner installation — 13–16 s of one-off setup, and td_doctor already says to run it.
CLI probe Why a person runs it The same, for the runtime half of the index: it needs a running instance whose cook it will occupy for a minute.
CLI reload Why a person runs it Replaces the running bridge's own handler. A tool that can restart its own transport reports its outcome to nobody.
CLI mcp Why a person runs it Starts the MCP server. It is how the tools exist; it cannot be one of them.
CLI project expand Why a person runs it Unpacks a project into a directory and prints the path. Its whole output is a filesystem location an agent cannot read from.
CLI project collapse Why a person runs it Repacks such a directory into a .toe/.tox. The MCP side writes projects with td_project_write, which takes text, not a directory.
CLI project scripts Why a person runs it Writes every DAT's contents out as files. td_project_text gives an agent the same content in one string.

MCP only

Twenty-four, in three groups. The CLI's live surface is the few commands a person types at a terminal, which are exec, render, status, instances and log. A bridge capability gets a subcommand when someone wants to type it, not for symmetry.

MCP Group Why an agent calls it
MCP td_docs, td_glossary, td_example, td_expression_help, td_python_api, td_palette, td_search_parameters, td_op_info Group index lookups Why an agent calls it A person reads the wiki, the palette browser and the operator's own help; search and op cover what a terminal is actually better at.
MCP td_build, td_set_params, td_flags, td_set_flags, td_network, td_errors, td_annotate, td_annotations, td_undo, td_snapshot, td_extension_add, td_palette_load, td_health Group live editing Why an agent calls it A person editing a network does it in TouchDesigner, where the result is visible. These exist because an agent cannot see the network.
MCP td_claim_scope, td_release_scope, td_scopes Group scope claims Why an agent calls it An agreement between agents about which subtree each may touch. A person at a terminal is the party the claims protect, not one of the claimants.

Declared argument divergences

Everything else must match. These do not, and each row says why; anything not listed here fails the test.

Pair Divergence Why
Pair op / td_operator_schema Divergence --groups is CLI only Why The tool returns parameter members (tx, ty) and never the documented groups (t), because an agent that sets t gets a refusal it cannot read. The flag exists for a person cross-reading Derivative's own docs, which name the groups.
Pair render / td_render Divergence -o/--output is CLI only; width defaults to 512 in the tool and to the TOP's own resolution in the CLI Why The tool hands the image back inline, so it has nowhere to write and every pixel costs context; the CLI writes a file, where the artist's own resolution is the right answer.
Pair project read / td_project_read Divergence --refresh is CLI only Why The expansion cache is keyed on the file's path, size and mtime, so a changed project is re-expanded without asking. The flag is a repair for a cache damaged by something outside this program — a person's problem, diagnosed at a terminal.
Pair project text / td_project_text Divergence --refresh and -o/--output are CLI only; max_bytes is MCP only Why --refresh as above. The tool must fit its answer in a context window, so it refuses a network over max_bytes; the CLI writes to a file or a pipe, where there is no such ceiling and -o is the whole point.
Pair project grep / td_project_grep Divergence --fixed is CLI only Why An agent composing a pattern can escape it; a person typing v1.2.3 at a prompt cannot be asked to.
Pair doctor / td_doctor Divergence --install-path and --clear-cache are CLI only Why Both change this machine — one points the index at another installation, the other deletes every cached expansion. Repairs belong to whoever owns the machine.

The rule that matters most here

Measure, do not guess. Almost none of what this project depends on is documented by Derivative. That covers the .toe container format, the parameter flag bits, the contracted type names, and which noise types run on the CPU. Every one of those was settled by running an experiment against the shipped example libraries or a live instance, and each finding carries a comment saying what was measured and how many cases it covers.

Two of these were nearly shipped as plausible guesses, and both would have been wrong:

If a heuristic can be confidently wrong, prefer leaving the answer unresolved, or derive the real mapping from TouchDesigner.

Invariants a change has to keep

Two gates are not obvious from the code that has to satisfy them, and a named test holds each. project/rebuild.py and tests/test_health.py point here for the rule they serve.

Running things

bash
uv pip install -e . pytest
pytest                       # the default run: no TouchDesigner needed
pytest -m live               # the rest: needs one running, with the bridge

A plain pytest does not need TouchDesigner running. Keep it that way. [tool.pytest.ini_options] in pyproject.toml deselects the live marker by default, so a test that dials a live instance is not collected at all unless it is asked for by name. A new test that reaches a running instance gets @pytest.mark.live, or a module pytestmark. --strict-markers rejects a misspelling.

Some tests need TouchDesigner installed, since they shell out to toeexpand and toecollapse, or read a shipped example, and they skip without one. Those stay in the default set, and the same checks pass on any machine with the application present.

Rebuilding the index after changing an extractor:

bash
td-atlas build               # offline, 13-16 s
td-atlas probe               # needs TouchDesigner open with the bridge
td-atlas reload              # push handler changes into a running instance

td-atlas reload is the fast loop for component/handler.py. It re-stages the sources and has the running bridge replace its own handler, so there is no need to return to the textport.

Packaging

bash
.venv/bin/python scripts/build_mcpb.py     # bundle + registry submission

Two rules it enforces, both of which a hand-maintained manifest breaks inside one release:

The bundle uses the uv server type, so it ships source and pyproject.toml and lets the host resolve dependencies. A python-type bundle would have to carry mcp's compiled pydantic-core wheel, which is built for one CPU and one Python minor and cannot be produced reproducibly from a clean checkout. The launch string stays the one cli.mcp_command() settled on, which is -m td_atlas.cli mcp. Only the interpreter differs, since a bundle has no sys.executable of the user's to point at.

scripts/publish.sh is the only thing here that sends anything outward, and nothing calls it. Before the first gh it gates on five things, cheapest first. A clean working tree, untracked files included, since git archive HEAD packs neither. A bundle whose dist/build.json names the current HEAD. A v<version> tag that does not exist yet. A green pytest. Last, and the only gate that needs the network, a git ls-remote saying the tag is not on origin either. That last one catches a tag deleted here but still published there, which is what a half-finished release leaves behind. A ls-remote that fails to answer fails the gate. The SHA-256 in dist/server.json must match the bundle too, since clients verify that hash before installing.

Code that runs inside TouchDesigner

src/td_atlas/component/handler.py and bootstrap.py execute in TouchDesigner's embedded Python 3.11, not the host interpreter. Constraints:

Any change to the METHODS table raises PROTOCOL_VERSION, and MIN_PROTOCOL_VERSION follows it. Adding or removing a method counts. So does a change to a method's name, to the set of parameters it reads, or to the shape of the reply the host reads back. The case this guards is a bridge component left inside a project across an upgrade, since host and bridge ship together in one .mcpb bundle. Such a bridge answers with the number it was laid down with, and if that number did not move, the host cannot tell it from a current one, and the failure it produces is a plausible-looking answer to a different question. tests/test_protocol_fingerprint.py derives the method names and their parameter keys from the source and compares them with a listing recorded against the current number. Reply shape it cannot see, and that half is held by this rule, the review, and the CHANGELOG.md obligation in CONTRIBUTING.md.

Probe snippets in atoms/probe.py are %-formatted templates. A literal % inside one must be written %%.

Adding an MCP tool

  1. Write the function in mcp/server.py with a docstring aimed at an agent. The docstring is the interface, so say when to reach for it and what trap it avoids, not just what it returns.
  2. Put @guarded under @mcp.tool(). It catches BridgeUnavailable and BridgeError and returns the message with a repair hint attached; an exception that escapes surfaces to the agent as an opaque ToolError. tests/test_recovery_hints.py asserts that every bridge tool carries the decorator, so a new tool without it fails.
  3. Return types feed a generated output schema. A union of Image | str fails to generate; leave the annotation off when a tool can return either.
  4. Add it to plugin/skills/touchdesigner/references/tools.md.

Search ranking

Full-text search has been wrong twice in the same way, so check any new search surface against real queries before believing it.

FTS5 ANDs bare terms, which makes plain-language questions match nothing, and AtomStore.fts_query ORs them. But ORing lets an operator matching one common word outrank the one that fits, so td_search_operators and td_palette both search names and labels first and widen to body text only when the result set is thin. AtomStore.match additionally re-parses any query that looks like FTS5 syntax, since prose can accidentally contain NEAR( or an unbalanced quote and SQLite raises.

Conventions

Things to be careful with

Verified facts worth not re-deriving

Recorded in docs/formats.md with the evidence. The payload prologue is 27 bytes ending in a length field (686/686 files). Parameter flag bit 0x10 marks expression mode (14,835/14,835 lines across 2,861 files). There are 71 contracted type names. The wiki mirror puts pages whose title contains a slash into subdirectories.

Contributing

The contributor guide is AGENTS.md. It holds the conventions, the rules for the code that runs inside TouchDesigner, and the CLI↔MCP parity tables a new capability has to appear in. Read it before changing anything.

Run the tests

bash
uv pip install -e . pytest
pytest

No test fails for want of a running TouchDesigner. Tests that need an installed application skip themselves and say so, so a plain pytest is green on a machine with neither TouchDesigner nor an index.

Tests that need a live instance with the bridge installed sit behind the live marker, and a plain run does not collect them:

bash
pytest -m live          # needs TouchDesigner running with the bridge

With nothing listening they skip, and the run stays green.

Lint

bash
uvx ruff check .

The rule set is narrow, E4, E7, E9, F, W. Widening it means changing the code, not editing a line of config, and the reasoning is in AGENTS.md.

Before opening a pull request

Reporting a problem

Open an issue with the TouchDesigner build, your OS, and the output of td-atlas doctor. The template asks for exactly that.