Letting Your AI Edit the Config
Switch on the four
sovrium mcptools that read, replace and undo the files your config is made of — what turns them on, every reason a write is refused, and how to go back.
Set MCP_CONFIG_WRITE=1 and four more tools appear, able to change the config file itself. It is off by default, and it is an environment variable rather than a config key on purpose: a config that could authorise its own editing would be a config that authorises itself.
It is honoured only when both hold — the variable is set, and the project directory was named explicitly, by --project or an inherited SOVRIUM_PROJECT_DIR. A session that fell back to the working directory gets the reads only and says so on stderr. A write surface confines itself to one folder, so the folder has to be one somebody chose on purpose rather than one a client happened to start in.
{
"mcpServers": {
"sovrium": {
"command": "sovrium",
"args": ["mcp", "--project", "/Users/me/apps/crm"],
"env": { "MCP_CONFIG_WRITE": "1" }
}
}
}On a deployed app the variable does nothing. The write tools are stdio-only and are never registered on an HTTP endpoint. Setting it there is not an error — the instance boots and serves normally — but the boot warns that it bought you nothing and names this verb instead, because an operator who believes they enabled config writes over the network has not.
| Tool | Arguments | Returns |
|---|---|---|
_config_list_files |
none | { files: [{ path, sha256, bytes }] }, each path project-relative |
_config_read_file |
path |
{ path, content, sha256 } — the bytes, verbatim |
_config_write_file |
path, content, expectedSha, acknowledgeDataLoss optional |
{ path, sha256, snapshot, reloadHint } |
_config_undo |
none | { snapshot, files } — which snapshot, and what it changed |
The two readers carry readOnlyHint. Both writers are marked destructive, so a client that asks you before running a destructive tool will ask — overwriting a file is a destructive update of that file whatever the tool then refuses to do to a table, and undo overwrites several at once.
Whole file bytes, never a patch. There is no config writer here and no serialiser: content replaces the file, so your comments, key order, anchors and blank lines survive an edit untouched. The cost is that the caller must read before it writes, and expectedSha is what makes that cost real rather than advisory.
What a write is refused for
Every write runs the same ordered checks, and each refusal names what it refused. The cheap structural ones come first, and the ones that open a database come last.
- Outside the project. The path is resolved against the project directory and must land inside it.
- Not a config file.
.yaml,.ymland.jsononly, and never a symlink — a symlink can point anywhere, including out of the folder. - A protected location.
.envand friends,.git/,.claude/, the data directory, and the template marker. A tool that can write.envis a credential-writing tool; one that can write.git/rewrites history. - A stale
expectedSha. The digest you edited against no longer matches the file, so something else saved over it — very possibly you, in your own editor. The refusal says to re-read, because an assistant told only "no" retries the identical call forever. - It would not decode. The candidate is overlaid in memory onto the resolved
$refgraph and the whole app is decoded before anything is written. An invalid config never reaches the disk; the findings come back instead, in the vocabulary_config_validatealready speaks. This is what makes editing one split-out partial safe — it is judged as part of the app it belongs to rather than as a document that happens to parse. - A new reference out of the folder. A candidate that introduces a
$refresolving outside the project directory is refused, or the file being read could walk out of the folder the file being written may not leave. - The live database would reject it. Where a server is running, the change is put through the same migration planner
sovrium migrate --dry-runreports from, and a refusal there is the write's refusal — before the file changes rather than after the server has stopped trying to apply it. - It drops data. A candidate that introduces
allowDestructive: trueneedsacknowledgeDataLoss: truealongside it, and the tool never sets either flag for itself. Dropping a column deletes the rows in it, and that is your decision every time. - It would renumber a table. A field
idis optional, and an omitted one is the field's position. Inserting a field above one of those shifts every id after it and re-points the data behind them, so the insert is refused until the table's ids are explicit. The same insert into a table that spells every id out is accepted, because nothing moves.
Going back
An accepted write copies the pre-write state into the history described in Undo and Reset before it changes a byte, so _config_undo has somewhere to go even when no server is running and nothing has ever reached "accepted". It skips the copy when the newest entry already holds those exact bytes, which is why a watched project ends up with one entry before the edit and one after rather than three.
_config_undo restores the most recent snapshot whose files differ from what is on disk, and answers with the files it changed. When none differs there is nothing to go back to, and it refuses rather than reporting a success that changed nothing.
Undo puts the file back unconditionally. Whether a running instance follows it is a separate question: reverting a field you added is a column drop, so the watcher's own pre-flight refuses that reload without allowDestructive, keeps the configuration it is already serving, and publishes the reason. Your app stays up and your rows stay where they are.
A write does not make anything live. It puts bytes on disk; _config_status is how you find out whether an instance took them. Calls are answered one at a time, in the order you sent them, so a write followed by an undo happens in that order.
The four read tools, how a client is pointed at the server and which config it reads are in Your Config over MCP.
Behaviour
sovrium mcp Lets a Local AI Edit the Config File, Under Eight Bounds
- With
MCP_CONFIG_WRITEunset, no write tool is listed, while the four read tools in the same list prove it matchable - With the flag and an explicit project, all four write tools are listed, the two writers annotated
readOnlyHint: false - With the flag but no explicit project directory, no write tool is listed; the same run with
--projectlists all four - An HTTP instance booted with the flag says the variable is ignored, naming
MCP_CONFIG_WRITEandsovrium mcp _config_list_filesreturns the whole$refgraph, project-relative, eachsha256matching the file's real digest_config_read_filereturns the bytes and the samesha256_config_list_filespublished for that path- A write whose path escapes the project directory is refused and the file outside it is left byte-unchanged
- A write to
.env,.git/,.claude/or the data directory is refused, each naming the location it refused - A write to a non-config extension is refused, and a write through a symlink is refused
- A write carrying a stale
expectedShais refused, tells the caller to re-read, and leaves the file byte-unchanged - A candidate that fails to decode is refused with
findings, and the partial it targeted is byte-unchanged on disk - A candidate introducing a
$refthat resolves outside the project directory is refused, naming the offending reference - With a server running, a change the live database would refuse is refused before the file changes, naming the table
- A candidate introducing
allowDestructive: trueis refused withoutacknowledgeDataLoss, accepted with it, and never sets it - Inserting a field mid-array is refused citing
field-id-implicit; the same insert with explicit ids is accepted - An accepted write lands the candidate bytes, returns
sha256+reloadHint, and records the PRE-write state in the history _config_undorestores the most recent differing snapshot and names the files it changed; it refuses when there is none- The loop closes under
--watch: a valid write is served,_config_statusreports the new hash, and undo returns the old one - One stdio session: list, read, every refusal, an accepted write, and undo — then the live reload loop end to end
Last updated September 27, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.