Notes from January 2026, when giving an AI agent live database access still felt reckless. Written for engineers who are about to do this, and for the leads deciding whether they should.


The moment that made me stop

I had just finished connecting a coding agent to two things at once: the filesystem of a legacy PHP application, and the MySQL database behind it. Both with write access. It took about twenty minutes.

Then I ran a test prompt, watched the agent read a table definition, cross reference it against a model file, and correctly identify a column that existed in the database but had never been declared in the code, a real bug, in a system nobody had documented in years, and I felt genuinely excited for about ninety seconds.

Then I thought about what else it could do with those same permissions.

I had just handed a non-deterministic process the ability to modify source files and issue arbitrary SQL against a system that people depend on. The capability was trivial to set up. The judgment about whether it should have that capability was entirely mine, and nothing in those twenty minutes had asked me for it.

That gap, between how easy the capability is and how much engineering the constraint requires, is what this article is about.


Why grounding changes everything in legacy work

The bottleneck in modernizing an old system has never been typing speed. It's context. You cannot safely change what you don't understand, and understanding is expensive because the knowledge is distributed across a database schema, a codebase, and assumptions nobody wrote down.

This is exactly where AI assistants used to fall apart. Ask a model to query a table it has never seen and it produces something like this:

-- What the model guesses from a million tutorials
SELECT id, email, created_at
FROM users
WHERE is_active = 1;

Enter fullscreen mode Exit fullscreen mode

Confident, clean, and useless, because the legacy schema actually looks like this:

-- What exists once the agent can read the schema
SELECT id_usr_pk, correo_e, fec_reg
FROM tbl_usr_sys
WHERE estado_reg = 'A';

Enter fullscreen mode Exit fullscreen mode

Both queries are syntactically perfect. Only one of them runs.

MCP closes this gap. Instead of guessing the schema, the agent queries it. Instead of guessing the model class, it reads the file. Generated code stops being a plausible looking artifact and starts being grounded in what actually exists.

That's a real improvement. It's also what makes the security question urgent rather than theoretical, because grounding requires access, and access is the whole risk.


The architecture: four layers of constraint

What I ended up building was less about capability and more about blast radius.

Layer 1: A disposable database

Before anything else, I built a dummy database, same schema, synthetic data, no relationship to anything real. Every agent driven exploration, every "what happens if we add this column," ran against the copy.

This is not a sophisticated idea. It's the same instinct that stops you from testing a migration against production. But it's worth stating plainly, because the tooling makes it easy to skip: the connection string is the only difference between a safe experiment and an incident, and nothing in the setup process prompts you to notice which one you're configuring.

The disposable database is also what makes everything downstream cheaper. When the worst case is "restore a snapshot," you can afford to let the agent be wrong.

Layer 2: Least privilege on the connection itself

A database MCP server has no permissions of its own. It executes queries as whatever user you gave it. If that user can DELETE, the agent can DELETE.

The correct default is a dedicated account with SELECT only, used for every task that involves understanding the system rather than changing it, which, in modernization work, is most of them. Write access should be a deliberate escalation for a specific task, not the standing configuration.

I understood this later than I should have. The configuration I first implemented used a full privilege account, and it worked, so I didn't question it. Working is not the same as correct.

Layer 3: An unreachable undo button

Agents are good at shell commands, and shell commands are how you lose work. I added a permissions config that blocks version control operations outright. The shape looks like this:

// .claude/settings.json  illustrative
{
  "permissions": {
    "deny": [
      "Bash(git commit:*)",
      "Bash(git push:*)",
      "Bash(git rebase:*)",
      "Bash(git reset:*)",
      "Bash(rm -rf:*)"
    ]
  }
}

Enter fullscreen mode Exit fullscreen mode

The reasoning: git is the undo button. If the agent can operate the undo button, there is no undo button. Every other guardrail assumes I can revert to a known good state, so the mechanism that provides known good states has to sit outside the agent's reach.

In practice this cost nothing. I commit manually, which I was doing anyway, and it means every change passes through a moment where a human reads a diff and decides.

Layer 4: Separation of duties across agents

Rather than one agent doing everything, I defined several with narrow mandates, each a separate definition in the project's .claude/agents/ folder:

  • Implementer: writes the change. Its job is to make the thing work.
  • Security reviewer: reads the diff with an adversarial brief. Does not care whether the feature works.
  • Test author: writes tests against the stated behavior, deliberately without seeing the implementation reasoning, so it tests the requirement rather than the code.
  • QA: checks whether the change satisfies what was asked, including what was implied.
  • Me: final review, and the only one who can commit.

Here is the security reviewer, trimmed to its essentials:

# .claude/agents/security-reviewer.md — illustrative
---
name: security-reviewer
description: Reviews diffs for security issues only. Run after any implementation.
---

You receive a diff and nothing else. You do not evaluate
functionality, style, or performance.

Look specifically for:
- SQL built by string concatenation
- Authorization checks that trust client supplied identifiers
- Secrets or connection strings appearing in code
- Input that reaches file paths, shell commands, or queries unvalidated

Output: findings with file, line, severity, and the attack you would
attempt. If you find nothing, state exactly what you checked.

Enter fullscreen mode Exit fullscreen mode

The reason to split roles is not that five agents are smarter than one. It's that a single agent asked to implement and critique its own work in the same context will tend toward agreement with itself. A fresh context with an adversarial brief produces meaningfully different output than "now check your work." Separation of duties is an old control; it translates almost directly.


The constitution file

Alongside the agents, a CLAUDE.md at the project root with persistent instructions that load into every session. This is where you encode everything the model's generic priors will get wrong about your system.

A skeleton of mine, placeholders only:

# CLAUDE.md — skeleton

## Hard rules
- Only ever connect to the development database. Never any other host.
- Never run git commands. A human commits.
- Never modify /vendor or applied migrations.
- Schema changes are written as migration files, never executed directly.

## Things you would guess wrong
- Primary keys follow `id_<entity>_pk`. Timestamps are `fec_*` and are
  stored as strings, not DATETIME.
- "Deleted" rows are `estado_reg = 'B'`. Nothing is ever physically deleted.
- Routing was customized in <file>; the framework docs do not apply there.

## Domain vocabulary
- <One line for each of the five nouns that actually matter in this business.>

## Definition of done
- A failing test existed before the fix and passes after it.
- The security reviewer ran on the final diff.
- Output states: what changed, why, and what you did NOT change.

Enter fullscreen mode Exit fullscreen mode

The failure mode this prevents is subtle. Without it, every session starts from generic assumptions about how a PHP application should be structured, and the agent will helpfully suggest changes that would be correct in a modern codebase and catastrophic in this one.

Treat it as a living document: every time the agent makes the same wrong assumption twice, that's a missing line.


What actually improved, honestly

I want to avoid the version of this article that ends in a productivity multiplier.

What genuinely got better: schema aware code generation stopped being wrong by default. Cross-referencing the database against the codebase, finding drift, undeclared columns, dead tables went from a manual audit to something I could ask for. My own understanding of unfamiliar corners of the system got faster to build.

What did not get better: anything requiring judgment about whether a change should be made. The agent will implement a bad idea with excellent syntax. It has no opinion about whether the requirement makes sense, and in legacy work, interrogating the requirement is frequently the actual job.

And one thing got worse before it got better: review load. Generated code arrives faster than you can meaningfully evaluate it, and the temptation to approve a diff that merely looks reasonable is real. The guardrails above exist substantially to protect against my own review fatigue.

I'd love to close this section with a dramatic story of the agent attempting something destructive and a guardrail catching it mid flight. I don't have one, and I've come to think that's the point. The incident that doesn't happen is invisible. You build the envelope precisely so that you never find out what the unconstrained version would have done.


The part organizations should think harder about

Here is what an MCP server actually is, described plainly: a long running local process, holding credentials to your data, executing instructions that originate from a language model, installed from a public package registry, usually by a developer acting alone.

Every clause in that sentence is a security consideration.

The credentials sit in plaintext. In my setup they lived in a wrapper script on disk. Better than pasting them into a shared config; still just a file with a password in it. I learned how fragile this is the mundane way: my first wrapper silently corrupted the password because the shell interpreted its special characters. Nothing was breached, the connection simply failed, but a secret that can be mangled by string interpolation is a secret being handled at the wrong layer.

The supply chain is wide open. The standard invocation fetches and executes a package from a public registry at launch, resolving to the latest version unless you pin it. That package receives your database credentials in its environment. A compromised maintainer account or a typosquatted name is all it takes. Pin every version; for anything touching real data, mirror through a private registry.

There's no audit trail by default. When something goes wrong, "which tool call did that" is a question you want to be able to answer. Out of the box, you usually can't.

Instructions can arrive from untrusted places. If an agent reads a file, a database record, or a ticket containing text crafted to look like instructions, that text is now in the context of a process holding your credentials. This is a working argument for read only defaults independent of everything else.

None of this means don't use MCP. It means the developer installs community package pattern is a prototyping pattern, and teams are quietly running it in production because it works and nobody stopped to reclassify it.


If you lead a team

The individual workflow above is a pilot. Turning it into a process is a different exercise, and it's mostly about deciding things once instead of per developer.

Start with comprehension, not generation. A read only MCP connection against a replica turns "understand this legacy module" from weeks into days. It's the lowest risk, highest value entry point, and it builds the team's judgment about the tool before anyone asks it to write anything.

Make the envelope a team artifact. CLAUDE.md, the agent definitions, and the deny list belong in the repository, reviewed like code. One developer's careful setup doesn't scale; checked in constraints do. When the agent makes a wrong assumption, fixing the constitution file is a commit anyone can make, the team's accumulated judgment becomes infrastructure.

Define the escalation path. Who may enable write access, for which task, recorded where. If the answer is "whoever needs it, informally," you don't have a policy, you have a default you haven't noticed.

Watch review load as your leading indicator. If merged diffs per day double and review time doesn't, quality is degrading silently. Cap change size per task at what a human can genuinely evaluate. The cheapest control in this entire article is a diff budget.

Build your own MCP servers for anything real. An internal server can be scoped to exactly the queries the work requires, read-only unless explicitly not, authenticated against your existing identity infrastructure, and audited by default. It's a small service, not a research project, and it's the difference between a capability you can defend in a review and one you're hoping nobody asks about.

Hire and train for the envelope. The scarce skill is no longer producing code. It's designing the constraints code gets produced inside, and being able to tell whether what came out is correct. That has interview implications and mentoring implications, and the teams that internalize it first will quietly outbuild the ones optimizing for generation speed.


The checklist

Before connecting an agent to anything that matters:

  • [ ] A disposable copy of the data exists and is the default target
  • [ ] The database user is SELECT only; write access is a separate, deliberate step
  • [ ] Every MCP package version is pinned
  • [ ] Secrets are not sitting in plaintext inside a repo path
  • [ ] Git is your undo mechanism and is outside the agent's reach
  • [ ] A CLAUDE.md exists with the rules you'd give a contractor on day one
  • [ ] Review roles are separated; the critic never sees the implementer's reasoning
  • [ ] "Done" requires a test that failed before the change existed
  • [ ] Diff size is capped at what you can genuinely review
  • [ ] You can answer "which tool call did that" after the fact

The setup is twenty minutes. The engineering is everything on this list, and the list is just the same controls you'd apply to a capable contractor with commit access and no context, which is a reasonable mental model for what you've actually hired.


Written from experience modernizing an internal system with an agentic workflow. All identifiers, schemas, and configuration values shown are placeholders.