The biggest problem with giving an agent direct access to your database is that sometimes it will delete all of your data. Unfortunately, I have experienced this myself.

Databases can be somewhat difficult to query. If you have spent many years with SQL, you probably know most of the clauses off the top of your head, but more complex queries still take some time and attention to come up with. Having a machine that can take a freeform english question and produce a series of queries that can give you an answer (maybe even a correct one, assuming you've asked the right question, and that the question can be answered by the database) is pretty cool.

It's even cooler when your agent can build the database itself for you. You describe, with words, what information you want to track, and a few minutes later you have a database for tracking it. Easy!

If you've worked with databases for long enough, you are probably also familiar with the feeling of what happens when you type a query wrong. One time, I was updating the password in a SQL database containing user credentials by issuing a SQL UPDATE statement, and I forgot a WHERE clause, and accidentally set the password for all users in the database. That was a bad day. Silly fallible human brain.

After that incident I learned some techniques for avoiding the same mistake in the future. Most databases that I care about offer a feature called transactions. Instead of just writing a statement to change the data, I would write a series of statements:

  1. 1.

    BEGIN TRANSACTION

  2. 2.

    UPDATE ... WHERE ...

  3. 3.

    Run a bunch of other queries to check my work

  4. 4.

    COMMIT if everything looks good, or, if I messed up, ROLLBACK to instantly revert the database to the way it looked at step 1.

Databases are cool.

My agent deleted my database

Some months ago I was prototyping a project, and in the process I had an agent connecting directly to the database to run queries, add tables, change data, etc. The data didn't really matter at that point, but at one point the agent made the exact same mistake I had made and paved over all of it.

The agent knew immediately what it had done. It had intended to update a single-digit number of rows, but was informed by the database that it had updated a triple-digit number of rows. The agent said "oh no, that doesn't sound right", and checked a couple things, and then said "I have made a huge mistake, I am so sorry, there is nothing I can do."

Why did the agent not do the BEGIN TRANSACTION / COMMIT procedure I described in the last section? Well, because I didn't tell it to. I just didn't think of it, it was a small prototype project and it didn't matter. That was my mistake.

Fixing my mistake for good

I did not want to just add an instruction in this project telling the agent to follow the procedure and to make no mistakes. For one, I don't think that would be a very reliable solution, but even if it is, it only helps with this project. I have many, many projects that involve databases, and each time I have to remember to do this is another opportunity for me to forget. I do not trust the agent to never make mistakes and I trust myself even less. So I set out to rethink how agents use databases in the first place so that neither of us would have to remember.

If you read my last piece on adversarial review, you should have a baseline understanding of how tools work, so I won't review that. Let's look at a naive set of tools for database access:

  1. 1.

    query - runs a query

Pretty simple. There's not much to say about this. The agent runs a query and you hope it doesn't make a mistake. I would prefer to use a method that does not rely on hope.

To wit:

GitHub - tilmon-engineering/sqlite-mcp: A stdio MCP server for editing local sqlite files in a way that forces the agent to check its work.
A stdio MCP server for editing local sqlite files in a way that forces the agent to check its work. - tilmon-engineering/sqlite-mcp
https://github.com/tilmon-engineering/sqlite-mcp
GitHub - tilmon-engineering/typedb-mcp: An MCP server for TypeDB that prioritizes agent affordances and safety.
An MCP server for TypeDB that prioritizes agent affordances and safety. - tilmon-engineering/typedb-mcp
https://github.com/tilmon-engineering/typedb-mcp

sqlite-mcp and typedb-mcp are MCP servers (tool providers) that provide agents with access to SQLite and TypeDB respectively. They offer, roughly, the following set of tools:

  1. 1.

    show_schema - shows the entire database schema

  2. 2.

    begin_transaction - starts a database transaction

  3. 3.

    query - runs a query

  4. 4.

    commit - commits a transaction to the database

  5. 5.

    rollback - reverts a transaction, restoring the database to the original state

Crucially, the MCP server also embeds a state machine that forces the agent through a certain path.

  1. 1.

    The agent MUST start by reading the schema. It cannot do anything until it reads the database schema.

  2. 2.

    The agent MUST open a transaction before it can execute a query.

  3. 3.

    Opening a transaction or running a query reminds the agent that the transaction is open and must be rolled back or committed. (The transaction will also time out eventually.)

  4. 4.

    The agent cannot have more than one transaction open at a time.

A flow chart, representing the valid "next moves" that the agent can take, with the following sequence 1: show_schema 2: begin_transaction 3: query (1 or more times) 4: either commit or rollback 5: return to begin_transaction

This sequence of rules is a concept that the design documents refer to as next moves. Each tool call includes a message back to the agent instructing it precisely which actions it may take next, and any other action yields an invalid state error (with another reminder of what the valid next moves are).

These rules force the agent to observe the state of the database before committing to any changes. An agent may not even begin a transaction until it observes what tables exist. It is not possible for the agent to permanently change the data without first seeing what the results of that change would be.

Deterministic guardrails for nondeterministic agents

One thing that makes it challenging to use agents for operational tasks is that sometimes there's just no going back. When you pull a lever that changes the state of the world, then the state of the world is changed forever. There is only forward.

But sometimes you are fortunate enough to be working with a tool like database transactions that have a cheap and well-defined rollback process. In these cases, I have had a great deal of success using state machines to control the agent's allowable path through its tool calls, forcing the agent to encounter its own mistake before the mistake is made permanent.

For other operational tasks, I'm still thinking about it. If this gives you a good idea, please share it with me.