# Let an AI agent query your prodution database safely with read-only SQL over MCP

> Source: <https://railyard.run/blog/ai-agent-read-only-sql/>
> Published: 2026-11-13 14:36:34+00:00

[← Blog](https://railyard.run/blog/)Guides & tutorials • 4 min read

# Let an AI agent query your production database safely with read-only SQL over MCP

 Follow this on your own server. [Start free, no card needed](https://app.railyard.run/signup) 

“How many teams signed up this week?” “Which customers have more than three failed payments?” “Is the orders table actually using that index?” These questions come up every day, and every answer means opening a database console with production credentials. An agent can answer them in plain language, as long as it can’t change anything while it looks.

This tutorial sets that up on Railyard with the `list_tables` and `query_database` MCP tools, and explains each guard rail so you know exactly what an agent can and can’t do.

## The guard rails, in order

Before the how-to, here’s why it’s safe. Every query passes through five layers.

1. **Scope.**`query_database` and`list_tables` need a** write** -scoped connection, even though they only read. Your data is the most sensitive thing in the account, so a read-only dashboard connection doesn’t get it. Agents that only check deploys never see rows.
2. **Role cap.** Only admins and owners can grant`write` . A viewer’s or deployer’s agent can’t query data, whatever scope it asked for. Members limited to some apps can’t reach other apps’ databases.
3. **Read-only transaction.** On Postgres the query runs inside a`READ ONLY` transaction that is always rolled back. On MySQL it runs in a read-only transaction, and ClickHouse runs it in read-only mode. An`INSERT` ,`UPDATE` ,`DELETE` or DDL statement fails at the database, not just in a filter that could be bypassed.
4. **Time limit.** A statement timeout stops runaway queries, so an accidental cross join can’t pin your primary.
5. **Row cap.** At most 200 rows come back, flagged as`truncated` when there were more. Aggregate in SQL and the agent gets a precise answer instead of a sample.

And one rule outside the database: the SQL text is never written into the MCP audit entry, so a query that happens to contain an email address doesn’t get copied into your activity log.

## Step 1: Connect with write access

Write access is for admins and owners. In Claude, go to *Customize → Connectors → Railyard → Connect* and choose **Full (write)** on the consent page. In Claude Code:

```
claude mcp add --transport http railyard https://app.railyard.run/api/mcp
```

Then run `/mcp` → **railyard** → **Authenticate**, and pick write.

If you’d rather give a specific agent data access without dashboard sign-in, create a `write` API token under *Settings → Access & tokens*, set an expiry, and pass it as a Bearer header. The same rules apply.

## Step 2: Let it learn the schema

```
> What tables does the shop app's database have?
```

`list_tables` returns each table with approximate row counts and sizes, largest first. That’s usually enough for the agent to orient itself:

```
orders        1.2M rows   840 MB
order_items   3.9M rows   610 MB
customers     210K rows    95 MB
payments      1.3M rows   120 MB
```

When it needs columns, it asks the database the standard way:

```
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'orders'
ORDER BY ordinal_position;
```

If an app has more than one database (say Postgres and ClickHouse), name it with `engine: "clickhouse"` or just say “in ClickHouse”.

## Step 3: Ask real questions

**Business questions**

```
> How many new customers signed up each day this week?
SELECT date_trunc('day', created_at) AS day, count(*)
FROM customers
WHERE created_at >= now() - interval '7 days'
GROUP BY 1 ORDER BY 1;
```

**Data-quality questions**, often while [debugging a failed deploy](https://railyard.run/blog/debug-failed-deploy-with-claude/):

```
> Are there duplicate emails in orders? A unique index migration just failed.
SELECT email, count(*) FROM orders GROUP BY email HAVING count(*) > 1 ORDER BY 2 DESC LIMIT 50;
```

**Performance questions**

```
> Is the orders lookup by customer_id using an index?
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
```

Plain `EXPLAIN` is fine. `EXPLAIN ANALYZE` actually executes the query, so on a big table the time limit may stop it, which is the point.

## Step 4: Write queries that fit the limits

The 200-row cap shapes good habits, and Claude follows them quickly when you say so once:

- **Aggregate in SQL.**`count` ,`sum` ,`avg` ,`percentile_cont` and`GROUP BY` return a handful of rows that answer the question exactly.
- **Filter early.** Put a date range on large tables. It’s faster and stays under the time limit.
- **Order and limit on purpose.** “Top 20 by revenue” beats “all customers”.
- **Look at the `truncated` flag.** When it’s`true` , the answer is a sample. Ask for an aggregate instead.

## What it can’t do, and what to do instead

- **Change data.** No writes, ever, through this tool. To fix bad data, have Claude draft the`UPDATE` and run it yourself in the dashboard’s database console, or ship it as a migration so it’s reviewed and repeatable.
- **Restore a backup.**`restore_backup` exists, but it always waits for an admin’s approval in Railyard, because it replaces today’s data.
- **Export everything.** Bounded results are deliberate. For a full export, download a backup from the dashboard.

## Backups, from the same conversation

The agent can also see and start backups:

```
> When was the shop database last backed up? Take one now before I run the migration.
```

`list_backups` shows recent backups with status and size. `create_backup` starts one immediately (it needs `write`, and it’s additive, so no approval). It’s a good habit before any risky migration, and now it’s one sentence.

## Questions people ask

**Does the agent see my database password?** Not through queries; they run through Railyard, and no connection string is needed or returned. But a write connection can read variable values with `get_env`, and that can include a `DATABASE_URL`. Seal sensitive variables and they stay masked.

**Can a prompt injection make it write?** No. The read-only transaction is enforced by the database itself, not by checking the SQL text. A malicious instruction hidden in a row the agent reads can’t turn a read into a write.

**Which databases are supported?** Postgres, MySQL and ClickHouse that Railyard manages for your app.

The full tool reference is in the [MCP docs](https://railyard.run/docs/mcp/).

## Why people pick Railyard for this

-  **Your own cloud account.** Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
-  **One flat price per team.** Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $99 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
-  **One-click templates.** Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
-  **Managed databases.** Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
-  **Push-to-deploy.** Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.
