SafeDataBaseMCP
Provides guarded database operations for SQLite, including read-only queries and a two-step write process with transactional preview and confirmation.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SafeDataBaseMCPPropose changing user 42's email to jane@new.com and show me the diff before confirming."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
SafeDataBaseMCP
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.

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 | 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 |
"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 |
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:
sqlparseclassifies the statement; anything that is not a singleSELECT(read path) or a singleINSERT/UPDATE/DELETEagainst a known table (write path) is refused.The flattened token stream is scanned for forbidden keywords, forbidden functions and SQLite's internal tables, so DML hidden in a CTE or a
UNIONis caught too.Reads execute on a connection opened
mode=ro, so even a validation bug could not produce a write on the read path.propose_changecomputes its preview inside a transaction that is rolled back in afinallyblock, so a preview cannot persist even if the statement raises.confirm_changere-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
claudeClaude 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-databaseIf you would rather register it yourself instead of using the shipped file:
claude mcp add safe-database -- python -m safe_db_mcpThen 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: 1And 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.pyIt 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 |
| read | Every table with its row count and column names. |
| read | Columns, types, nullability, defaults, primary keys, foreign keys. |
| read | One |
| write, step 1 | Validates, runs in a transaction, snapshots a row-level diff, rolls back, returns a single-use |
| write, step 2 | Re-validates, recomputes the preview inside the committing transaction, and commits only if it still matches. Once. |
| 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_mcpPoint 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 |
|
| Which SQLite file to serve. |
|
| How long a |
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_mcpSeeding 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 |
| A separate |
Rows matched for the diff |
| The declared primary key. |
Concurrent writers |
|
|
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 |
|
| Local development |
| An AWS Secrets Manager secret, read in the | RDS with a rotated secret |
| Nowhere. There is no password. | 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-1In 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 |
|
|
|
|
| Where the database is. |
|
| Which database and schema. |
|
| The two logins. Keep them separate; running reads as the writer throws away the strongest guarantee this backend has. |
|
| Use |
|
|
|
| falls back to | 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 pytestThe 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 |
| The grammar itself: what is accepted, and that |
| The same rules with a real database underneath. A refused write leaves the file untouched, |
| The Postgres backend against a live server: the SELECT-only reader really cannot write, foreign keys and primary keys come back correctly from |
| Credential resolution with |
| 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 |
| Runs |
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_tablereads the catalog directly.PRAGMA table_infoon SQLite,pg_catalogon Postgres. That is the server reading schema on its own behalf, against a table name already checked against the real table list.PRAGMAandpg_catalogfrom 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 toolsconfirm_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.
| Name | Required | Description | Default |
|---|---|---|---|
| change_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| table | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
6 tool updates
v1.0.0- First observed
confirm_change - First observed
describe_table - First observed
list_pending_changes - First observed
list_tables - First observed
propose_change - First observed
run_query
TDQS
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.
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.
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.
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
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
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
AI agents propose database changes as reviewable requests — no direct write access.
AI agents need permission before production SQL writes. Pilot $100 · Gateway $299. Lint≠authorize.
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to securely interact with multiple databases (MySQL, PostgreSQL) via natural language queries, with cross-database querying and enterprise-grade security.21MIT
- AlicenseBqualityAmaintenanceEnables LLM agents to query databases with read-only access, while requiring human approval for writes through a token-based confirmation system.6GPL 3.0
- AlicenseAqualityCmaintenanceEnables AI agents to dynamically register and query MySQL-compatible databases at runtime with built-in destructive SQL protection.61MIT
- FlicenseNot gradedqualityCmaintenanceProvides 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
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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