Most of the conversation about AI agent safety focuses on authentication and authorization: who can call what, which roles have which permissions, whether the API key is scoped correctly. That conversation matters. But there is a second layer of safety that gets far less attention — helping the agent itself understand whether the operation it is about to execute is safe to run without confirmation.
The MCP 2025-11-25 specification introduced tool annotations specifically to solve this problem. They are not access controls. They do not enforce anything. What they do is communicate the nature of a tool to the client, so that clients and agents can make informed decisions about whether to run something automatically or pause for human review.
For database access — where the gap between a safe SELECT and a catastrophic DELETE is a few characters — this distinction is fundamental.
What Tool Annotations Are
In MCP, every tool definition can include an annotations object. The 2025-11-25 spec defines four boolean hint properties:
readOnlyHint: The tool does not modify any data. It is safe to call repeatedly without side effects.destructiveHint: The tool may delete or permanently alter data. Clients should request explicit user confirmation before invoking it.idempotentHint: Calling the tool multiple times with the same arguments produces the same result. Safe to retry on failure.openWorldHint: The tool interacts with external systems that may have effects beyond the MCP server itself (sending emails, charging payments, etc.).
Here is what the wire format looks like in a tools/list response:
{
"name": "query_customers",
"description": "Fetch customers matching filter criteria",
"inputSchema": {
"type": "object",
"properties": {
"status": { "type": "string" },
"_limit": { "type": "integer" }
}
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
}
The spec is explicit that these are hints, not enforceable guarantees. A malicious or buggy server could lie about them. The warning in the spec is direct: clients MUST consider tool annotations to be untrusted unless they come from a trusted server. This is an important caveat we will return to.
But for a trusted server — one managing a database through a governed API layer — these annotations enable something powerful: the agent can reason about safety before committing to an action.
Why This Matters for Database Operations
Database operations fall into a natural safety hierarchy:
| Operation | SQL | Safe to auto-execute? | Annotation |
|---|---|---|---|
| Read records | SELECT | Yes | readOnlyHint: true |
| Insert new record | INSERT | Maybe, with confirmation | idempotentHint: false |
| Update existing record | UPDATE | Needs review | destructiveHint: true |
| Delete record | DELETE | Requires explicit confirmation | destructiveHint: true |
| Truncate table | TRUNCATE | Never auto-execute | destructiveHint: true |
Without annotations, an AI agent has no protocol-level way to distinguish between these categories. It sees a list of tools, each with a name and a description string. The agent has to infer safety from natural language — and natural language inference is unreliable when the cost of being wrong is data loss.
Consider a support agent tasked with “cleaning up inactive accounts.” Without annotations, the agent might:
- Call
list_accountswithstatus=inactiveto identify targets - Immediately call
delete_accountfor each result, because that is what “cleaning up” implies
With correct annotations, step 2 triggers a destructiveHint: true signal. A well-implemented MCP client will either pause and request user confirmation, or refuse to execute without explicit authorization. The annotation does not prevent the operation — it creates a checkpoint.
This is the difference between an agent that assists and an agent that causes incidents.
The Trust Caveat Is Real
The spec’s warning about untrusted annotations deserves emphasis. Any MCP server can claim any annotation values it wants. An attacker who controls an MCP server you connect to could label a DROP TABLE operation as readOnlyHint: true and your agent might execute it without confirmation.
This is why the trust model for MCP database servers matters enormously. When you connect an agent to a third-party MCP server you downloaded from the internet, you are trusting that server’s annotation claims. The MCP spec tells clients to treat annotations as untrusted from unknown sources — but most current MCP clients do not implement that skepticism correctly or at all.
The practical implication: for production database access, you want to run your own MCP server, one you control, with annotations you have verified. The server’s source code should be auditable. Its annotation values should be derived from its actual behavior, not declared in a config file.
This is exactly the architectural position Faucet occupies.
How Faucet Implements MCP Tool Annotations
Faucet’s built-in MCP server generates tool definitions directly from your database schema and applies annotations based on the HTTP method semantics of the underlying REST API:
| HTTP Method | SQL Operation | readOnlyHint | destructiveHint | idempotentHint |
|---|---|---|---|---|
GET | SELECT | true | false | true |
POST | INSERT | false | false | false |
PUT / PATCH | UPDATE | false | true | true |
DELETE | DELETE | false | true | false |
The annotations are not configured — they are derived. A GET-based tool is structurally read-only because the implementation uses a SELECT query with no writes. A DELETE-based tool is structurally destructive because the implementation executes a DELETE statement. There is no annotation config to misconfigure or forget to update when you add a new table.
Connect Faucet to your database and start the MCP server:
brew install faucetdb/tap/faucet
faucet connection add mydb --driver postgres --dsn "postgres://user:pass@localhost/mydb"
faucet serve
The MCP endpoint is available at http://localhost:8080/mcp. Add it to Claude Desktop or any MCP client:
{
"mcpServers": {
"mydb": {
"command": "faucet",
"args": ["mcp", "--connection", "mydb"]
}
}
}
Now run tools/list against it and you get properly annotated tools for every table in your database:
{
"tools": [
{
"name": "list_customers",
"description": "List records from the customers table",
"inputSchema": { ... },
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
},
{
"name": "delete_customer",
"description": "Delete a record from the customers table by ID",
"inputSchema": { ... },
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": false
}
}
]
}
The agent connecting to this server gets a complete, correctly annotated tool surface for every table — without writing a line of MCP server code.
Combining Annotations with RBAC
Tool annotations tell the agent what an operation does. RBAC tells the server what the agent is allowed to do. These are complementary controls, and both are necessary.
An agent with a readOnlyHint: true tool might still try to call a write endpoint if it is confused. RBAC catches that at the server. An agent with write permissions but no destructiveHint guidance might execute a delete without pausing. Annotations catch that at the client.
In Faucet, you layer these controls with role definitions:
# faucet.yaml
roles:
support-agent:
services:
- name: mydb
tables:
customers:
allow: [GET] # RBAC: read-only at the server level
deny_columns: [ssn, payment_method]
tickets:
allow: [GET, POST] # RBAC: can create tickets, not delete
faucet apikey create --role support-agent --name "claude-support-bot"
The support agent gets:
- API-key-scoped RBAC: physically cannot call DELETE endpoints regardless of what it tries
- MCP tool annotations: GET tools are labeled
readOnlyHint: true, POST tools haveidempotentHint: false - Column-level filtering:
ssnandpayment_methodfields stripped from all responses
The annotations layer over RBAC to give the agent intent signal even within its permitted operation set. A POST to create a ticket is not destructive, but it is not idempotent either — the agent should not retry it silently on timeout. That is exactly what idempotentHint: false communicates.
The Tool List Explosion Problem
One practical issue with MCP and databases: a schema with 100 tables generates 400+ tools (list, get, create, delete per table). This overwhelms LLM context windows and makes tool selection unreliable.
The MCP spec’s listChanged notification mechanism helps here — servers can signal when tool availability changes, allowing clients to refresh their tool list dynamically rather than loading everything at startup. Faucet uses this to support selective tool exposure: you can restrict the MCP server to specific tables, keeping the tool list focused.
# Expose only the tables your agent actually needs
faucet mcp --connection mydb --tables customers,orders,products
This generates 12 tools instead of 400+. The agent gets a complete, correctly annotated tool surface for exactly the data it needs — nothing more.
A smaller tool list is also a security benefit. Every tool you expose is a potential attack vector for prompt injection. Minimizing the exposed surface area reduces risk even when RBAC would catch unauthorized access anyway.
What Good MCP Clients Do with Annotations
The annotations only matter if MCP clients act on them. The current client ecosystem is inconsistent. Claude Desktop respects destructiveHint and surfaces a confirmation prompt for destructive tools. Other clients vary.
What a fully compliant client should do:
-
Read-only tools (
readOnlyHint: true): Execute automatically as part of agent reasoning. No confirmation required. Safe to call multiple times as the agent refines its understanding. -
Non-idempotent write tools (
idempotentHint: false): Present the agent’s intended action to the user before executing. Include the tool name and arguments in the confirmation. -
Destructive tools (
destructiveHint: true): Require explicit user confirmation, not just passthrough. Display the target resource (which record, how many rows) before proceeding. -
Open-world tools (
openWorldHint: true): Treat with the same caution as destructive tools, because the effects extend beyond the database.
When Faucet generates a MCP server for your database, the annotation values it produces are designed with this client behavior model in mind. Read queries are optimized to run freely. Write operations carry hints that responsible clients will surface to users. The architecture assumes agents will be aggressive — and builds in the signals to make clients appropriately cautious.
Why the Protocol Approach Beats Application-Level Heuristics
Before tool annotations existed, developers built safety heuristics into agent prompts: “always confirm before deleting data,” “never run destructive operations without asking.” These work sometimes, but they fail in predictable ways.
Prompt-level safety instructions are not enforced by the protocol. They can be overridden by subsequent instructions in the same conversation. They depend on the model’s interpretation of natural language, which varies by model version and context window state. They are invisible to the MCP client, which cannot apply its own confirmation UI because it does not know which tools are dangerous.
Protocol-level annotations solve each of these failure modes. They are in the wire protocol, not the prompt. They are consistent across models and clients. They are visible to the client layer, which can apply its own UI controls independently of what the model is instructed to do.
This is what makes the 2025-11-25 spec a meaningful safety milestone for agentic database access — not a marginal feature addition, but a structural improvement to how safety information flows through the system.
Getting Started
Faucet is open-source under the Apache 2.0 license. The MCP server with full tool annotations ships in the same binary as the REST API server.
# Install
brew install faucetdb/tap/faucet
# Add your database connection
faucet connection add prod --driver postgres --dsn "postgres://user:pass@host/db"
# Start both REST API and MCP server
faucet serve
# Or run MCP only (stdio, for Claude Desktop)
faucet mcp --connection prod
# Or restricted to specific tables
faucet mcp --connection prod --tables orders,customers,products
Supported databases: PostgreSQL, MySQL, SQL Server, Oracle, Snowflake, SQLite.
GitHub: github.com/faucetdb/faucet
The MCP specification gave database tools a way to declare their safety profile in the protocol itself. The question is whether the servers you connect your agents to are actually implementing it correctly — or just asserting safe annotations on operations that are not.