AI agents — tcm-ts

AI agents

tcm mcp is a Model Context Protocol server that lets a coding agent work in a codebase: find definitions, change them, fix what a change broke, and run the tests it reaches. Claude Code, Cursor, Codex and other MCP clients pick it up.

Setting it up

tcm init does it. It adds the server to the project's .mcp.json:

{ "mcpServers": { "tcm": { "command": "tcm", "args": ["mcp"] } } }

and writes a short section about tcm into AGENTS.md, for agents that read that instead. A .mcp.json that already has a tcm server, or an AGENTS.md that already mentions tcm, is left alone.

For another client, register tcm mcp (stdio) as a server run in the codebase's directory.

Start your agent there and ask it for a change. It works on a branch of its own, and lands it on main only when you tell it to.

How the agent works

The server's instructions teach the agent the loop:

  1. Find code: tcm_find, tcm_search, tcm_view.
  2. Change it: tcm_patch for a small change; for a bigger one, tcm_edit into a scratch file, edit that with its own file tools, then tcm_check and tcm_update.
  3. Fix what tcm_todo lists.
  4. tcm_test.

Each change gets a message saying what it's for. The instructions also say what pays off here:

  • keep definitions out of cycles (saving warns about ones worth avoiding);
  • let imports follow the code: tcm_patch, tcm_add, tcm_check and tcm_update import what the code uses, and saving drops imports nothing uses;
  • change together what changes together (a class and what's typed with it, in one scratch file);
  • patch doc comments as tcm_view shows them;
  • view saved code again rather than keeping copies of it in files.

Each tool runs the tcm command of the same name, so it does what that command does at a terminal. Results say what to do next where there's a next step (dependents to fix, a check to save).

Output is brief, since an agent reads every line (TCM_BRIEF=1 does the same for the CLI):

  • a save lists what changed (a type only where it's new or changed); re-hashed dependents, cycle-mates and lint warnings are counts (tcm diff lists them);
  • a test run lists passing tests by name under their shared namespace (cached ones too when you named tests or one failed, otherwise counted); a failure shows the start of its message and where it failed, with the full report in a file it names;
  • a history entry lists what it edited, added and removed; definitions only re-hashed are counted (all lists them);
  • tcm_todo lists the first 20 dependents to fix (all for every one);
  • a merge lists its changes by name and count.

A client that asks for progress gets each line of output as it comes (MCP progress notifications), so a long tcm_test shows suites as they finish.

Tools

tcm_status
where things stand: the branch (in a session, how many entries and names it has that main doesn't), the codebase by namespace, scratch files holding unsaved work (and the session's directory), what needs fixing, other branches with work main doesn't have, session directories left with unsaved work, the mirror
tcm_find
names, docs and signatures; :<type> searches by type
tcm_search
text in code and pages, as find in files: each line with it, by definition, numbered as tcm_view's lines. Text as written, or a regular expression /…/ (regex is the old way); under a namespace or definition (finding code)
tcm_view, tcm_docs
definitions with current names, as Markdown: a line with names, hash and placement (and, from the app's traces, this version's calls in the last day, p50, p95, failures, how long pages waited, errors thrown), then the code (or a file's content). lines (120, 120-180, 120-, 1-20,90-) or grep (text, or /…/) for part of a large one, numbered. A missing name is said first, with likely matches or what a namespace holds; it's an error only if none are found. tcm_docs: documentation with examples evaluated
tcm_deps, tcm_dependents
what a definition uses, and what uses it
tcm_edit
definitions into a scratch file, returning its path and contents; a file holding unsaved work keeps it and gets them added. A conflicted name gets its own file (lib.k.ts), its two sides merged from the base with conflict markers where both changed the same lines, saying what each side changed and whether the merge typechecks; one using another conflicted name shares its file. tcm_update that file, markers out, to resolve it (resolve <name> (merged) in history; merging)
tcm_patch
a small change in one step: find-and-replace on a definition's code as tcm_view shows it, checked and saved as tcm_update does. Nothing is saved if an edit doesn't apply (the nearest text is said) or the result doesn't typecheck (the problem's line numbered as tcm_view numbers it). Imports follow the code; one that can't be found that way (an alias) goes in imports ([{"name": "isImportDeclaration", "from": "typescript", "as": "ts_isImportDeclaration"}]). patches changes several definitions and pages as one update and one history entry. In a template literal, old and new are as the source writes them, escapes included ("\\n" in JSON for the source's \n); an old found only with escapes read the other way is matched once and said; a new that would end or split the literal (a bare backtick or ${) is refused, saying how to escape it
tcm_add
code given in the call, saved into a namespace without a scratch file (update replaces definitions of the same names)
tcm_at
which definition is at a line of a file (a stack trace's, a search result's), or every definition in a file with its lines
tcm_check, tcm_update
what saving a scratch file would do; saving it, then deleting it (keep keeps it). into for a file kept as written, as tcm_edit says. A definition saved at a conflicted name resolves the conflict (resolves). Code that doesn't parse is reported by its first syntax error
tcm_todo
conflicts (each side labelled ours or theirs, with its branch), dependents that stopped typechecking (the first 20, unless all), code using a version nothing names
tcm_resolve
settle a conflicted name by keeping one side whole: side ours or theirs, as tcm_todo labels them, or a hash
tcm_test, tcm_run
tests (cached by hash; names for some, fresh to run all) and suites (definitions to leave them out, suites for only them; bin runs them against another build, {"tcm": "<tcm.js>"} or {"tcm": "known-good"}); running a definition
tcm_mv, tcm_rm, tcm_history
renaming, removing, history (the latest n entries, 10 by default; re-hashed definitions counted unless all)
tcm_merge
land the session's branch (or branch, an earlier session's) on main (or into), only with approved: see landing. message is required: what the branch did and why. It reports what landed by name and count, or, if merging main in stops, the conflicts and what no longer typechecks; of failing tests, only the failures. background runs it as a job
tcm_traces
what the app did, from its traces: by definition, with what runs worse than its previous version and the change that did it; name for one definition's versions, callers, slowest calls and errors; errors grouped by the definition that threw them; trace for one trace as a tree; since (30m, 24h, 7d), limit
tcm_job
a background command (or tcm_merge): how it's going (a test run: how many finished, what failed) or how it ended; wait waits up to that many seconds; stop stops it. Without an id, the session's jobs (background jobs)
tcm_cli
any other command (CLI). cwd runs it on the codebase in another directory (tcm -C); build runs it with a kept build (known-good, previous, an id). ["switch", "<branch>"] moves the session (below)
tcm_guide
longer explanations by topic: the model, editing, testing, apps, packages and releases, mirrors, troubleshooting

Options several tools share

  • message on the tools that change names (tcm_patch, tcm_update, tcm_add, tcm_mv, tcm_rm, tcm_resolve): what the change is for, a line saying what and why, more after a blank line if needed. It's the history entry's title (messages). tcm_cli ["describe", "<n>", "-m", "…"] changes one later.
  • branch on the tools that only read (tcm_status, tcm_find, tcm_search, tcm_view, tcm_deps, tcm_dependents, tcm_docs, tcm_todo, tcm_history, tcm_at): read that branch (another session's, say) as it is, changing nothing. The session stays on its own.
  • background on tcm_test, tcm_run, tcm_cli and tcm_merge: start the command and answer at once; tcm_job follows it.

A call with an option its tool doesn't have is refused, naming the option, the build answering and the options it has (tcm_cli has no `cwd` option in this build (#ycaei6cx): known are args, background). A call written for a newer tcm does nothing rather than something else.

Branches: sessions

An agent's work stays on a branch until you say to land it, as on a git branch. Each tcm mcp is a session on a branch of its own (session/<date>-<id>), so several agents can work in one codebase at once.

(none)
a new session branch, or the one this directory's last session left holding work
--branch agent/a
that branch (made if new, gone on with if not); refused if another server is on it
--direct
no session: work on the codebase's own branch, changes landing as they're saved

(--session asks for the default, for .mcp.json files that still pass it.) Two servers never share a branch.

A session:

  • Works on its own branch, made from the codebase's own. Every command it runs is on that branch (TCM_BRANCH names it, for the CLI too), including changes to only comments or docs: main reads them as before until the session lands, and tcm reset and tcm revert undo them (history).

  • Has its own files in .tcm/sessions/<branch>/: scratch files, #codebase imports and definition views (src/, which its stack traces point into). The directory is the branch name with each character other than a letter, digit, _, . or - made _: session/2026-10-31-84f401 is .tcm/sessions/session_2026-10-31-84f401/. A scratch file named by itself (scratch.ts) goes there; one with a path goes where the path says.

  • Leaves the repository's files alone. A mirror follows the branch it was linked on (main), so the work reaches the files when it lands.

  • Says what it has that main doesn't: tcm status counts its history entries and changed names.

  • Lands with tcm_merge, when you say so. The agent asks you once the change is done and tested, and lands nothing unless it says you told it to (approved: true). tcm_merge is tcm merge --squash on main (landing):

    1. it merges main into the session's branch; conflicts or broken dependents wait there, with scratch files holding what to settle, for the agent to fix and tcm_merge again;
    2. it runs the tests the change reaches;
    3. if they pass, it lands the branch on main as one squashed entry, so pushing main sends what the session ended with, not every step (unless it made releases: then its history comes too). tcm revert undoes the landing alone;
    4. it merges the landing back into the session's branch, so further work lands next without conflicts.

    You can land a branch yourself: tcm merge <branch> --squash on main.

  • Goes on after a restart. A server started again (tcm replaced, a crash, the client reconnecting) from the same directory goes on with that directory's last session branch, if it holds work main doesn't have (or unsaved scratch work) and no other server is on it. Its instructions say so; its scratch files and background jobs are where they were. A server whose branch was deleted under it makes it again from main, and says so.

  • Switches without a restart: tcm_cli ["switch", "<branch>"] moves the session, every tool included, onto a branch it owns (an earlier session's holding work, as tcm_status lists), if no other server is on it. Not onto main: work lands there with tcm_merge. The branch it leaves goes if main has all of it.

  • Reads other branches without moving: the read-only tools take branch (tcm_view with "branch": "session/2026-10-31-84f401"), and the CLI --branch. The branch is read as it has it, comments, docs and history included; nothing changes it, and the session's own files stay as they are. A command that changes things refuses --branch. (TCM_BRANCH=<b> in a shell makes write commands act on that branch too.)

  • Is cleaned up. When its server stops, a session branch goes, with its directory, if main has all of it and no scratch file holds unsaved work. Otherwise it stays, and tcm status lists it under "branches with work main doesn't have", for you to land, another session to land (tcm_merge with branch), or an agent to go on with (tcm_cli ["switch", "<it>"], or tcm mcp --branch <it>). A killed server can't clean up: the next server to start does it for session branches no running server is on, on the same terms, and for session directories whose branch is gone (unless they hold unsaved work; tcm status lists those). tcm branch <b> --delete removes the directory too, unless it holds unsaved work, and refuses while a server works on the branch.

Test passes are cached by hash for everyone: a test one session ran doesn't run again in another for the same code. Tests don't see the session's branch: a test that opens a codebase of its own finds it as it is.

A session also lets you keep working while something long reads main (pnpm verify in tcm's repository): its changes don't touch main until they land. In tcm's repository, a session's verify (tcm_cli ["run", "tcmts.dev.verify"]) builds in a place of its own (developing), so sessions' verifies never wait for each other; only main's waits its turn.

Test runs from all sessions (tcm_test, a tcm_merge's, a verify's) share one budget: no more suites run than the machine has slots for. A run that waits says so once, and on whom; the report keeps one line of it, or the total (…, 6m12s waiting for slots).

Background jobs

A job (background: true) is a process of its own, so it outlives the server that started it. When the server is started again (a new tcm, a crash), the job runs on, and the new server's tcm_job lists it ("started by an earlier server"), follows it with wait, and reports how it ended with all it printed. A tcm_merge running when its server is replaced still lands, or fails as it would have. Job ids go on across servers: a session's job 3 is always the same job.

  • Files: .tcm/sessions/<branch>/jobs/ (.tcm/jobs/ for --direct): <id>.json (what it runs, when, its process), <id>.log (its output as it comes), <id>.exit (how it ended).
  • stop stops what the job runs (its process group: asked, then forced after five seconds) and shows what it printed.
  • A job whose process vanished without saying how it ended (killed from outside, or with the machine) is reported so, with its output and log path. An id the session never gave is said so, with the ids it has.
  • A job's files are cleared a day after it ends (its id isn't reused), and go with the session's directory.

A new tcm, without restarting

When tcm is replaced (upgraded, or a new known-good build verified in tcm's repository), tcm mcp starts the server again at once and tells the client its tools changed (notifications/tools/list_changed). New tools and fixes reach a running agent without restarting it. Requests already asked are answered by the old server; jobs it started run on, and the new server follows them.

Without MCP

The CLI is the same workflow: tcm edit <name>, edit the scratch file, tcm check, tcm update, tcm todo, tcm test. tcm help lists every command.

Built from c482a14e