# Giving AI tools real power, safely

By [Anubhav Choubey](https://anubhavchoubey.com/) · AI & Agents · 2026-11-04 · 4 min read

> A dashboard API exposed as MCP tools, a SQL guard for AI database access, audited writes, and CSV export where the rows never pass through the model.

Part 4 of the MCP series. Start at [part 1](/blog/mcp-auth-for-people-and-agents).

```stats
authorization inside every tool · SQL re-checked after rewriting · writes audited before they run · exports that skip the model
```

## The problem

Read-only tools are easy to trust. The value starts when an AI can deploy, change settings, build tables and pull data out. A model calls tools eagerly, guesses arguments and retries without asking. "It can only do what the user could do in the UI" is not enough, because the UI has a human looking at every click.

So I set myself a stricter goal: every tool does its own authorization, every write leaves a record before it happens, and bulk data never has to flow through the model at all.

## A routed tool fabric

The first piece exposed a dashboard's internal API as MCP tools. The request path looks like this:

```text
MCP client ── token ──▶ edge (verifies token, stamps identity)
                           ▼
                        gateway (routes to the right server)
                           ▼
                        dashboard tools ──▶ dashboard internal API
```

The first version had no tools at all, just to prove the pipe end to end.

Identity is set once, at the edge, from verified token claims. Downstream servers trust that and never re-read the token. One rule mattered more than any other: a verified identity always beats anything the model passes in. A tool argument called `slug` that means "branch name" must never be read as "which store". Model input is data, never an authorization input.

Inside each tool, the rules are stricter than the UI in the right places:

- Environment variable values are always redacted over MCP, whatever the caller's role. Seeing a value means going through the UI and signing in again.
- Deploying to staging needs an editor. Deploying to production needs an admin.
- Deleting a variable removes it from every environment, because the API has no per-environment delete, and the tool description says so plainly.
- A grant that covers a store is not a permission on an action. Writes check the live "manage" permission, reads the live "view" permission.
- Responses are reduced to fixed values, so internal error text never reaches the model.

## A SQL guard for AI database access

For analytics, the AI writes SQL. Every pooled connection is read-only at the session level, it uses a SELECT-only database user, and it can only see allowlisted databases.

On top of that sits a parser-based guard. The subtle part is that the guard parses the query, checks it, and then serializes it again, and a round trip through a serializer is not guaranteed to produce the query you checked. So the guard follows one rule:

```callout tip
Validate the SQL that will actually execute, not the SQL you were given. Re-parse and re-check after every rewrite.
```

I also pinned the parser version, with tripwire tests that fail on a new version until someone re-verifies the guard against it.

## Writes that are audited before they run

The AI can build its own reporting tables and schedule refreshes, but only through named tools. There is no raw write SQL, and the store's own database is never writable. Write tools need their own permission and database user, only touch writable databases and prefixed table names, and have size and count caps.

The write audit fails closed. A pending audit row is written first, and if it can't be saved, the database is never touched. Read queries get an audit too, but that one fails open: losing an audit row is better than failing a customer's read.

The same work fixed a quiet identity bug class. Identity is now read from the HTTP request behind each MCP message, not from whoever opened the session.

## Exports where the rows never touch the model

People wanted bulk data out of the chat, for example a customer list for an ad audience. Pushing thousands of rows through a model is slow and unaudited.

So the export tool runs the guarded query on the server and streams the result into an encrypted, gzipped CSV. The model gets back only metadata and a short-lived link. The key comes from a token in the link's fragment, and the server stores only its hash, so when the link expires the data is really gone. Downloads are creator-only, behind a consent page, with an audit row for every outcome. Large exports are split by id range into a multi-file ZIP.

## What I'd do differently

- **One SQL guard, not copies.** Several servers carried their own copy of the guard, and every fix had to land in all of them identically. A shared package should have come first.
- **Audit reads from day one.** Writes were audited before reads. The read trail turned out to be just as useful for debugging bad answers.

## Takeaway

Give AI tools real power, but put the checks inside the tools, not in the prompt. Trust verified identity over model input, validate what actually runs, write the audit before the write, and keep bulk data out of the model's context entirely.

**Next:** [From working to usable to central](/blog/mcp-server-working-usable-central)
