-
Notifications
You must be signed in to change notification settings - Fork 0
Databases
One section per database: how to write the connection URL, how to create a read-only account, what the tools show there, and examples.
Reading the formats. Each section shows the URL format with [PLACEHOLDERS], then a real example. Replace each placeholder with your own value:
| Placeholder | What to put there |
|---|---|
[USER], [PASSWORD]
|
The database account. Leave out :[PASSWORD] if there is none, or [USER]:[PASSWORD]@ if there is no login at all |
[HOST] |
The server's address, e.g. localhost or db.example.com
|
[PORT] |
The server's port. Optional: leave out :[PORT] to use the usual one given in each section |
[DATABASE] |
The database name. Optional for most engines |
If a password contains @, /, #, : or ?, leave it out of the URL and set it with DB_PASSWORD instead.
Two things are the same everywhere:
-
Browse tools work on every database:
list_tables,describe_table,get_table_sample, and, where the database has them,get_table_indexesandget_foreign_keys. The Using the Tools page explains each one. -
Each family has its own query tool:
run_queryfor SQL databases,find_documentsand friends for MongoDB,redis_commandfor Redis,searchfor Elasticsearch. Ask for the wrong one and the server tells the assistant which one to use.
Use a read-only account. The server refuses writes on its own, but a read-only account means nothing can ever write, whatever happens. Each section shows how to create one.
URL
mysql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
mariadb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
The usual port is 3306. Example: mysql://readonly:secret@localhost:3306/shop
| Option | Meaning |
|---|---|
?ssl=true |
Require TLS |
?ssl-mode=VERIFY_IDENTITY |
Require TLS and check the certificate |
The database is optional; without it, ask the assistant to list databases and switch to one.
Read-only account
CREATE USER 'readonly'@'%' IDENTIFIED BY 'a strong password';
GRANT SELECT, SHOW VIEW ON shop.* TO 'readonly'@'%';What you'll see
-
list_databases: the schemas on the server, withoutmysql,sysand the other internal ones (ask for "system databases too" to include them). -
describe_table: columns, types, keys and defaults (SHOW COLUMNS). -
get_table_indexes,get_foreign_keys: both supported. -
run_query:SELECT,WITH,SHOW,DESCRIBE,DESCandEXPLAIN.
Example
{ "tool": "run_query", "arguments": { "query": "SELECT status, COUNT(*) AS n FROM orders GROUP BY status" } }Worth knowing
-
INTO OUTFILE,LOAD DATA,SLEEP()andBENCHMARK()are refused. - A column named after a write keyword (
update,delete) inside aWITHquery must be quoted with backticks.
Also works with hosted and extended PostgreSQL, such as Supabase, Neon, TimescaleDB and Amazon RDS / Aurora for PostgreSQL.
URL
postgres://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
postgresql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
The usual port is 5432. Example: postgres://readonly:secret@localhost:5432/myapp?sslmode=require
| Option | Meaning |
|---|---|
?sslmode=require |
Use TLS without checking the certificate (like psql) |
?sslmode=verify-full |
Use TLS and check the certificate |
Read-only account (PostgreSQL 14 and newer)
CREATE ROLE readonly LOGIN PASSWORD 'a strong password';
GRANT pg_read_all_data TO readonly;On older versions, grant per schema:
GRANT CONNECT ON DATABASE myapp TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;What you'll see
-
list_tables: tables inpublicby name, and tables in other schemas asschema.table, e.g.reporting.monthly. -
describe_table: acceptstableorschema.table. -
get_table_indexes,get_foreign_keys: both supported. -
run_query:SELECT,WITH,SHOW,EXPLAIN,VALUESandTABLE. - Dates and timestamps come back exactly as stored, never shifted by time zone.
Example
{ "tool": "run_query", "arguments": { "query": "SELECT date_trunc('day', created_at) AS day, count(*) FROM signups GROUP BY 1 ORDER BY 1 DESC LIMIT 7" } }Worth knowing
- Every query runs inside a read-only transaction that is always rolled back, which also works through connection poolers like PgBouncer.
- Functions that reach outside the database or change server state are refused:
dblink,lo_export,pg_terminate_backend,set_config,pg_sleepand a few more. -
SELECT ... INTOis refused, because it creates a table.
URL
sqlite:///[PATH_TO_FILE]
Example: sqlite:///Users/me/data/app.db
Three slashes before an absolute path. A relative path (sqlite://data/app.db) is resolved from the folder the server starts in, which is easy to get wrong, so prefer absolute paths.
Read-only account
Not needed: the file is always opened read-only. Make sure the server can read the file.
What you'll see
-
list_tables: tables and views. -
describe_table,get_table_indexes,get_foreign_keys: all supported. -
run_query:SELECT,WITH,EXPLAINandVALUES. Read a pragma through its function form, e.g.SELECT * FROM pragma_table_info('notes'). -
use_databasedoes not apply: the file is the database. Connect to a different file instead.
Example
{ "tool": "run_query", "arguments": { "query": "SELECT title, created_at FROM notes ORDER BY created_at DESC LIMIT 10" } }Worth knowing
- Queries run in a separate process, so one that runs past the timeout is stopped without freezing anything.
- In Docker, mount the folder holding the file:
-v /Users/me/data:/data:ro, then usesqlite:///data/app.db.
URL
mssql://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
sqlserver://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
The usual port is 1433. Example: mssql://readonly:secret@localhost:1433/Sales?trustServerCertificate=true
| Option | Meaning |
|---|---|
?trustServerCertificate=true |
Accept a self-signed certificate (local and development servers) |
?encrypt=false |
Turn off encryption (only for very old servers) |
?applicationIntent=ReadOnly |
Route to a readable secondary in an availability group |
Connections are encrypted by default.
Read-only account
CREATE LOGIN readonly WITH PASSWORD = 'a Strong password 1';
USE Sales;
CREATE USER readonly FOR LOGIN readonly;
ALTER ROLE db_datareader ADD MEMBER readonly;What you'll see
-
list_tables: tables indboby name, others asschema.table. -
describe_table,get_table_indexes,get_foreign_keys: all supported. -
run_query:SELECTandWITH. UseTOP (10)rather thanLIMIT 10.
Example
{ "tool": "run_query", "arguments": { "query": "SELECT TOP (5) CustomerName, SUM(Total) AS total FROM Orders GROUP BY CustomerName ORDER BY total DESC" } }Worth knowing
- SQL Server lets statements follow each other without a semicolon, so every query is checked for write keywords anywhere in it. A column named like one (
set,open) must be bracketed:[set]. - Every query runs inside a transaction that is always rolled back.
-
SELECT ... INTO,OPENROWSETandOPENQUERYare refused.
Connects over the HTTP interface, which is what ClickHouse Cloud and most hosted services expose.
URL
clickhouse://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
clickhouse+https://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]
The usual port is 8123, or 8443 with clickhouse+https. Example: clickhouse://reader:secret@localhost:8123/analytics
The database defaults to default.
Read-only account
CREATE USER reader IDENTIFIED BY 'a strong password' SETTINGS readonly = 1;
GRANT SELECT ON analytics.* TO reader;
GRANT SELECT ON system.data_skipping_indices TO reader;The last grant lets get_table_indexes show skipping indices; everything else works without it.
What you'll see
-
describe_table: columns and types. -
get_table_indexes: the partition, sorting, primary and sampling keys, plus any data-skipping indices. -
get_foreign_keys: not applicable; ClickHouse has none. -
run_query:SELECT,WITH,SHOW,DESCRIBE,DESC,EXPLAINandEXISTS.
Example
{ "tool": "run_query", "arguments": { "query": "SELECT toStartOfHour(ts) AS hour, count() FROM events WHERE ts > now() - INTERVAL 1 DAY GROUP BY hour ORDER BY hour" } }Worth knowing
- Table functions that reach outside the server are refused:
url(),file(),s3(),remote(),mysql(),executable()and similar. - For an account that is not already read-only, the server asks ClickHouse to run every query in read-only mode, with a time limit and a cap on result size.
URL
mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]?authSource=admin
mongodb://[USER]:[PASSWORD]@[HOST1]:[PORT],[HOST2]:[PORT]/[DATABASE]?replicaSet=[REPLICA_SET]
mongodb+srv://[USER]:[PASSWORD]@[CLUSTER_HOST]/[DATABASE]
The usual port is 27017; mongodb+srv (MongoDB Atlas) takes no port. Example: mongodb://reader:secret@localhost:27017/myapp?authSource=admin
Any MongoDB connection option can go in the query string. authSource=admin is needed when the user was created in the admin database, which is the usual case.
Read-only account
use admin
db.createUser({ user: "reader", pwd: "a strong password", roles: [{ role: "read", db: "myapp" }] })What you'll see
-
list_tables: the collections. -
describe_table: the fields found in a sample of 100 documents, each with its types and how often it appears, plus the collection's JSON Schema validator if it has one. -
get_table_indexes: the indexes. -
get_table_sample: a few real documents.
Query tools
| Tool | For |
|---|---|
find_documents |
Find documents with a filter, projection, sort and limit |
aggregate |
Run an aggregation pipeline |
count_documents |
Count documents matching a filter |
distinct_values |
List the different values of a field |
Filters use MongoDB's Extended JSON, so ids and dates work: {"_id": {"$oid": "64f..."}}, {"createdAt": {"$gt": {"$date": "2026-09-01T00:00:00Z"}}}.
Examples
{ "tool": "find_documents", "arguments": { "collection": "orders", "filter": { "total": { "$gt": 100 } }, "sort": { "createdAt": -1 }, "limit": 5 } }
{ "tool": "aggregate", "arguments": { "collection": "users", "pipeline": [{ "$group": { "_id": "$country", "n": { "$sum": 1 } } }, { "$sort": { "n": -1 } }] } }
{ "tool": "count_documents", "arguments": { "collection": "users", "filter": { "active": true } } }
{ "tool": "distinct_values", "arguments": { "collection": "orders", "field": "status" } }Worth knowing
-
$outand$mergeare refused anywhere in a pipeline, and so are$where,$functionand$accumulator. - MongoDB has no read-only session, so the
readrole is your server-side protection.
URL
redis://[HOST]:[PORT]/[DB_NUMBER]
redis://:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
redis://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
rediss://[USER]:[PASSWORD]@[HOST]:[PORT]/[DB_NUMBER]
The usual port is 6379. Example: redis://reader:secret@localhost:6379/0
The number at the end is the database (0 by default). rediss:// uses TLS.
Read-only account (Redis 6 and newer)
ACL SETUSER reader on >a-strong-password ~* &* -@all +@read +ping +info +command|info
+command|info matters: before running a command for the assistant, the server asks Redis itself whether that command is read-only, and refuses it if it cannot ask.
What you'll see
-
list_databases: the numbered databases that hold keys. -
list_tables: keys, found withSCAN, neverKEYS. Give a pattern for large instances:session:*. At most 1000 are listed. -
describe_table: a key's type, time to live, length, encoding and memory use. -
get_table_sample: the start of the value, read the right way for its type (a string, hash fields, list items, set members, scored members, stream entries or a JSON document).
Query tool: redis_command
Read commands only, such as GET, MGET, HGET, HGETALL, LRANGE, SMEMBERS, ZRANGE, XRANGE, SCAN, TYPE, TTL, INFO and the RedisJSON, RediSearch and RedisTimeSeries read commands.
{ "tool": "redis_command", "arguments": { "command": "HGETALL", "args": ["user:42"] } }
{ "tool": "redis_command", "arguments": { "command": "ZRANGE", "args": ["leaderboard", "0", "9", "WITHSCORES"] } }Worth knowing
-
KEYSis refused because it freezes a large server; uselist_tableswith a pattern orSCAN. -
CONFIG GETis refused because it can reveal the password.
URL
elasticsearch://[USER]:[PASSWORD]@[HOST]:[PORT]
elasticsearch+https://[USER]:[PASSWORD]@[HOST]:[PORT]
opensearch://[USER]:[PASSWORD]@[HOST]:[PORT]
opensearch+https://[USER]:[PASSWORD]@[HOST]:[PORT]
elasticsearch+https://[HOST]:[PORT]?api_key=[API_KEY]
The usual port is 9200. Example: elasticsearch+https://elastic:secret@search.example.com:9200
There is no database part. ?api_key= authenticates with an API key, which is never shown or logged.
Read-only account (Elasticsearch)
PUT _security/role/mcp_reader
{ "cluster": ["monitor"], "indices": [{ "names": ["*"], "privileges": ["read", "view_index_metadata", "monitor"] }] }Then create a user with that role, or an API key limited to it. On OpenSearch, the built-in readall role plus cluster_monitor does the same.
What you'll see
-
list_tables: the indices, without hidden.ones unless your pattern starts with a dot. -
describe_table: the index mapping. -
get_table_sample: a few documents. -
list_databases,use_database: not applicable.
Query tool: search
Takes an index (or pattern, like logs-*) and a normal search body.
{ "tool": "search", "arguments": { "index": "logs-*", "body": { "query": { "match": { "level": "error" } }, "sort": [{ "@timestamp": "desc" }], "size": 10 } } }
{ "tool": "search", "arguments": { "index": "orders", "body": { "size": 0, "track_total_hits": true } } }
{ "tool": "search", "arguments": { "index": "orders", "body": { "size": 0, "aggs": { "by_status": { "terms": { "field": "status" } } } } } }The second example is a count; the third groups by a field.
Worth knowing
- At most 100 hits per search.
-
scrollandpitare refused, because they leave state behind on the server.
User guide
Developer guide
- Architecture
- Design Patterns
- Domain and Configuration
- Drivers
- Read Only Enforcement
- Tools internals
- Call Logging internals
- Server Lifecycle
- Testing
- Release Process
Links