Skip to main content
Glama
Hamzah-Muhammad

SafeDataBaseMCP

SafeDataBaseMCP

CI Backends: SQLite, Postgres, AWS RDS MCP License: MIT Python 3.11+ No LLM

An MCP (Model Context Protocol) server: guardrails on real database infrastructure. Give an agent read/write access to Postgres on AWS RDS, and take away its ability to wreck the data.

Reads answer immediately. Writes cannot happen in one step: the agent sends an INSERT/UPDATE/DELETE to propose_change, the server runs it inside a transaction to compute a real preview, rolls it back, and hands back a single-use change_id. Nothing reaches disk until confirm_change is called with that id. There is no tool that writes without one. Against RDS, the connection itself is guarded the same way: no stored password, just a 15-minute IAM token minted per session - see Running it against Postgres and AWS.

list_tables      ---> answers immediately (read-only connection)
describe_table   ---> answers immediately
run_query        ---> answers immediately, single SELECT only

propose_change   ---> BEGIN -> execute -> snapshot diff -> ROLLBACK
                      returns: preview + single-use change_id, expires in 300s
                          |
                   a human reads the diff
                          |
confirm_change   ---> re-validate -> execute -> COMMIT    (once, then the id is dead)

Everything outside that grammar is refused before it reaches SQLite: DROP, ALTER, CREATE, PRAGMA, ATTACH, VACUUM, transaction control, stacked statements, SQL comments, load_extension, SQLite's internal tables, and UPDATE/DELETE with no WHERE clause.

An agent tries DROP TABLE and an unqualified DELETE, both refused, then does the propose/confirm write flow correctly

A terminal recreation of a real, unedited run - same tool calls, same responses, same change_id - captured in docs/verified-runs.md sections 1c and 1b.


Why an MCP server at all, when the agent could just run SQL?

This is the actual point of the project, so it goes first.

A capable coding agent - Claude Code, for instance - already has a Bash tool. It can reach a SQLite file with three lines of Python and do anything it likes to it. Connecting it to this MCP server gives it no new capability whatsoever. A tool that adds nothing an agent could not already do would normally be pointless.

What it adds is a constraint, and that is the whole design:

Without the server

With the server

The interface is "run arbitrary Python or shell against the database file".

The interface is six named tools: five operations, plus a read-only view of what is pending.

Every statement the agent can compose is reachable, including DROP TABLE.

The grammar is checked before execution; anything outside it is refused with a reason.

A destructive write is one tool call away, and looks like any other tool call.

A destructive write is impossible in one call. The tool that writes accepts only a change_id, and only propose_change can mint one.

"Show me a preview before you change anything" is an instruction in a prompt.

The preview is the only way to obtain the token that the write tool requires.

Reads and writes go through the same all-powerful channel.

Reads run on a connection opened mode=ro. SQLite itself refuses a write on that handle.

The last two rows are the ones that matter.

The gate is structural, not advisory. Plenty of agent setups get this behaviour by asking for it: a system prompt that says "always show the user a diff before writing." That works until the model is having a bad day, until the context is long, until the instruction is three thousand tokens up the conversation. It is a convention, and conventions are followed statistically.

Here, confirm_change takes exactly one parameter and it is a change_id. That id is a random token minted by propose_change and held in a store that enforces single use and expiry. The model cannot invent one, cannot reuse a spent one, and cannot skip the preview step, because there is no argument it could pass that would let it. The preview is not a courtesy the agent extends to the user; it is a value the agent physically needs in order to proceed. Removing the preview step from the flow would require editing this repository, not writing a more persuasive prompt.

Defence in depth, not one clever check. Validation is a parser-based allowlist, not a regex denylist, and it is not the only thing standing between a bad statement and the data:

  1. sqlparse classifies the statement; anything that is not a single SELECT (read path) or a single INSERT/UPDATE/DELETE against a known table (write path) is refused.

  2. The flattened token stream is scanned for forbidden keywords, forbidden functions and SQLite's internal tables, so DML hidden in a CTE or a UNION is caught too.

  3. Reads execute on a connection opened mode=ro, so even a validation bug could not produce a write on the read path.

  4. propose_change computes its preview inside a transaction that is rolled back in a finally block, so a preview cannot persist even if the statement raises.

  5. confirm_change re-validates the SQL before re-executing it, so a proposal cannot outlive the rules that admitted it.

Each of those is boring on its own. The point is that no single one of them is load bearing.

What this is not. This is not a claim that a sandboxed tool surface makes an agent safe in general. The agent in the room still has Bash. What it demonstrates is the design move: when you want a guarantee about how an agent touches something that matters, you get it by shrinking and shaping the interface, not by adding more instructions to a prompt. That move is the reason MCP is interesting, and it is what this repository is built to show.


Related MCP server: db-mcp

Two ways to connect

The server is a plain MCP server over stdio. It is not built around any one client, and this repository proves that by shipping two, both of which were run end to end before this README was written.

1. Claude Code (the primary demo)

A project-scoped .mcp.json sits in the repository root, so cloning the repo and opening it in Claude Code is the entire setup:

git clone https://github.com/Hamzah-Muhammad/SafeDataBaseMCP.git
cd SafeDataBaseMCP
pip install -r requirements.txt
claude

Claude Code discovers .mcp.json, asks once whether to trust the project's MCP servers, and the tools appear as mcp__safe-database__*. The demo database seeds itself on first connect. Check it is connected with /mcp, or:

claude mcp get safe-database

If you would rather register it yourself instead of using the shipped file:

claude mcp add safe-database -- python -m safe_db_mcp

Then ask it things. A read:

Which library members currently have a book on loan that has not been returned?

Seven loans are still outstanding, held by 5 members:

| Member           | Book                      | Due        |
|------------------|---------------------------|------------|
| Amara Osei       | The Left Hand of Darkness | 2025-12-11 |
| Daniel Whitfield | Never Let Me Go           | 2025-12-30 |
| Priya Raman      | Kindred                   | 2025-12-03 |
...

A write, which takes two steps whether or not the model feels like taking two steps:

Member id 4, Lukas Vogel, has settled their account. Set their status to active.

propose_change  {"sql": "UPDATE members SET status = 'active' WHERE id = 4"}
  -> rows_affected: 1
     diff: members.id=4 status: "suspended" -> "active"
     change_id: f0b91c1088f31f84   status: pending   expires_in_seconds: 300
     (nothing written yet)

confirm_change  {"change_id": "f0b91c1088f31f84"}
  -> status: committed, rows_affected: 1

And a request the tool layer simply will not carry out:

The loans table is cluttered. Drop it, and also try clearing it with a DELETE with no WHERE.

propose_change  {"sql": "DROP TABLE loans"}
  -> is_error: true
     Rejected: 'DROP' is a DDL statement. This server refuses schema changes.

propose_change  {"sql": "DELETE FROM loans"}
  -> is_error: true
     Rejected: A DELETE without a WHERE clause would touch every row and is
     refused. Add a WHERE clause.

run_query       {"sql": "DROP TABLE loans"}
  -> is_error: true
     Rejected: 'DROP' is a DDL statement. This server refuses schema changes.

The table is still there with all 12 rows, and list_pending_changes returns zero: the refusals happened in the validator, so no transaction was ever opened and no change_id was ever minted. The full trace of that session, taken from the protocol stream rather than retyped, is in docs/verified-runs.md.

2. A framework-free reference client

examples/reference_client.py is a standalone script that uses the official mcp Python SDK client and the standard library. No agent framework, no LangChain, no LangGraph, no LLM, no API key, no network call. It spawns the server as a subprocess over stdio and drives it directly:

python examples/reference_client.py

It exists to prove the server is protocol-level rather than coupled to Claude, or to any model at all. It walks the same four behaviours: a read, a write previewed but not committed, that write committed by id, and a set of refusals including a replayed change_id.

========================================================================
2. Write, step one: propose (previewed, nothing committed)
========================================================================
  sql            UPDATE members SET status = 'active' WHERE id = 4
  rows affected  1
  status         pending
  change_id      e226b618b65d5a36 (expires in 300.0s)

--- preview diff ---
{
  "added": [],
  "removed": [],
  "updated": [
    {
      "row": { "id": 4, "full_name": "Lukas Vogel", ... "status": "active" },
      "changed": { "status": { "before": "suspended", "after": "active" } }
    }
  ]
}

  pending changes awaiting confirmation: 1
  row on disk right now: {'id': 4, 'full_name': 'Lukas Vogel', 'status': 'suspended'}

That last line is the interesting one: the preview says active, the file on disk still says suspended. The full run is in docs/verified-runs.md, and tests/test_reference_client.py runs this script as a subprocess in CI, so the second path is covered by the test suite rather than by a screenshot.


The tools

Tool

Kind

What it does

list_tables()

read

Every table with its row count and column names.

describe_table(table)

read

Columns, types, nullability, defaults, primary keys, foreign keys.

run_query(sql)

read

One SELECT, executed on a read-only connection. Results capped at 200 rows.

propose_change(sql)

write, step 1

Validates, runs in a transaction, snapshots a row-level diff, rolls back, returns a single-use change_id.

confirm_change(change_id)

write, step 2

Re-validates, recomputes the preview inside the committing transaction, and commits only if it still matches. Once.

list_pending_changes()

read

Proposals still awaiting confirmation, with time remaining.

The preview diff is computed by the database, not estimated. propose_change snapshots the target table before and after the uncommitted statement - by rowid on SQLite, by primary key on Postgres - so an UPDATE shows as a changed row with before and after values per column rather than as a delete plus an insert. Tables above 5,000 rows, or Postgres tables with no primary key, fall back to a rows-affected count, and the response says so via diff_available.

The demo database

A small public library: authors, books, members, loans, seeded from safe_db_mcp/schema.sql with 6 authors, 13 books, 8 members and 12 loans, some returned and some outstanding. Real foreign keys, real constraints, real dates - enough that a query has to mean something. It is created on first connect at data/library.db (gitignored), so a clone always starts from the same state and deleting the file resets it.

SAFEDB_DATABASE_PATH=/path/to/your.db python -m safe_db_mcp

Point it at any SQLite database you like. The grammar is not specific to the demo schema: the table allowlist is read from the database at validation time.

Environment variable

Default

Meaning

SAFEDB_DATABASE_PATH

data/library.db

Which SQLite file to serve.

SAFEDB_PROPOSAL_TTL_SECONDS

300

How long a change_id stays confirmable.


How it is put together

safe_db_mcp/
  validation.py       the allowed grammar and every refusal. Pure, no database handle.
  proposals.py        the single-use, expiring pending-change store.
  engine.py           the write gate. Validates, mints proposals, never commits
                      anything unpreviewed. Knows no SQL dialect.
  backends/
    base.py           the four questions a backend has to answer.
    diffing.py        row-level diffs, and whether an approved preview still holds.
    sqlite_backend.py the zero-setup default. No server, no credentials.
    postgres_backend.py  reader role, SERIALIZABLE, primary-key diffs.
  aws/credentials.py  env, Secrets Manager, or an RDS IAM token.
  server.py           the MCP adapter. Thin on purpose.
  schema.sql          the seeded library demo, SQLite and Postgres dialects.
examples/
  reference_client.py the framework-free client.
tests/                178 tests, all deterministic.

The layering is the same argument the README opens with, applied twice. server.py contains no policy: it translates a tool call into a method call and an exception into a message, so swapping stdio for another transport would not touch a rule. engine.py contains no SQL dialect: it validates, mints proposals and refuses to commit anything unpreviewed, so SQLite and Postgres share one gate rather than each re-implementing it. A new backend answers four questions and cannot weaken the guarantee, because a backend is never asked to commit something the engine did not first preview.

No API key anywhere

There is no LLM in this project. No Anthropic call, no OpenAI call, no NVIDIA call, no orchestration framework. That is not an omission, it is the scope: the interesting claim here is about the shape of a tool surface, and a model in the middle would only make it harder to test. The consequence is that all 178 tests are deterministic, CI needs no secrets, and there is nothing in this repository that could leak a credential. The AWS integration is real code, but its tests stub botocore, so they need no account and no network either.


Running it against Postgres and AWS

SQLite is the default so a clone runs with no server, no credentials and nothing installed. The same server also speaks to Postgres, which is what makes it deployable: Postgres is protocol-identical whether it runs in a container on your laptop or as AWS RDS, so the code is unchanged between the two and only the connection string and the credential source differ.

Switching backend is one variable:

pip install 'safe-db-mcp[postgres]'
export SAFEDB_BACKEND=postgres
python -m safe_db_mcp.seed_postgres       # explicit, one time. The server never runs DDL.
python -m safe_db_mcp

Seeding is a separate command on purpose. The entire argument of this project is that a server should not be able to run DDL, so the server does not run DDL, not even to help you set it up.

What Postgres does that a file cannot

Guarantee

SQLite

Postgres

Reads cannot write

mode=ro in the connection URI, enforced by SQLite

A separate safedb_reader role granted SELECT and nothing else, plus a READ ONLY transaction. Two independent defences, and the first is enforced by the server against a login that holds no other privilege, rather than by a flag this process sets on itself.

Rows matched for the diff

rowid

The declared primary key. ctid moves when a row is updated, so it cannot match a row across a write; a table with no primary key reports diff_available: false rather than guessing.

Concurrent writers

BEGIN IMMEDIATE, plus the preview recheck below

SERIALIZABLE isolation, plus the preview recheck below

The preview cannot go stale

Both backends now close what used to be a known limitation. confirm_change recomputes the preview inside the committing transaction and refuses if it no longer matches the one that was approved:

propose_change   -> preview: members.id=4  status "suspended" -> "active"
                    change_id: 46a0466...

   [somewhere else, someone runs: UPDATE members SET status='lapsed' WHERE id=4]

confirm_change   -> Refused: The data changed since this change was proposed, so the
                    preview you approved is no longer what would happen. Nothing was
                    written. Propose the change again to see a current preview.

The check is deliberately targeted rather than paranoid, and both halves of that are tested:

  • it ignores identity columns on inserted rows, because a rolled-back Postgres preview still burns a sequence value, so the committed row legitimately gets a different id than the preview showed. Treating that as a conflict would make every insert fail;

  • it ignores unrelated rows. Someone deleting loan 11 does not block your approved deletion of loan 12. A gate that fires on any concurrent activity anywhere in the table is a gate nobody can use.

A refused confirm also does not burn the proposal. Nothing was committed, so the change_id goes back to pending and the caller can look at the current state and propose again.

Credentials, and why the good option has none

SAFEDB_CREDENTIALS picks where the database password comes from. All three go through one call, so nothing downstream knows which was used.

Value

Where the password comes from

Use it for

env (default)

SAFEDB_PG_READER_PASSWORD / SAFEDB_PG_WRITER_PASSWORD

Local development

secretsmanager

An AWS Secrets Manager secret, read in the {"username": ..., "password": ...} shape RDS creates for managed master credentials

RDS with a rotated secret

rds-iam

Nowhere. There is no password. boto3 signs a short-lived RDS IAM authentication token from the caller's IAM identity, scoped to one database user on one host, valid 15 minutes

RDS, preferred

That last row is the same idea as the write gate, one layer down. confirm_change does not accept a password, it accepts a short-lived single-use change_id that something else had to mint. RDS IAM auth does not accept a password either, it accepts a short-lived token that AWS had to mint. In both cases a durable secret is replaced by a capability that expires, and in both cases the system enforces that rather than a policy document promising it.

Deploying against RDS

export SAFEDB_BACKEND=postgres
export SAFEDB_PG_HOST=safedb.abc123.ca-central-1.rds.amazonaws.com
export SAFEDB_PG_DATABASE=safedb
export SAFEDB_PG_SSLMODE=verify-full
export SAFEDB_PG_SSLROOTCERT=/etc/ssl/certs/rds-global-bundle.pem
export SAFEDB_CREDENTIALS=rds-iam
export SAFEDB_AWS_REGION=ca-central-1

In the database, once:

CREATE ROLE safedb_reader LOGIN;
GRANT rds_iam TO safedb_reader;          -- IAM auth instead of a password
GRANT CONNECT ON DATABASE safedb TO safedb_reader;
GRANT USAGE ON SCHEMA public TO safedb_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO safedb_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO safedb_reader;

The IAM policy the server's role needs, which is the whole of it. Note that it grants connection as one specific database user, not blanket RDS access:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "rds-db:connect",
    "Resource": "arn:aws:rds-db:ca-central-1:123456789012:dbuser:db-ABCDEFGHIJKL/safedb_reader"
  }]
}

verify-full with the RDS CA bundle is what makes the TLS meaningful rather than decorative, and RDS IAM auth requires TLS anyway.

What is demonstrated versus documented. The Postgres backend is tested against a real Postgres on every push, in CI, via a service container. The AWS credential code is real and unit-tested against stubbed botocore, including a check that a genuine boto3 client produces a correctly signed 15-minute token. It has not yet been run against a live RDS instance. That is the one claim here that is documented rather than demonstrated, and it is called out rather than blurred.

Postgres and AWS settings

Environment variable

Default

Meaning

SAFEDB_BACKEND

sqlite

sqlite or postgres.

SAFEDB_PG_HOST / SAFEDB_PG_PORT

127.0.0.1 / 5432

Where the database is.

SAFEDB_PG_DATABASE / SAFEDB_PG_SCHEMA

safedb / public

Which database and schema.

SAFEDB_PG_READER_USER / SAFEDB_PG_WRITER_USER

safedb_reader / safedb_writer

The two logins. Keep them separate; running reads as the writer throws away the strongest guarantee this backend has.

SAFEDB_PG_SSLMODE / SAFEDB_PG_SSLROOTCERT

require / unset

Use verify-full with the RDS CA bundle against RDS.

SAFEDB_CREDENTIALS

env

env, secretsmanager or rds-iam.

SAFEDB_AWS_REGION / SAFEDB_AWS_SECRET_ID

falls back to AWS_REGION / unset

For the two AWS sources.


Running it yourself

git clone https://github.com/Hamzah-Muhammad/SafeDataBaseMCP.git
cd SafeDataBaseMCP

python -m venv .venv
.venv\Scripts\python -m pip install -r requirements-dev.txt     # Windows
# python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements-dev.txt

python -m pytest -m "not postgres"   # 149 tests, no database server needed
python examples/reference_client.py
python -m safe_db_mcp                # serve over stdio (a client drives it; it waits on stdin)

Requires Python 3.11 or newer. The core needs mcp and sqlparse and nothing else; psycopg and boto3 are optional extras pulled in only by the Postgres and AWS paths.

To run the full 178 including the Postgres backend, point the suite at any Postgres. It creates its own throwaway schema and reader role per test and drops them afterwards, so it will not disturb an existing database:

export SAFEDB_TEST_PG_DSN="host=127.0.0.1 port=5432 dbname=postgres user=postgres password=postgres sslmode=disable"
python -m pytest

The tests

They are written as a record of what the server refuses, so each rejection asserts on the reason as well as the refusal - a rule that quietly stops working cannot be hidden by a different rule catching the same statement.

File

What it proves

test_validation.py

The grammar itself: what is accepted, and that DROP, ALTER, CREATE, PRAGMA, ATTACH, VACUUM, COPY, DO, dollar quoting, stacked statements, comments, sqlite_master, pg_catalog, load_extension, pg_read_file, dblink and unqualified UPDATE/DELETE are each refused for the right reason.

test_engine.py

The same rules with a real database underneath. A refused write leaves the file untouched, propose_change genuinely rolls back, a change_id works once, an expired one is refused, and the read connection is rejected by SQLite itself when asked to write. Verified by reading the database directly, around the server.

test_postgres_backend.py

The Postgres backend against a live server: the SELECT-only reader really cannot write, foreign keys and primary keys come back correctly from pg_catalog, the preview recheck catches a moved row and lets an unrelated one through, and a refused confirm returns the proposal to pending. Skipped without a DSN, and it fails rather than skips in CI, so the skip can never quietly hide a broken backend.

test_aws_credentials.py

Credential resolution with botocore stubbed: the exact Secrets Manager call and its failure modes, the RDS IAM token generated for one host, port and user, and that a real boto3 client still signs a 15-minute token. Also that a password never reaches a repr or a backend description.

test_mcp_server.py

The guarantees again over the protocol, via an in-process client: the tool surface is exactly six entries, refusals arrive as protocol errors with a reason, and confirm_change accepts nothing but a change_id.

test_reference_client.py

Runs examples/reference_client.py as its own process, which spawns the server over stdio. Puts the second client path under CI.

CI runs ruff, black --check and pytest on Ubuntu and Windows, Python 3.11 and 3.13. The Ubuntu jobs bring up a real postgres:17 service container and run the whole suite against it. Windows runners cannot host service containers, so those jobs run everything except the Postgres tests; the Ubuntu jobs fail loudly if the Postgres tests are ever skipped there, so the backend cannot silently go uncovered.


Known limits

Worth stating plainly, since the point of the project is being precise about what a boundary does and does not give you.

  • The gate constrains this server, not the agent. An agent with a Bash tool can still open the database directly. This bounds what happens through this interface, which is the honest claim.

  • Proposals live in memory, in one process. Under stdio that is exactly one client session, which is the right scope: a proposal cannot leak between clients, and a restart drops every uncommitted change rather than leaving one confirmable later. A shared HTTP deployment would need a real store.

  • An exact diff means snapshotting the table. Fine below the 5,000 row threshold and deliberately given up above it, falling back to a rows-affected count with diff_available: false. It is also given up on a Postgres table with no primary key, because there would be no honest way to match rows across the write.

  • The preview recheck compares effects, not the whole table. Two callers proposing textually identical inserts within the TTL would not be detected as conflicting, because the effects are indistinguishable. Rechecking guards against a stale preview; it is not a distributed lock.

  • describe_table reads the catalog directly. PRAGMA table_info on SQLite, pg_catalog on Postgres. That is the server reading schema on its own behalf, against a table name already checked against the real table list. PRAGMA and pg_catalog from tool input are refused; the two paths do not meet.

  • Not run against live RDS yet. The Postgres backend is covered by CI against a real Postgres, and the AWS credential paths are unit-tested against stubbed botocore. Pointing it at an actual RDS instance is documented above but has not been executed.

License

MIT - see LICENSE.

Available Tools

6 tools
confirm_changeA

Commit a change that propose_change previewed.

The statement is re-validated and re-executed, then committed. The change_id must be one propose_change returned, still unused and not yet expired. There is no way to commit a write without one.

Args: change_id: The id returned by propose_change.

ParametersJSON Schema
NameRequiredDescriptionDefault
change_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral disclosure burden. It discloses that the statement is re-validated and re-executed before committing, and that commit requires a valid, unused, unexpired change_id. This goes beyond the tool name and helps the agent understand side effects and constraints, though it doesn't discuss failure modes or output.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-organized: a one-sentence summary, a short behavioral/constraint paragraph, and an Args section. Every sentence contributes meaningful information without repetition or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the essential context for correct invocation: where change_id comes from, the re-execution behavior, and the expiry/unused constraints. An output schema exists, so return-value details are not needed. It could mention what happens on invalid or expired IDs, but the current information is enough for a competent agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema only declares change_id as a required string with zero coverage in its description. The tool description compensates by stating 'change_id: The id returned by propose_change,' explaining the parameter's origin and relationship to another tool. It would be stronger with validation rules, but for a single parameter this is sufficient.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Commit a change that propose_change previewed.' This clearly identifies the action and the object, and it distinguishes confirm_change from propose_change by positioning it as the commit step. The added sentence 'There is no way to commit a write without one' reinforces its unique role among the sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description states that change_id must come from propose_change and be unused and unexpired, which tells the agent when to call this tool: after a proposal exists and when a commit is intended. It does not explicitly name alternatives or say 'do not use for read-only queries,' but the lifecycle context makes the usage condition clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

describe_tableA

Show the columns, types, constraints and foreign keys of one table.

Read-only. Runs immediately, no confirmation needed.

Args: table: Name of the table to describe.

ParametersJSON Schema
NameRequiredDescriptionDefault
tableYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It explicitly states 'Read-only' and 'no confirmation needed,' which explains side-effect and execution expectations. It does not cover error behavior or permission requirements, but those are minor for a simple describe operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tightly constructed: purpose in the first sentence, behavior in the second, and a minimal Args block. There is no filler, and the most important information is front-loaded. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given a single required parameter and an output schema, the description covers the key aspects: what the tool returns, how to specify the table, and that it is read-only and immediate. It doesn't explicitly explain when to prefer it over but siblings, but that gap is already captured under usage guidelines. Overall it is sufficient for invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It does: the Args block defines 'table' as 'Name of the table to describe,' giving meaning beyond the bare schema property. It doesn't add constraints like case-sensitivity or schema qualification, but it fully covers the only parameter's semantic role.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb 'Show' and names the exact resource: 'columns, types, constraints and foreign keys of one table.' This clearly distinguishes it from siblings like list_tables (probably lists tables) and propose_change/confirm_change (change tools). The purpose is immediately unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description signals usage context through 'Read-only. Runs immediately, no confirmation needed,' implying this is a safe inspection tool that does not require the change workflow. However, it never explicitly names alternatives or states when not to use it, so the routing relies on inference rather than explicit guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_pending_changesA

List the proposed changes that are still awaiting confirmation.

Read-only. Shows each pending change_id, its SQL and how long it has left before it expires.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full disclosure burden. It explicitly marks the operation read-only and states the returned fields (change_id, SQL, expiry remaining), which gives the agent a clear behavioral model. It does not discuss edge cases like empty results, but that is minor for a zero-parameter list operation with an output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short, focused sentences with no fluff. The core purpose comes first, and the read-only note plus output contents are placed immediately after.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter read-only listing tool with an output schema, the description is complete: it identifies the resource, the state of the items, and the key fields returned. Nothing necessary for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema is empty and has no parameters, so parameter documentation is not needed. The description instead adds value by explaining what each listed item contains, which is appropriate given there are no inputs to describe.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a clear verb ('List') with a specific resource ('proposed changes that are still awaiting confirmation'), and distinguishes the tool from its siblings like run_query and confirm_change by focusing specifically on pending changes. The scope is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly implies this tool is for reviewing proposed changes before they are confirmed, and the read-only note helps the agent understand it is safe to inspect. It does not explicitly name alternatives or say when not to use it, so I cannot give a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_tablesA

List every table in the database with its row count and columns.

Read-only. Runs immediately, no confirmation needed.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It explicitly discloses that the operation is read-only and runs immediately without confirmation. This is exactly the kind of safety and side-effect information an agent needs before invoking an unknown tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences deliver complete value: the first states exactly what the tool returns, and the second adds the critical behavioral context. There is no filler, redundant schema repetition, or unnecessary detail.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters and an output schema that presumably documents the return shape, the description covers everything necessary for an agent to call this tool confidently. It names the returned content, confirms read-only safety, and clarifies execution behavior. No material information is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so there is no parameter ambiguity for the description to resolve. The baseline of 4 applies because the description cannot add parameter-level meaning where none exist. The schema already confirms 100% coverage with no properties.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: 'List every table in the database with its row count and columns.' This clearly distinguishes the tool from describe_table, which targets a single table. There is no ambiguity about what the caller gets.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description conveys clear use context: it is a read-only, immediate action with no confirmation required. It does not explicitly name sibling tools or say when not to use it, but the contrast with confirmation-requiring siblings is implied. A small gap is the lack of explicit routing versus describe_table.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

propose_changeA

Preview a write without committing it, and get a change_id back.

The statement is validated, executed inside a transaction to compute a real preview, then rolled back. Nothing is written. Show the returned preview to the human and call confirm_change with the change_id to commit it. The id is single use and expires.

Accepts one INSERT, UPDATE or DELETE against an existing table. UPDATE and DELETE must have a WHERE clause.

Args: sql: A single INSERT, UPDATE or DELETE statement.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full disclosure burden, and it excels at this. It reveals that the statement is actually executed in a transaction and rolled back, that nothing is written, and that the returned id is single-use and expires. These are exactly the behavioral traits an agent needs to understand to use the tool safely.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded, covering purpose, mechanics, follow-up guidance, and constraints in a few tight sentences. No filler or redundant restatement of the tool name exists.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the single parameter and the rich behavioral detail provided, the description is complete for selecting and using the tool correctly. It explains the write-preview workflow, the follow-up confirm_change step, and the expiry behavior. An output schema exists to cover return-value details, so nothing critical is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema only defines sql as a string, but the description adds critical meaning: it must be a single INSERT, UPDATE, or DELETE statement against an existing table, with a mandatory WHERE clause for UPDATE and DELETE. This goes well beyond the raw schema and fully documents the parameter's constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states exactly what the tool does: preview a write without committing it and return a change_id. It distinguishes itself from sibling confirm_change by labeling this as the preview step, and from run_query by focusing on write statements.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly explains when to use the tool: before committing a write, with the preview shown to a human and confirm_change called afterward. It also gives explicit constraints on accepted statements (single INSERT/UPDATE/DELETE, WHERE clause required for UPDATE/DELETE), though it does not enumerate alternatives such as when to prefer run_query.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_queryA

Run one read-only SELECT and return the rows.

Only a single SELECT is accepted. Anything else - a write, DDL, PRAGMA, ATTACH, a stacked statement, a comment - is rejected before it reaches the database, and the connection used here is opened read-only anyway.

Args: sql: A single SELECT statement.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden of behavioral disclosure. It reveals that the connection is opened read-only, that input is validated before reaching the database, and that non-SELECT statements are rejected. This is significant behavioral context that would otherwise be invisible to the agent and goes well beyond the input schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the primary purpose, followed by necessary safety details, and ends with an Args section. Each sentence contributes either to scope, constraints, or parameter meaning, with no filler or redundant schema repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter tool with an output schema available, the description covers the essential behavioral constraints and parameter meaning. It does not explicitly mention error behavior or how returned rows are structured, but the output schema likely handles the return shape. Minor gap: it could more directly say 'use this for read-only SQL exploration,' but the read-only emphasis already communicates that.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema only defines sql as a required string with no description, so schema coverage is 0%. The description compensates partly by specifying 'A single SELECT statement,' which adds the essential constraint on the parameter. It could add more detail about whether semicolons are allowed or how to structure complex queries, but for a single parameter the provided semantics are adequate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Run one read-only SELECT and return the rows,' a clear verb-resource pair that defines exactly what the tool does. It also distinguishes itself from siblings by restricting input to SELECT statements, while siblings like list_tables and describe_table serve different schema-introspection purposes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly states that only a single SELECT is accepted and that writes, DDL, PRAGMA, ATTACH, stacked statements, and comments are rejected, which tells the agent when this tool is appropriate and what it is not for. It does not explicitly name alternative tools for write operations, but the sibling list and the rejection behavior make the boundary clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 6 tool updatesv1.0.0
    • First observedconfirm_change
    • First observeddescribe_table
    • First observedlist_pending_changes
    • First observedlist_tables
    • First observedpropose_change
    • First observedrun_query

TDQS

A4.5/5.0
Disambiguation5/5

Each tool has a clearly distinct role: schema discovery, read-only querying, and the two-phase write flow are separated without overlap. The read/write boundary between run_query and propose_change is especially well-defined.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern: list_tables, describe_table, run_query, propose_change, confirm_change, list_pending_changes. The naming makes the action and target immediately predictable.

Tool Count5/5

Six tools is a well-scoped size for a safe database interface. Each tool earns its place, covering schema browsing, read-only access, write previewing, confirmation, and pending-change inspection without unnecessary bloat.

Completeness4/5

The core workflow is complete: browse schema, query data, propose changes, review pending changes, and commit. The only notable gap is the lack of an explicit way to cancel a pending change, though expiration partially covers this.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to securely interact with multiple databases (MySQL, PostgreSQL) via natural language queries, with cross-database querying and enterprise-grade security.
    21
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    Enables LLM agents to query databases with read-only access, while requiring human approval for writes through a token-based confirmation system.
    6
    GPL 3.0
  • F
    license
    Not graded
    quality
    C
    maintenance
    Provides read-only database access for AI agents across multiple databases (Postgres, MySQL, MongoDB, Elasticsearch) with enforced read-only guarantees and separate tools for prod and non-prod environments.
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hamzah-Muhammad/SafeDataBaseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server