MCP
chronos mcp <folder> [--allow-merge | --agent NAME] [-b branch]
Serves a database folder to an AI agent as MCP tools, over stdio; chronos serve serves the same tools over HTTP, to an agent with its token (below). Why worlds work the way they do: concepts. Who may do what: security.
claude mcp add chronos -- chronos mcp /path/to/mydb
Modes
| Command | Acts as | Tools listed | Changes |
|---|---|---|---|
chronos mcp mydb | safe mode (the default) | 27: all but merge, restore, undo_merge | only worlds forked in this session |
chronos mcp mydb --agent bot | the agent bot | those bot has the rights for (below) | what bot may change; its forks stay its own across sessions |
chronos mcp mydb --allow-merge | the database's own user | all 30 | everything |
--agentwins: with both--agentand--allow-merge, the session acts as the agent.--agenttakes a name, not a token: an unknown or disabled agent is refused at start (chronos: ..., exit 1). Anyone who can runchronos mcpon the folder can name any agent; see security.-b branch: the branch must exist or the command exits 1. Tools don't use it: each call names its ownbranch(defaultmain).- The folder already open: if
chronos serve, the shell or anotherchronos mcphas the folder open,chronos mcpjoins that process over its Unix socket and gets the same modes.--token,--safeand--admin-tokendon't apply to it.
Adding it to a client
Claude Code:
claude mcp add chronos -- chronos mcp /path/to/mydb
claude mcp add chronos -- chronos mcp /path/to/mydb --agent bot
Any client that starts stdio servers from a JSON config (mcpServers):
{
"mcpServers": {
"chronos": { "command": "chronos", "args": ["mcp", "/path/to/mydb"] }
}
}
Use an absolute folder path: the client picks the working directory.
Over HTTP (chronos serve)
chronos serve answers MCP at POST /mcp, as the agent whose token the client sends (Authorization: Bearer <token>), so the client needs only that token: not the folder, not the chronos binary. This is how an agent in a container or on another machine connects (see agents in containers).
claude mcp add --transport http chronos https://db.internal:7070/mcp --header "Authorization: Bearer $BOT_TOKEN"
- Who it acts as: always an agent, the same as
chronos mcp --agentwith that agent's name: its rights decide which tools are listed and what they may change, its quotas apply (max_concurrentcounts each tool call, and each statement of asqlcall; the SQL any tool runs has itsmax_query_msandmax_memory_mb), and what it does is in the audit trail.describeshows no rows of a table a row-level security policy limits it on. Under--safe, a request with no agent's token is the agentguest: one with no token on a server without--token, or with the server's token. Every such client is the same guest, sharing its worlds (any of them can change or discard what another guest forked) and its rights, and the guest has no quotas: give each agent a token of its own. The server's token, the admin token, and no token on a server without--safeget403: the database's own user useschronos mcp <folder>on the database's machine. A wrong, disabled or dropped agent's token gets401, and wrong tokens back off per address as on the rest of the API. - Transport: MCP's Streamable HTTP, without its event stream. Each request's body is one JSON-RPC message, and a request's reply is the response's JSON body (
200). A notification, or a client's response, gets202with no body. A batch (a JSON array) gets400(-32600), and a body that isn't JSON gets400(-32700).GET /mcp(an event stream) gets405. There are no session IDs: nothing carries over between requests, as nothing does between stdio's calls. - Sizes: a message may be at most 8 MB (
413from itsContent-Length, before the body is read). Over HTTP and stdio alike,findandhistorygive at most 1000 rows a call,findpages tooffset100000 with a beam (ef) up to 4096, and past those the call says to read with thesqltool. - The rest of the API's rules apply: loopback only unless the server has a token and TLS; a request from a web page (with an
Originheader) gets403; the body must be sent withContent-Type: application/json.
Protocol
- Transport: stdio, JSON-RPC 2.0, one message per line. stdout carries only protocol messages; blank lines are skipped. Over HTTP, see above.
- Protocol version: the server answers
initializewith the client'sprotocolVersion, or2025-06-18if it sends none. - Capabilities:
toolsonly. No resources, no prompts. - Methods:
initialize,ping(replies{}),tools/list,tools/call. A message without anid(a notification such asnotifications/initialized) gets no reply. - Server info:
{"name": "chronos", "version": "<chronos version>"}, plusinstructionsthat teach the workflow:describe,fork, change your branch,diff, thenmergeordiscard. The instructions follow what the session may do. If it may not merge (safe mode, or an--agentwithoutmergeormerge_own), they say a person merges withchronos <database folder> merge <branch>, and to report the branch name. If it may merge its own branches (--allow-merge, or an--agentwithmerge_own,mergeoradmin), they tell it to callmergewhen its diff is right, and that a merge policy may hold the merge for a person: then it stops and reports the branch and the reasons. - State: none between calls, except the worlds a safe-mode session forked. Each
sqlcall is its own session: a transaction mustBEGINandCOMMITwithin one call.
Errors
A tool that fails replies with a normal result, isError: true, and a message saying how to fix the call:
{"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text","text":"'id' is required: a non-empty string without spaces"}],"isError":true}}
Refusals (safe mode, an agent's rights), unknown tools (unknown tool 'x'; see tools/list), SQL errors and merge conflicts all come back this way. Protocol errors use JSON-RPC error:
| Code | When |
|---|---|
| -32700 | the line isn't JSON (id is null) |
| -32601 | unknown method, resources/list and prompts/list included |
- Names:
table,id,name,branch(where required) andworldmust be non-empty strings without spaces. - Results are text: the shell's output for the same command, SQL results as a psql-style table (
name\n--------\n Ada L.\n(1 row)).
Tools
Every tool that reads or writes rows takes branch (string, default main). A branch as it was, name@when, works wherever a branch is read (see worlds).
| Tool | Does | Safe mode |
|---|---|---|
describe | tables, fields, a sample row | yes |
find | filter, text and vector search | yes |
sql | run SQL | reads; writes on its own worlds |
get | read one row | yes |
put | create or replace one row | on its own worlds |
delete | delete one row | on its own worlds |
fork | create a branch | yes |
set_meta | set a branch's notes | on its own worlds |
branches | list branches | yes |
diff | what changed | yes |
history | recent events of a branch | yes |
restore | put a branch back to a moment | not listed |
merge | apply a branch to its parent | not listed |
merge_preview | what a merge would do | yes |
rebase | take in what changed in the parent, keep the branch's own changes | on its own worlds |
check_reads | whether anything the branch read changed since it forked | yes |
undo_merge | undo a branch's latest merge | not listed |
checkpoint | name a moment of a branch | on its own worlds |
rollback | go back to a checkpoint | on its own worlds |
discard | throw a branch away | on its own worlds |
simulate | fork, run and score many worlds | yes; kept worlds are its own |
replay | re-run a simulated world | yes |
merge_policies | the rules agents' merges keep to, and who keeps to each | yes |
check_merge_policy | try rules on a branch's real changes | yes |
set_merge_policy | create or change a policy, give it to agents | drafts the SQL for a person |
drop_merge_policy | remove a policy | drafts the SQL for a person |
reviews | merges waiting for a person | yes |
review | a policy's reviewer decides a held merge: approve, reject or pass | yes |
merge_checks | queries every merge's result must find nothing in | yes |
set_merge_check | create or replace a merge check | drafts the SQL for a person |
drop_merge_check | remove a merge check | drafts the SQL for a person |
describe
Tables with row counts, their fields with types and example or allowed values, and a sample row. Call first.
| Parameter | Type | Required | Default |
|---|---|---|---|
table | string | no | all tables |
branch | string | no | main |
find
Exact filters, plus optional text search (BM25) and/or vector similarity, best first. Returns the match count (1 match, 3 matches), then up to limit rows with scores. With neither text nor vector, lists matching rows in key order. See search.
| Parameter | Type | Required | Default |
|---|---|---|---|
table | string | yes | |
where | object | no | none; {"field": value, ...}, all must match |
text | string | no | words to find in the rows' string fields |
vector | array of numbers | with vector_field | ranks by dot product |
vector_field | string | with vector | the field holding stored vectors |
limit | integer | no | 20 |
offset | integer | no | 0 |
ef | integer | no | 0: chosen automatically; the vector graph's beam |
branch | string | no | main |
vector without vector_field (or the reverse) fails: give both 'vector' and 'vector_field' ....
sql
Runs SQL on a branch, as its own session. $1, $2, ... take params. See SQL and worlds.
| Parameter | Type | Required | Default |
|---|---|---|---|
sql | string | yes | one or more statements; BEGIN ... COMMIT within the call |
params | array | no | none; JSON values for $1, $2, ... |
branch | string | no | main |
Returns each result as a table, or a statement's tag.
get
Reads one row.
| Parameter | Type | Required | Default |
|---|---|---|---|
table | string | yes | |
id | string | yes | |
branch | string | no | main |
put
Creates or replaces one row (the whole row).
| Parameter | Type | Required | Default |
|---|---|---|---|
table | string | yes | |
id | string | yes | |
record | object | yes | the row as a JSON object |
branch | string | no | main |
delete
Deletes one row.
| Parameter | Type | Required | Default |
|---|---|---|---|
table | string | yes | |
id | string | yes | |
branch | string | no | main |
fork
Creates a branch: an instant private copy. In safe mode it becomes one of the session's own worlds.
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | yes | e.g. your task id |
from | string | no | main; a branch, or name@when |
meta | object | no | none; notes, e.g. {"owner": "you", "task": "..."} |
To say what the branch is meant to change, held to at its merge, put a declared scope in meta: {"may_change": "leads.status, lists", "tenant": "org_id = 42", "max_rows": 250, "max_deletes": 0, "intent": "close stale leads", "run": "your run or subagent id"}. To say which run made its changes (shown in the audit and in reviews, and undone together with UNDO AGENT ... WHERE run_id), add run_id, prompt_version, model and tool_schema (which run made it).
Returns created branch task-1 (world id <id>) from main; pass branch="task-1" to work on it.
set_meta
Merges a JSON object into a branch's metadata; a null value removes a key. Returns <branch>: <metadata>.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
meta | object | yes |
branches
Lists branches with their parent and number of changes. No parameters.
diff
What changed on a branch since it was forked, row by row and field by field: the count first, then a page of changes.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
to | string | no | none: compare with the fork point. Else from branch to to; either can be name@when |
as_sql | boolean | no | false; true: the change as SQL statements (INSERT, UPDATE, DELETE, CREATE/ALTER TABLE) |
readers | boolean | no | false; true: instead of rows, what in the database reads each changed column, then what that couldn't see (DIFF ... READERS) |
limit | integer | no | 20 |
offset | integer | no | 0 |
Without to: 2 changes on task-1 (leads: 1 deleted, 1 updated, 0 inserted), what each table loses first, then one line per change, the deleted rows first with what they held: - DELETE table/id {row}, then + table/id {row} or ~ table/id field: old -> new. With count: true, the same per-table line alone, and what the branch's merge would send (; effects on merge: email (2)). With to, the table from DIFF ... TO. A longer diff ends with (showing changes 1-20 of 57; call diff again with offset=20 for the next ones).
history
What happened to a branch recently, newest first: forks, writes, commits, merges. Runs SHOW HISTORY.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
limit | integer | no | 30 |
restore
Puts a branch back as it was at a moment, as an ordinary write. Runs RESTORE WORLD.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
at | string | yes | a time (2026-09-20 10:00) or a time ago (-10 minutes) |
merge
Applies a branch's changes to its parent and deletes the branch. If rows both sides changed, nothing is merged and the conflicts come back (isError: true), with a hint naming resolve, by_columns, rows and merge_preview. With only_tables, only_keys or into, just those rows merge (or go to another branch) and the branch stays. See MERGE.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
into | string | no | the parent; another branch to merge into (the branch stays) |
only_tables | array of strings | no | all tables; non-empty (the branch stays with the rest) |
only_keys | array of strings | no | all rows; "table/id", non-empty (the branch stays with the rest) |
by_columns | boolean | no | false; true: only a column both sides changed conflicts |
rows | object | no | none; {"table/id": "ours" | "theirs" | "delete" | {row to keep}} |
resolve | string | no | fail; ours (this branch's values) or theirs (the parent's) |
confirm | boolean | no | false; after a crash, merge the flagged branch as it is now (check its diff first) |
version | integer | no | none; merge only if the branch is still at this version (what merge_preview or reviews showed), refused otherwise |
target | integer | no | none; with rows settled (resolve, by_columns, rows), the target merge_preview named: refused if the parent changed since |
confirm_deletes | integer | no | none; for a person approving a merge whose merge policy has confirm_deletes_over: the number of rows it deletes (merge_preview gives it). A wrong count is refused with the real one; an agent's own merge goes to review instead |
Returns merged 1 change from task-1 into main. confirm applies to a plain merge only: it's ignored when into, only_tables, only_keys, by_columns, rows or version is given.
rebase
Moves a branch's fork point up to its parent as it is now, keeping the branch's own changes on top, without changing the parent: a long-running branch picks up what changed under it and settles conflicts as they come. Refused, with nothing changed, on a conflict nothing settles, or when a row the branch read (with check_reads) changed since. Runs REBASE WORLD.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
by_columns | boolean | no | false; true: only a column both sides changed conflicts |
rows | object | no | none; {"table/id": "ours" | "theirs" | "delete" | {row to keep}} |
resolve | string | no | fail; ours or theirs |
version | integer | no | none; rebase only if the branch is still at this version |
Returns REBASE n, n the parent's rows it took in.
check_reads
Whether anything a branch read changed since it forked, without merging or changing anything: for a branch forked with (check_reads = true) (or by an agent whose merge policy has check_reads). The rows, key ranges and filtered scans it read, an empty result included, compared with its parent as it is now, and in a folder attached to Postgres with Postgres as it is now too. Meant for the start of each step of a long task: if something changed, read it again and decide again (rebase first) before writing; the branch's merge would be held for it anyway. The branch's view doesn't change. Runs CHECK READS.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes |
Returns "Nothing ... read changed since it forked" or a table: checked_against (the parent, or Postgres (...)) and changed (what, and why).
merge_preview
What merge would do with the same parameters, without doing it: per row, whether it applies or conflicts and why, with the fork point's, the branch's and the parent's versions. Runs MERGE ... DRY RUN; its columns are table, id, outcome, detail, base, ours, theirs, result, postgres. It leads with a row per table (outcome summary: 150 deleted, 1 updated, 1 inserted), then the rows it deletes (DELETE: the merge deletes this row), then the rest. In a folder attached to Postgres, a merge into main is checked against Postgres as it is now too (read only, rolled back): postgres is each row as Postgres has it, a row changed there since the copy has outcome changed in postgres, and a row with outcome postgres says when it was read. Then What reads the changed columns: and the table from DIFF ... READERS for the branch, and the branch's version: pass it as version with later pages (offset), which are refused if the branch changed since, and with merge, which then merges exactly what the pages showed.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
into, only_tables, only_keys, by_columns, resolve, rows | no | as for merge | |
limit | integer | no | 20 |
offset | integer | no | 0 |
version | integer | no | none; with a later page, the version the first page named (refused if the branch changed since) |
undo_merge
Puts back, in the parent, every row the branch's latest merge changed. Refuses if some were changed since, naming them. Runs UNDO MERGE.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | the merged branch (it's gone; its name still works) |
skip_changed | boolean | no | false; true leaves rows changed since the merge alone |
checkpoint
Names this moment of a branch. Runs CHECKPOINT WORLD.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
name | string | yes | e.g. before-migration |
rollback
Puts a branch back as it was at one of its checkpoints, as an ordinary write (its history keeps the rest).
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
checkpoint | string | yes | a name given to checkpoint |
discard
Throws a branch and its changes away.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
cascade | boolean | no | false; true also discards every branch forked from it, and from those |
simulate
Forks worlds branches from one moment of from, runs script in each in parallel ($1 is the world's index 0, 1, ..., $2 its seed), scores each with score, keeps the best keep and discards the rest. random() is seeded per world, so the same call gives the same worlds. Kept worlds are the session's own. See SIMULATE.
| Parameter | Type | Required | Default |
|---|---|---|---|
worlds | integer | yes | more than 0 |
prefix | string | yes | world i is named <prefix>_<i> |
script | string | yes | SQL run in each world |
score | string | yes | a SELECT giving one number per world |
from | string | no | main; or name@when |
keep | integer or "all" | no | all |
order | string | no | desc (highest score best); or asc |
seed | integer | no | 0 |
threads | integer | no | 0: all free cores |
Returns <n> worlds from <base> in <ms> ms, best first; <f> failed; kept: <names>, then up to 20 lines <name> (index <i>): score <s>[, kept] (or failed: <why>), then (<n> more).
replay
Makes a simulated world again from its recorded inputs and says whether it comes out identical, rows and score.
| Parameter | Type | Required | Default |
|---|---|---|---|
world | string | yes | a world a simulation kept |
as | string | no | none; keep the replay under this name |
Returns <world>: identical (0 rows differ; score <s> now, <s> recorded), with ; failed: <why> and ; kept as <name> when they apply.
merge_policies
The merge policies: the rules under which an agent's merge goes through without a person. A psql-style table (name, max_rows, max_deletes, tables, review_tables, schema, overwrite, created), then a line per agent keeping to one (agent bot keeps to small), or no agent keeps to a merge policy. No parameters. Runs SHOW MERGE POLICIES.
check_merge_policy
Tries rules on a branch's real changes before any agent keeps to them: would its merge go through on its own, or wait for a person, and why. Give a policy's name, rules, or both: the rules change the named policy's for this check only, so "what if max_rows were 1000?" is one call. Nothing is saved.
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
name | string | no | none: only rules (and the defaults for the rest) |
rules | object | no | none; as for set_merge_policy |
Returns draft's merge would go through on its own under those rules, or draft's merge would wait for a person under merge policy small: it changes 3 rows (at most 2) (each broken rule, joined with ; it ). Rows both sides changed aren't a rule: see them with merge_preview.
set_merge_policy
Creates a merge policy, or changes the rules given of an existing one (the others stay), and has agents keep to it. Runs CREATE/ALTER MERGE POLICY and ALTER AGENT ... SET (policy = ...) as one call, so an agent needs admin (it isn't listed otherwise). In safe mode nothing changes: the reply (isError: true) is the SQL for a person to run, so an agent can draft rules and a person applies them.
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | yes | letters, digits, _ and - |
rules | object | no | none (a new policy gets every default) |
agents | array of strings | no | none; agents to keep to it from now on |
| Rule | Type | A merge needs a person when | Default |
|---|---|---|---|
max_rows | integer or null | it changes more rows than this | null: no limit |
max_deletes | integer or null | it deletes more rows than this; 0: any delete (the recommended start for any agent) | null: no limit |
rows_per_hour | integer or null | with the agent's other merges into main in the last hour, it changes more rows than this | null: no limit |
deletes_per_hour | integer or null | with the agent's other merges into main in the last hour, it deletes more rows than this | null: no limit |
max_age | string, integer or null | its branch was forked longer ago than this ("1h", or ms) | null: no limit |
tables | array of strings | it changes a table not in the list | []: any table |
review_tables | array of strings | it changes any table in the list | [] |
review_columns | array of strings | it changes any column in the list ("users.email"), even in one row | [] |
schema | boolean | false and it changes a table itself, a view, function, sequence, schema or type | false |
overwrite | boolean | false and it overwrites rows its parent changed since the fork | false |
critical | boolean | false and it changes a column a reader marked critical reads (MARK READER) | false |
check_reads | boolean | true: its agents' worlds keep what they read, and a merge that read rows changed since the fork waits (stale reads) | false |
require_scope | boolean | true: its agents' forks must declare may_change or tenant in meta (declared scope) | false |
An unknown rule, or a value of the wrong type, is refused with the rules' names. Returns the policy as a table.
{"name": "small", "rules": {"max_rows": 500, "max_deletes": 0, "review_tables": ["payments"]}, "agents": ["bot"]}
drop_merge_policy
Removes a merge policy. Refused while an agent keeps to it, unless release_agents (DROP MERGE POLICY ... CASCADE: released and dropped in one step). Needs admin; in safe mode, drafts the SQL like set_merge_policy.
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | yes | |
release_agents | boolean | no | false; true: take the policy from its agents in the same step (their rights alone decide then) |
reviews
Merges a policy sent to a person, oldest first: world, owner, policy, reasons, asked, version, changed_since (the branch was written to, or partly merged, after its agent asked), scope, changes (per table, deleted, updated and inserted, deletes first), effects (what its merge would send, per channel). A person approves with merge (or MERGE WORLD), or throws it away with discard. No parameters. Runs SHOW REVIEWS.
review
For a merge policy's reviewer (the policy's reviewer names the agent): decide a held merge before a person does. approve merges it as it was held (refused if the branch changed since), reject keeps it from merging, pass leaves it to a person; the reason is kept in reviews and the audit. Runs REVIEW WORLD ... APPROVE | REJECT | PASS REASON '...' (a reviewer before a person).
| Parameter | Type | Required | Default |
|---|---|---|---|
branch | string | yes | |
verdict | approve, reject or pass | yes | |
reason | string | yes |
merge_checks
The merge checks: SQL queries every merge's result must find nothing in. A merge whose result one finds rows in is refused with the rows, or for an agent keeping to a merge policy waits in reviews. A psql-style table (name, tables, timeout_ms, query, created). No parameters. Runs SHOW MERGE CHECKS.
set_merge_check
Creates a merge check, or replaces the one of that name (CREATE OR REPLACE MERGE CHECK). Needs admin (it isn't listed otherwise); in safe mode nothing changes and the reply (isError: true) is the SQL for a person to run.
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | yes | letters, digits, _ and - |
query | string | yes | one SELECT that changes nothing; the rows it finds are what's wrong |
tables | array of strings | no | none: every merge runs it. Names are taken as written ("Lists" stays capitalized; "app.orders" is table orders in schema app) |
timeout_ms | integer | no | 1000; a merge it doesn't finish in is refused |
{"name": "lists_keep_leads", "query": "select id from lists where id not in (select list from leads)", "tables": ["leads", "lists"]}
drop_merge_check
Removes a merge check. Needs admin; in safe mode, drafts the SQL like set_merge_check.
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | yes |
Safe mode
The default, without --allow-merge or --agent. The agent reads every world and forks; a person approves.
- Not listed, and refused if called:
merge,restore,undo_merge(safe mode: agents can't merge. A person reviews your diff and runs ...). - Writes need one of its own worlds:
put,delete,set_meta,discard,checkpoint,rollback, andsqlthat writes (BEGIN/COMMIT,EXPLAIN ANALYZEof a write, and aSELECTcalling a function that writes included) needbranchset to a world forked in this session (byfork, or kept bysimulateorreplay). Nobranch:safe mode: changes need branch="<a branch you forked>". Call fork first.mainor another world:... and 'main' isn't one. discardwithcascade: refused if any world forked from it wasn't forked in this session.- World statements in
sqlare refused on any branch: forking, merging, restoring, dropping or switching worlds, and changing system settings (safe mode: agents can't fork, merge, restore, drop or switch worlds in SQL, ...). Reads such asSHOW WORLDS,DIFF,MERGE ... DRY RUNandAS OFwork. Use theforktool to fork. - Session-scoped: "its own worlds" lasts as long as the process. A new
chronos mcpcan't change worlds an earlier one forked; use--agentfor forks that stay the agent's. - Merging: a person runs
chronos <folder> merge <branch>in a terminal, orMERGE WORLDfrom psql. - Merge policies:
merge_policies,check_merge_policyandreviewswork.set_merge_policyanddrop_merge_policychange nothing: they reply with the SQL for a person to run, so the agent drafts rules (tried withcheck_merge_policyfirst) and the person applies them.merge_checksworks;set_merge_checkanddrop_merge_checkreply with the SQL the same way. - Not the same as
chronos serve --safe: that makes HTTP and Postgres clients without an agent's token act as the agentguest(see HTTP API). It doesn't changechronos mcp.
As an agent
With --agent NAME, each call is checked against that agent's rights and quotas (see concepts), and what it writes is in the audit trail. Tools it can't use aren't listed:
| Tool | Listed when the agent has |
|---|---|
merge | merge or merge_own |
restore, rollback | restore |
undo_merge | admin, or restore and merge; never with a merge policy |
set_merge_policy, drop_merge_policy, set_merge_check, drop_merge_check | admin |
- Checked per call:
putanddeleteas writes tobranch;mergeas a merge ofbranch(or intointo);restore;set_metaanddiscardas managing that world (cascade: its whole tree);sql,checkpoint,rollbackandundo_mergestatement by statement, as the Postgres port checks them;fork,simulateandreplayas they run; everything else as a read ofbranch. - Refused calls come back with
isError: trueand the reason. - An agent with a merge policy: its
mergegoes through when the merge keeps to the policy's rules. When it breaks one, it's refused (merging bot-2 needs a person's review (merge policy small: it changes 5 rows (at most 2)); it's queued (SHOW REVIEWS)) and the branch waits inreviewsfor a person.merge_previewshows ablockedrow saying so first. See the guide. A merge check the merge breaks is one more broken rule: queued with the rows it found. An agent without a policy is refused (merging bot-2 breaks merge check keep, which finds (id = 1); nothing was merged).
Example session
Safe mode, one JSON-RPC message per line (replies shortened to their text):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"fork","arguments":{"name":"task-1","meta":{"task":"fix names"}}}}
created branch task-1 (world id ...) from main; pass branch="task-1" to work on it
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"put","arguments":{"table":"users","id":"1","record":{"name":"Ada L."},"branch":"task-1"}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"diff","arguments":{"branch":"task-1"}}}
1 change on task-1
~ users/1 name: "Ada" -> "Ada L."
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"merge_preview","arguments":{"branch":"task-1"}}}
The preview shows users/1 with outcome apply. The agent reports task-1, and a person merges:
chronos /path/to/mydb merge task-1