cd /news/artificial-intelligence/constraining-the-agent-what-i-learne… · home topics artificial-intelligence article
[ARTICLE · art-74597] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Constraining the Agent: What I Learned Wiring MCP Into a Legacy System

A developer connecting an AI agent to a legacy PHP application and MySQL database with write access found the capability trivial to set up but the engineering of constraints much harder. The agent correctly identified a real undocumented bug, but the developer realized the gap between easy capability and necessary safety measures. They built a disposable dummy database and advocate for dedicated read-only accounts as the default, with write access as a deliberate escalation.

read11 min views1 publishedJul 26, 2026

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.

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.

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;

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';

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.

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

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.

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.

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:*)"
    ]
  }
}

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.

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

folder:

Here is the security reviewer, trimmed to its essentials:

---
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.

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.

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:


## 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.

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.

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.

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.

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.

Before connecting an agent to anything that matters:

SELECT

only; write access is a separate, deliberate stepCLAUDE.md

exists with the rules you'd give a contractor on day oneThe 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.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/constraining-the-age…] indexed:0 read:11min 2026-07-26 ·