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:
- Find code:
tcm_find,tcm_search,tcm_view. - Change it:
tcm_patchfor a small change; for a bigger one,tcm_editinto a scratch file, edit that with its own file tools, thentcm_checkandtcm_update. - Fix what
tcm_todolists. 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_checkandtcm_updateimport 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_viewshows 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 difflists 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 (
alllists them); tcm_todolists the first 20 dependents to fix (allfor 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
maindoesn't), the codebase by namespace, scratch files holding unsaved work (and the session's directory), what needs fixing, other branches with workmaindoesn'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'slines. Text as written, or a regular expression/…/(regexis the old way);undera 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-) orgrep(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_updatethat 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_viewshows it, checked and saved astcm_updatedoes. 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 astcm_viewnumbers it). Imports follow the code; one that can't be found that way (an alias) goes inimports([{"name": "isImportDeclaration", "from": "typescript", "as": "ts_isImportDeclaration"}]).patcheschanges several definitions and pages as one update and one history entry. In a template literal,oldandneware as the source writes them, escapes included ("\\n"in JSON for the source's\n); anoldfound only with escapes read the other way is matched once and said; anewthat 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 (
updatereplaces 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 (
keepkeeps it).intofor a file kept as written, astcm_editsays. 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:
sideoursortheirs, astcm_todolabels them, or ahash tcm_test,tcm_run- tests (cached by hash;
namesfor some,freshto run all) and suites (definitionsto leave them out,suitesfor only them;binruns 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
nentries, 10 by default; re-hashed definitions counted unlessall) tcm_merge- land the session's branch (or
branch, an earlier session's) onmain(orinto), only withapproved: see landing.messageis required: what the branch did and why. It reports what landed by name and count, or, if mergingmainin stops, the conflicts and what no longer typechecks; of failing tests, only the failures.backgroundruns 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;
namefor one definition's versions, callers, slowest calls and errors;errorsgrouped by the definition that threw them;tracefor 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;waitwaits up to that many seconds;stopstops it. Without an id, the session's jobs (background jobs) tcm_cli- any other command (CLI).
cwdruns it on the codebase in another directory (tcm -C);buildruns 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
messageon 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.branchon 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.backgroundontcm_test,tcm_run,tcm_cliandtcm_merge: start the command and answer at once;tcm_jobfollows 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_BRANCHnames it, for the CLI too), including changes to only comments or docs:mainreads them as before until the session lands, andtcm resetandtcm revertundo them (history).Has its own files in
.tcm/sessions/<branch>/: scratch files,#codebaseimports 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-84f401is.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
maindoesn't:tcm statuscounts 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_mergeistcm merge --squashonmain(landing):- it merges
maininto the session's branch; conflicts or broken dependents wait there, with scratch files holding what to settle, for the agent to fix andtcm_mergeagain; - it runs the tests the change reaches;
- if they pass, it lands the branch on
mainas one squashed entry, so pushingmainsends what the session ended with, not every step (unless it made releases: then its history comes too).tcm revertundoes the landing alone; - 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> --squashonmain.- it merges
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
maindoesn'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 frommain, 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, astcm_statuslists), if no other server is on it. Not ontomain: work lands there withtcm_merge. The branch it leaves goes ifmainhas all of it.Reads other branches without moving: the read-only tools take
branch(tcm_viewwith"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
mainhas all of it and no scratch file holds unsaved work. Otherwise it stays, andtcm statuslists it under "branches with work main doesn't have", for you to land, another session to land (tcm_mergewithbranch), or an agent to go on with (tcm_cli ["switch", "<it>"], ortcm 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 statuslists those).tcm branch <b> --deleteremoves 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). stopstops 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.