Use when manage multiple local CLI agents via tmux sessions (start/stop/monitor/assign) with cron-friendly scheduling.
日本語の概要は準備中です。原文の説明を表示しています。
How to author one new tdmcp MCP tool the house way — the file shape (XImpl + registerX), the Python-bridge pattern (buildPayloadScript/parsePythonReport over executePythonScript), the result helpers (errorResult/guardTd/jsonResult/structuredResult), fail-forward error handling, ESM/.js + Biome style, and the offline msw unit test that proves it. Load this whenever building, extending, reviewing, or testing a tdmcp tool file under src/tools/** (and its test under tests/unit/**). Use it for every Phase-13 / v0.5.0 feature-build tool and any re-run after a gate failure.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
You build one new tdmcp tool, end to end, matching the repo so closely a reviewer cannot tell which files are new. tdmcp is a TouchDesigner MCP server: a Node/TS server (this repo) drives a Python bridge running inside TouchDesigner. A "tool" is a TS handler that the AI can call; most tools do their work by sending one Python script to the bridge and parsing a JSON report back.
The canonical template is src/tools/layer2/manageComponent.ts. Read it
first, every time. Copy its shape exactly. The rules below explain why each
piece exists so you make the right call on edge cases.
Every tool file exports three things and nothing surprising:
export const xSchema = z.object({ … }) — the Zod input schema. Each field
gets a .describe() written for an AI caller (say what it does and the units).export async function xImpl(ctx: ToolContext, args): Promise<CallToolResult>
— the pure, testable handler. It takes the injected ToolContext
({ client, knowledge, recipes, logger, vault?, allowRawPython }) and the
validated args. All TD work goes through ctx.client. Unit tests call this
directly with a mocked client — so it must never read globals or env directly.export const registerX: ToolRegistrar = (server, ctx) => server.registerTool( name, { title, description, inputSchema: xSchema.shape, annotations }, (args) => xImpl(ctx, args)).Types come from ../types.js (ToolContext, ToolRegistrar). The name is
snake_case (add_custom_parameters); the file is camelCase
(addCustomParameters.ts); the registrar is registerAddCustomParameters.
annotations describe side effects for the client UI. Mutating builders use
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true }. Pure read
tools use { readOnlyHint: true, openWorldHint: true }. A tool that can wipe data
sets destructiveHint: true.
structuredResult + outputSchemaIf the tool's job is to return data for an agent to process (analysis,
inspection), also export xOutputSchema = z.object({...}), pass
outputSchema: xOutputSchema.shape in the registration, and return
structuredResult(summary, data) (see snapshotTdGraph.ts). The text block stays
a one-line summary; the data rides the structured channel so agents read it with
code instead of re-parsing a JSON fence.
Appending parameters, walking children, editing a DAT — these have no dedicated
REST endpoint, so you do them in one Python pass. Do not add REST
endpoints; executePythonScript is the agreed escape hatch.
import { buildPayloadScript, parsePythonReport } from "../pythonReport.js";
const X_SCRIPT = `
import json, base64, traceback
_p = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
report = {"...": ..., "warnings": []}
try:
_c = op(_p["comp"])
if _c is None:
report["fatal"] = "Not found: " + str(_p["comp"])
else:
... # do the work; append per-item failures to warnings
except Exception:
report["fatal"] = traceback.format_exc().splitlines()[-1]
print(json.dumps(report)) # the report is the LAST line of stdout
`;
export function buildXScript(payload: object): string {
return buildPayloadScript(X_SCRIPT, payload);
}
buildPayloadScript(TEMPLATE, payload) replaces
__PAYLOAD_B64__ with base64(JSON.stringify(payload)), so arbitrary artist
strings (quotes, newlines, unicode) can never break Python quoting. Never
string-interpolate user input into Python.op, project, app, absTime,
me exist only in the bridge's exec scope. Reference them only inside the
template string.print(json.dumps(...)). parsePythonReport<T>( exec.stdout) recovers it even if TD logs other lines first. Define a TS
interface XReport { …; warnings: string[]; fatal?: string } for T.const exec = await ctx.client.executePythonScript(script, true);
then parsePythonReport<XReport>(exec.stdout). (The true returns stdout.)Inputs are already validated by the Zod schema. Past that, a handler must never
throw. Wrap the bridge call in guardTd(fn, onOk) (from ../result.js): it runs
fn, and converts any thrown TdError into a friendly errorResult instead of
exploding out of the MCP handler. In onOk, branch on report.fatal →
errorResult(msg, report); otherwise return jsonResult(summary, report).
Inside Python, collect per-item problems into report["warnings"] and keep going —
a partial result that did 4 of 5 things is more useful than a hard failure. Reserve
report["fatal"] for "nothing could be done" (target not found, wrong op type).
The result helpers (all from src/tools/result.ts):
errorResult(message, data?) — isError: true; optional data appended as a
JSON fence. Use for fatal and for pre-flight arg checks the schema can't express.jsonResult(summary, data) — a text summary + pretty JSON fence. The default for
mutating tools.structuredResult(summary, data) — summary + structuredContent. For read tools
(pair with outputSchema).guardTd(fn, onOk) — the try/catch wrapper. Always use it for bridge calls.imageResult, textResult — rarely needed here.Layer-1 tools that build a whole wired network use a different spine:
runBuild(async () => { const builder = await createSystemContainer(ctx, parent, name); await builder.add(type, name); await builder.python(...); return finalize( ctx, { summary, builder, outputPath, controls, extra }); }) (see
createSyncExternalClock.ts). Only follow this if your task explicitly says to
extend a Layer-1 file. New standalone tools use the bridge pattern above.
.js
(../result.js, ../types.js, ../pythonReport.js). Omitting it fails the build.noUncheckedIndexedAccess is on — guard array/object index access../node_modules/.bin/biome check . and
format. Just keep it close.Every tool ships a tests/unit/<tool>.test.ts that runs with no TouchDesigner:
msw mocks the bridge. Copy the closest sibling:
tests/unit/documentNetwork.test.ts for the
structure, but the assertion style of a buildPayloadScript tool: override
POST ${TD_BASE}/api/exec with server.use(...) to capture the script,
pull the base64 out (/b64decode\("([^"]+)"\)/), JSON.parse(Buffer.from(b64, "base64").toString("utf8")), and assert the payload you sent.tests/unit/createSyncExternalClock.test.ts
(captureCreateBodies + captureExecScripts).Boilerplate that every test needs:
import { HttpResponse, http } from "msw";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { makeTdServer, TD_BASE } from "../helpers/tdMock.js";
import { xImpl, xSchema } from "../../src/tools/layerN/x.js";
import { TouchDesignerClient } from "../../src/td-client/touchDesignerClient.js";
import { KnowledgeBase } from "../../src/knowledge/index.js";
import { RecipeLibrary } from "../../src/recipes/loader.js";
import { silentLogger } from "../../src/utils/logger.js";
import type { ToolContext } from "../../src/tools/types.js";
const server = makeTdServer();
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
function makeCtx(): ToolContext {
return {
client: new TouchDesignerClient({ baseUrl: TD_BASE, timeoutMs: 2000 }),
knowledge: new KnowledgeBase(),
recipes: new RecipeLibrary(),
logger: silentLogger,
};
}
TD_BASE is http://127.0.0.1:9980. The default handlers in
tests/helpers/tdMock.ts already answer /api/exec (returns {result:null, stdout:""}), /api/nodes, /api/preview/:seg, topology, etc. To assert on or
shape a response, server.use(...) an override inside the test.
Cover three things, minimum:
fatal (override /api/exec to return a stdout whose JSON report
has "fatal") → result has isError: true and does not throw.expect(() => xSchema.parse(bad)).toThrow()) or xImpl returns an isError result. Assert
it never throws out of the handler.Run your test in isolation before declaring done:
npx vitest run tests/unit/<tool>.test.ts.
Also typecheck — vitest does not. vitest strips types, so it will pass code
that tsc rejects. Run npx tsc --noEmit (or npm run typecheck) too. The most
common trap: a Zod field with .default(...) is required in the impl's
z.infer arg type (the inferred output type), so when your test calls
xImpl(ctx, {...}) directly (bypassing schema parsing) you must pass every
defaulted field explicitly — omit one and vitest is green but tsc fails. Test the
default itself separately via xSchema.parse({}).field.
You create only new files: the tool file and its test. You never edit
src/tools/layer*/index.ts, src/cli/agent.ts, src/prompts/index.ts, docs,
CHANGELOG, ROADMAP, or any other shared file — the lead wires your registrar
into the layer index and adds the matching CLI command after you finish. This is
how parallel builders avoid stepping on each other (one shared file edited by many
agents = merge hell). If you think a shared file must change, say so in your
final report; do not edit it.
Do not invent operator types or TD API calls. When unsure whether
op.appendXYZ/a parameter name/an extension par exists, probe it (the script
can read dir(comp) / comp.pars() and report) or consult the operator knowledge
base under src/knowledge/data — never guess a method that fails silently at
runtime.
xSchema, xImpl, registerX (+ xOutputSchema if a read tool).buildPayloadScript + parsePythonReport; all TD globals
live inside the script string; payload is base64 (no string interpolation).guardTd around the bridge call; fatal →
errorResult; per-item issues → warnings..js; Biome-clean (2-space, double quotes,
trailing commas).npx vitest run tests/unit/<tool>.test.ts.registerX → which index.ts; xImpl/xSchema → CLI key).まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Use when manage multiple local CLI agents via tmux sessions (start/stop/monitor/assign) with cron-friendly scheduling.
日本語の概要は準備中です。原文の説明を表示しています。
Use when a hybrid memory system that provides persistent, searchable knowledge management for AI agents (Architecture, Patterns, Decisions).
日本語の概要は準備中です。原文の説明を表示しています。
Reference for how BDB structures autonomous software engineering work — the seven-node dispatcher graph (Architect, TechLead, UI/UX, Engineering, Media/EventTech, Reviewer, Shipping) that /startcycle-graph actually runs. Use when you need the high-level lifecycle framing without inventing your own process.
日本語の概要は準備中です。原文の説明を表示しています。
Tools are how AI agents interact with the world. A well-designed tool is the difference between an agent that works and one that hallucinates, fails silently, or costs 10x more tokens than necessary. This skill covers tool design from schema to error handling.
日本語の概要は準備中です。原文の説明を表示しています。
Harness patterns for coding agents — memory, permissions, context engineering, delegation, skills, hooks, bootstrap.
日本語の概要は準備中です。原文の説明を表示しています。
agenttrail: live map of a multi-agent build in the browser: which plan component is being worked on, by which agent or harness, what is done and what is stuck. Use when a multi-agent pipeline starts (/startcycle, /startcycle-graph, /teamwork-preview) or after a plan-canvas approve, when the user asks to see what the agents are doing ("live map", "build board", "who is running now"), or by yourself whenever a multi-agent run is under way — no user prompt needed.
日本語の概要は準備中です。原文の説明を表示しています。