
DB Explorer
io.github.AYUSH-148由 GitHub 账号 AYUSH-148 发布v0.1.0更新于 Oct 27, 2026
Read-only SQL for PostgreSQL, MySQL and SQLite: schema, safe SELECTs, EXPLAIN, index advice
概览
只读 MCP 服务器,让助手在 PostgreSQL、MySQL 或 SQLite 上查看表结构、执行带行数上限的 SELECT、读取执行计划并获得索引建议。
- 可以做什么
- 查看结构:列出表与视图、列、主外键、索引、行数和少量样例行,支持分页与名称过滤
- 执行查询:运行单条经校验的只读 SELECT,带行数上限和截断标记
- 分析性能:获取数据库原生执行计划与 CREATE INDEX 建议
- 检查结构问题:报告缺少主键、外键无索引、宽表和无索引表
- 审阅迁移:收集列、键和索引信息,并校验 up/down 脚本而不执行
- 适用场景
- 适合让助手用自然语言回答关系型数据库的问题、排查慢查询,或起草并检查迁移脚本,同时从结构上杜绝写入。适用于本地开发与分析场景,前提是能提供一个只读的数据库账号。
- 运行要求
- 以 Python 包形式通过 stdio 在本地运行,需要 Python 3.11+ 和 uv。需要 DATABASE_URL,即 PostgreSQL、MySQL 或 SQLite 的 SQLAlchemy 连接串;不设置时使用内置演示 SQLite 数据库。可选的 HTTP 模式还需 MCP_AUTH_TOKEN 或 MCP_AUTH_TOKENS 及传输相关设置。无需 LLM API 密钥。
安装
在 SourceWeft 中
- 打开 控制台中的 DB Explorer,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
DB Explorer MCP
A read-only Model Context Protocol (MCP) server for PostgreSQL, MySQL, and SQLite that lets an AI coding assistant explore, query, and audit a relational database without ever being able to write to it.
Point your MCP client at a database and ask questions in plain language. The client's LLM writes the SQL; this server parses it, refuses anything that is not a single read-only SELECT, executes it with a row cap, and returns structured results. Schema inspection, execution plans, index recommendations, and migration review come along with it.
The server makes no LLM API calls of its own, so there is no API key to configure — reasoning happens in whichever client you connect. Works with SQLite, PostgreSQL, and MySQL through SQLAlchemy.
Tested and evaluated. 336 tests run on every push and pull request, plus 26 more against real PostgreSQL and MySQL in CI (Tests). In an agent evaluation, Claude completed 14 of 14 realistic database tasks through these tools with no failed tool calls, an average of 2 calls per task, and a mean independent judge score of 4.6 / 5 (Agent evals).
Why
Giving an assistant raw database credentials means one confused or prompt-injected turn can drop a table. Handing it a read-only replica loses schema context and plan analysis. This server takes the middle path: full introspection and query power, with mutation made structurally impossible at the parser level rather than by asking the model to behave.
Architecture
The SQL arriving at safety.py is written by the client's model and therefore untrusted, so it is parsed rather than pattern-matched, and a rejected query never reaches the database.
Both modes run identical tool code — only MCP_TRANSPORT changes.
Quickstart
Requires Python 3.11+ and uv.
From PyPI
No clone needed. uvx downloads the package and runs it in its own environment:
Leave out --env to try it against the bundled demo database first. In any other client's config, use "command": "uvx" and "args": ["db-explorer-mcp-server"].
From source
On Windows, if uv is not on PATH, prefix with py -m (py -m uv sync).
You don't start the server yourself: your MCP client launches it over stdio, so go on to Connect a client. Run by hand, uv run server.py prints nothing and waits for a client on stdin; stop it with Ctrl+C.
The default database is sample.db. Point at your own with DATABASE_URL, either in the client config below or in a .env file copied from .env.example:
Percent-encode special characters in passwords (@ → %40, # → %23, / → %2F).
Connect a client
Claude Code — local
Then run /mcp in a session to confirm the 7 tools are listed. Add -s user to make it available in every project.
Claude Code — remote
Run /mcp → Authenticate for the OAuth flow; tokens are cached and refreshed automatically.
For a server you host yourself with a bearer token, send it as a header. Read it from an environment variable, so a .mcp.json committed for the team holds no secret:
Claude Desktop
Local, in claude_desktop_config.json:
To reach a remote deployment without a custom connector, proxy it over stdio:
VS Code
.vscode/mcp.json is checked in and starts the server over stdio through the Windows py launcher. On macOS or Linux, change its command to uv and its args to ["run", "server.py"].
MCP Inspector
Use transport Streamable HTTP with your /mcp URL, or stdio with uv run server.py. The Inspector shows raw tool responses and unparaphrased errors, which makes it the fastest way to tell a server problem from a client problem.
Python
Try it
Once connected to the demo sample.db from Quickstart, prompts like these work directly:
- "What tables exist, and which ones are missing primary keys?"
- "Show me 5 rows from
orderswith the highest total." - "Why is this query slow?
SELECT * FROM orders WHERE created_at >= '2026-01-01'" - "Which foreign keys in this database lack indexes? Give me the
CREATE INDEXstatements." - "Draft a migration adding a
shipped_atcolumn toorders, then validate the up and down scripts."
To watch the guardrails work, ask it to run DELETE FROM users. The call fails with Unsafe query blocked: Only SELECT queries are allowed. Got: DELETE and the database is untouched.
Agent skill
skill/db-explorer/SKILL.md is an optional agent skill that teaches the model to use these tools well: read the schema once before querying, compute totals in SQL rather than from returned rows, say when a result is truncated, and follow error hints instead of retrying. It guides the model only; the server must still be connected.
For Claude Code, copy the skill/db-explorer folder to ~/.claude/skills/db-explorer/ for every project, or to .claude/skills/db-explorer/ inside one project.
Tools
validate_schema reports four codes: missing_primary_key, unindexed_foreign_key, wide_table (50+ columns), and no_indexes. A foreign key counts as indexed when any index, the primary key, or a unique constraint starts with its columns; partial, GIN/GiST/BRIN and FULLTEXT/SPATIAL indexes do not count.
explore_schema is cheap by default: with no arguments it returns table names and column counts. Row counts arrive only with table_name (tables only, capped at 100,000 with row_count_capped: true). Narrow a large schema with name_pattern (order matches any name containing it, order_* is a glob), page with limit/offset, and use detail=true for the columns, keys and indexes of a whole page in one call. Views and materialized views are listed alongside tables, each with a kind.
Safety model
Every execute_query, explain_query, and suggest_index call routes through safety.py before touching the database. A query is rejected unless it satisfies all of:
- Single statement.
SELECT 1; DROP TABLE users→Exactly one SQL statement is required SELECTonly, determined from the parsed statement type rather than a string prefix →Only SELECT queries are allowed. Got: DELETE- No SQL comments.
--,/*,*/are refused outside string literals and quoted names, closing the comment-smuggling route.#is refused on MySQL and MariaDB only, where it starts a comment. - No blocked keywords anywhere in the token stream:
ALTER,CREATE,DELETE,DROP,EXEC,EXECUTE,GRANT,INSERT,INTO,REVOKE,TRUNCATE,UPDATE. Matched as whole tokens, soSELECT grant_date FROM permissionspasses. Four entries are load-bearing:INSERT,UPDATEandDELETEcan hide in a PostgreSQL data-modifying CTE whose statement type is stillSELECT, andSELECT * INTO archive FROM userscreates a table. The rest cannot appear in aSELECT-typed statement and stay as a second line of defense ifsqlparsetype detection regresses. - No locking clause.
FOR UPDATE,FOR NO KEY UPDATE,FOR SHARE,FOR KEY SHAREand MySQL'sLOCK IN SHARE MODEare refused, because a locking read blocks other writers. Matched as a clause, not a keyword, so a column namedsharestill works.
Every query that passes is wrapped as SELECT * FROM (<your query>) AS limited_query LIMIT <row_limit + 1>, so an unbounded scan cannot flood the client's context. A LIMIT in your own query narrows the inner result, but row_limit (clamped to 1000) still caps what comes back. If the extra probe row comes back it is dropped and the result carries "truncated": true, so a capped result is never mistaken for a complete one.
Validation is only the first of three layers, because a keyword blocklist cannot see a query that is syntactically fine and still harmful:
-
A statement timeout.
SELECT pg_sleep(600)passes every check above, so time is bounded independently of syntax:statement_timeouton PostgreSQL (set per transaction, so it works behind PgBouncer, Supabase and Neon poolers),max_execution_timeon MySQL, and a progress-handler deadline on SQLite. Configured byQUERY_TIMEOUT_SECONDS, applied in db.py. -
A read-only transaction. Reads run through
BEGIN READ ONLYon PostgreSQL,START TRANSACTION READ ONLYon MySQL, andPRAGMA query_onlyon SQLite. The database refuses the write itself, which is a guarantee the blocklist cannot make. -
Privileges. Still the outermost boundary — see .env.example. A
SELECT-only user is what stops server-side file reads likepg_read_file()that no keyword check reliably catches.Privileges are also where to restrict what can be read. A token reads whatever its group's database user can
SELECT, so give each group its own user (see Named tokens and groups). Hide credentials and PII with grants, not with a server-side table or column blocklist: a parser cannot see the columns behindSELECT *,SELECT u FROM users u, orrow_to_json(u), but the database enforces a grant on all of them.MySQL takes the same column list:
GRANT SELECT (id, name) ON app.users TO 'explorer'@'%';. A query that touches a revoked column then fails withsql_error(permission denied).
Every tool call writes one line to the db_explorer.audit logger on stderr: the tool, how it ended (ok or the error code), the caller (user=<token name> group=<group> over HTTP), and its arguments, capped at 2000 characters. Arguments are logged verbatim, so treat the log as being as sensitive as the data.
Errors
A tool error is only useful to a model if it says what to do next, so every failure carries a code and a corrective hint instead of a bare message:
Configuration
At most DB_POOL_SIZE + DB_MAX_OVERFLOW (15 by default) calls hold a connection at once; a call past that waits DB_POOL_TIMEOUT_SECONDS, then fails with server_busy. Keep the pool under the database's max_connections, counted across every server instance.
The SQLite fallback is for local development only: sample.db next to config.py, opened read-only. .env is loaded at startup (the file next to config.py first, then one in the working directory); real environment variables win over both.
Serve over HTTP
The same on macOS or Linux:
An HTTP endpoint publishes SELECT on the configured database to anyone who can reach the port, so the server fails to start without MCP_AUTH_TOKEN or MCP_AUTH_TOKENS rather than coming up unprotected. Clients send a token as Authorization: Bearer <token>; it is compared in constant time in auth.py. Set MCP_ALLOW_UNAUTHENTICATED=true to override on a genuinely trusted network — the server then warns on stderr at every startup.
A host that loads server.py:mcp directly (Horizon, fastmcp run) answers every HTTP request 401 unless a token or MCP_ALLOW_UNAUTHENTICATED=true is set.
On a loopback bind, foreign Host and Origin headers are refused, so a web page cannot reach a local server through DNS rebinding. The transport is stateless, so any replica behind a load balancer can answer any request. GET /health answers {"status": "ok"} without a token, for load-balancer probes; it never touches the database.
Named tokens and groups
Give each person or service its own token, and optionally a group whose database user decides what it can read:
Each entry is name:token or name:group:token. A token's name goes into every audit line; its group picks the database login the call runs on, so the database's own grants (and PostgreSQL row-level security) decide what that caller can read. A lone MCP_AUTH_TOKEN behaves as one token named default with no group.
The server refuses to start on a malformed entry, a repeated name or token, a token under 32 characters, a group with no DATABASE_URL_<GROUP>, or both MCP_AUTH_TOKEN and MCP_AUTH_TOKENS set. Tokens are compared in constant time and read at startup: restart after changing them. Each group has its own connection pool.
On PostgreSQL, explore_schema lists tables a group cannot SELECT (the catalog is readable by everyone), but their rows stay protected. DEPLOYMENT.md has the GRANT statements for both databases, OAuth on Horizon, and a VM setup with HTTPS.
Hosted Supabase note: direct connections (db.<ref>.supabase.co) are IPv6-only, which fails from IPv4-only containers with an empty-looking psycopg2.OperationalError. Use the pooler host from the dashboard's Connect panel, and note that the username becomes postgres.<project-ref>.
Tests
336 tests covering the safety layer, serialization, read-only enforcement and timeouts, HTTP auth and token groups, every tool, and error reporting. Each uses a temporary SQLite database, so the suite needs no credentials and no running server.
tests/test_live_databases.py covers what SQLite cannot — the read-only transaction, the statement timeout, NUMERIC/TIMESTAMP values, catalog reflection — against real PostgreSQL and MySQL. Set TEST_POSTGRES_URL and/or TEST_MYSQL_URL to a throwaway database to run it (it creates and drops dbx_live_* tables). CI runs both.
Agent evals
The tests check each tool; the evals check whether a model uses the tools well. evals/run.py runs each task in evals/scenarios.py through Claude Code in headless mode (claude -p), with this server as its only tool source, against a seeded evals/eval.db. Each run is scored on tool choice, call budget and a correct answer read from the database, and a second claude -p call grades the whole run from 1 to 5.
It needs the claude CLI on PATH, logged in; runs count toward that login's usage limits, and no API key is involved. Results vary between runs, so the evals are not part of CI. Reports are written to evals/results/.
Latest results
Claude Sonnet 5.5 as agent and judge, 2026-10-14. Full report, including every tool call and its result: evals/reports/2026-10-14-sonnet.json.
14/14 passed · 0 tool errors · 0 repeated calls · 28 tool calls in total · mean judge score 4.64 / 5
Every answer that has a single right value was checked against the database, not just judged. Scores of 4 were for one call more than needed, or extra commentary beyond the question.
Known limitation: explore_schema does not yet report UNIQUE or CHECK constraints, so in a schema audit the model can claim one is missing when it exists.
Project layout
Design notes and limits
- Query-mode
suggest_indexflags full table scans, not missing indexes in general. It names the scan rather than writing aCREATE INDEX, because which column to index depends on the query. A scan on a tiny table is expected, not a problem. - Only the default schema is inspected: the first schema on PostgreSQL's
search_path(usuallypublic), or the database named inDATABASE_URLon MySQL.execute_querystill readsbilling.invoiceswhen the SQL names the schema. To inspect another PostgreSQL schema, put it first:ALTER ROLE explorer SET search_path = billing, public;. - A few read-only shapes are refused.
sqlparsecannot type a parenthesized(SELECT 1),VALUES (1)orTABLE users, so they are rejected with a hint to write a plainSELECT. - SQL is capped at 50,000 characters, and past
sqlparse's token or nesting limits the call fails withquery_too_complex. - The row cap guards context, not cost. A heavy aggregate still runs in full;
QUERY_TIMEOUT_SECONDSbounds that. - Long text and large results are cut. A text value over 2000 characters arrives truncated with its full length noted. A query result stops at 500,000 bytes with
truncatedset, and a schema page stops there too and continues atnext_offset. - MariaDB is not tested. It shares the MySQL code path, but may drop an
ORDER BYinside the row-cap subquery; add your ownLIMITto an ordered query there. - Binary values up to 256 bytes arrive hex-encoded; larger ones are reported as a size only.
- Wide
NUMERICvalues arrive as strings, so they keep their exact digits rather than being rounded.
License
MIT. See LICENSE.
来源:README.md,提交 6c08909
工具
0版本历史
1- v0.1.0最新Oct 20, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分