关于 Postgres (read-only)
Postgres (read-only) 是面向分析和 Agent 数据库探索的 Postgres MCP 服务。它通过多层机制限制写入,并使用可编辑的上下文文件记录团队特有的架构说明和业务定义。
主要功能
- 01只读 SQL 查询
- 02多层写入防护
- 03表与架构发现工具
- 04可编辑的共享上下文文件
- 05Agent 添加表说明
- 06限制查询结果和执行时长
适用场景
- 01探索数据库表以开展分析
- 02在不授予写权限时查询数据
- 03向 Agent 提供业务定义
- 04记录未文档化的表或列含义
MCP 相关
该 MCP 服务提供只读查询、架构发现及表上下文工具,并将表架构作为 MCP 资源提供。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
4收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| sql | string | 是 | 单条只读 SQL 语句。 |
详细介绍
@contextflo/postgres-mcp
适用于 Postgres 的分析型 MCP 服务。默认只读,并配有一个由你和 Agent 共同编写的上下文文件,让模型给出符合团队习惯的回答。
由 Contextflo 构建并维护。
可直接替代已归档的 @modelcontextprotocol/server-postgres。该服务器曾存在 SQL 注入漏洞,攻击者可以让 COMMIT; DROP SCHEMA public CASCADE 直接绕过只读事务。
观看 100 秒演示:设置、Agent 发现 events 表并非表面看起来的那样后将其记录为备注,以及服务器拒绝执行的 DROP。
npx @contextflo/postgres-mcp postgresql://localhost/mydb
为什么选择它
真正可靠的只读保护。 已归档的服务器通过 SQL 字符串 属性强制执行只读。此处的只读保护由线协议、连接、Postgres 解析器和数据库角色共同保障:四个独立层级各自都能拦截该攻击载荷。本仓库将此漏洞利用作为测试用例。
给出靠谱的答案。 如果模型不知道 fct_orders_v2 才是团队实际使用的表,或者 revenue 指的是毛收入而非净收入,它就会写出自信却错误的 SQL。.contextflo/context.md 是一个由你编辑的 Markdown 文件,此服务器会将其交给模型,背后没有其他依赖,只有这个文件。Agent 也会向其中添加内容:当它发现架构中未提及的信息时,add_table_context 会追加一条备注,你可以像审查其他更改一样通过 diff 检查。
设置
1. 创建只读角色。 以数据库所有者身份,在 psql 或提供商的 SQL 编辑器中运行。使用你的数据库名称和真实密码,并为 Agent 应查看的每个架构重复相应的架构行:
CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;
这样,只读保护就由数据库本身保障,而不仅仅依赖此服务器的代码。
2. 将该角色的连接字符串放入项目文件夹中的 .env:
DATABASE_URL='postgresql://mcp_readonly:[email protected]:5432/mydb'
请保留引号:托管服务提供商会添加 ?sslmode=require&...,其中的 & 需要用引号括起来。服务器会从其启动目录中的 .env 读取 DATABASE_URL,因此连接字符串不会出现在命令行中。
如果可以,请将其指向只读副本或分支,而不是主库。只读保护会阻止写入,但不会限制负载:Agent 探索数据时可能会执行全表扫描或开销很大的联接,这会在主库上与应用争抢资源。Neon 和 Supabase 只需几次点击就能创建数据库分支或副本,RDS、Cloud SQL 和大多数其他托管服务也提供只读副本。语句超时(默认 30 秒,可通过 --statement-timeout 配置)和行数上限会限制单次查询的运行时长和返回数据量,但不会降低查询成本。
3. 生成上下文文件:
npx @contextflo/postgres-mcp init
此命令会根据你的 COMMENT ON 值生成 .contextflo/context.md。请编辑此文件:业务定义部分最有价值。
4. 将其添加到客户端。 Claude Code 会在项目文件夹中启动服务,因此会自动找到 .env 和上下文文件:
claude mcp add postgres --scope project -- npx -y @contextflo/postgres-mcp
--scope project 会将 .mcp.json 写入当前文件夹,而不是写入全局配置。
Cursor、Claude Desktop、VS Code
其他客户端可能会在项目文件夹之外启动服务(Claude Desktop 会在 / 中启动),因此需要明确提供连接字符串和上下文文件:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@contextflo/postgres-mcp", "--context-file", "/path/to/project/.contextflo/context.md"],
"env": { "DATABASE_URL": "postgresql://mcp_readonly:[email protected]:5432/mydb" }
}
}
}
Cursor 使用 .cursor/mcp.json。Claude Desktop 使用 claude_desktop_config.json(macOS:
~/Library/Application Support/Claude/,Windows:%APPDATA%\Claude\)。VS Code 使用 .vscode/mcp.json,并以 servers 替代 mcpServers。
| 工具 | 功能 |
|---|---|
query | 执行一条只读语句:SELECT、WITH ... SELECT、EXPLAIN 或 SHOW。 |
list_tables | 列出可读取的表及其说明。pattern 会匹配名称或说明中的任意位置。 |
get_table_context | 描述表:列、类型、键、外键目标、枚举值和精选说明。 |
add_table_context | 让 Agent 将发现的注意事项(例如,amount 的单位是分,status 有一个未记录的值)写入上下文文件。 |
这里没有单独的搜索工具,这是有意为之。information_schema 和 pg_catalog 都是普通表,因此,更具体的操作,例如查找所有名称包含 %revenue% 的列,或列出没有主键的表,都可以由模型自行编写查询:
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE column_name ILIKE '%revenue%';
表架构也会作为 postgres://<host>/<table>/schema 资源公开,与已归档的服务保持一致,供固定使用这些 URI 的场景使用。大多数客户端不会自行获取资源,因此发现功能由工具提供。
如何强制只读
共四层。每一层都足以单独阻止已归档服务中的漏洞。
1. 扩展查询协议。 用户 SQL 会通过 pg-cursor 执行,它始终发送 Parse/Bind/Execute,因此 Postgres 本身会拒绝多语句输入。已归档的服务调用 client.query(sql) 并传入纯字符串;node-postgres 只有在存在绑定值时才会准备语句,因此这会走 simple 协议路径,其中 ; 用于分隔语句。这就是整个漏洞。
2. 连接级只读。 启动数据包中会设置 default_transaction_read_only=on,并且每条语句都会在显式的 BEGIN READ ONLY 中运行,结束时始终执行 ROLLBACK,绝不执行 COMMIT。回滚还会撤销事务中执行的任何 SET,因此语句无法削弱连接池中的连接,影响下一个获取该连接的人。
3. 基于真实 Postgres 解析器的语句允许列表。 libpg-query 是编译为 WASM 的真正 Postgres C 解析器,而不是对 SQL 的 JavaScript 近似实现。它会遍历整个解析树,而不只是顶层节点,因此能捕获会修改数据的 CTE:
WITH x AS (INSERT INTO users VALUES (1) RETURNING *) SELECT * FROM x
它会被解析为 SelectStmt。只检查语句类型的验证器会执行它。遇到未知节点类型时则默认拒绝。
函数会根据数据库自身的目录进行检查。Postgres 会将每个函数标记为 immutable、stable 或 volatile,只有 volatile 函数可能产生副作用,因此除非 volatile 函数位于一份简短的无害函数列表中(分析所需,如 random()、clock_timestamp()、表大小函数),否则都会被拒绝。这能涵盖 dblink、pg_logical_emit_message(即使在只读事务中也会写入 WAL)、咨询锁、统计信息重置,以及未来 Postgres 新增的功能,无需逐一列举。SECURITY DEFINER 函数无论被标记为何种属性都会被拒绝,因为这类函数会以其所有者的权限运行。此外,还会检查一份固定的已知绕过方式列表。
4. 只读数据库角色。 上述各层都是代码,而代码总会有漏洞。Postgres 会强制要求角色无法写入,无论其他情况如何,这也是为什么设置的第一步是创建这样的角色。如果你以超级用户身份连接,服务会在启动时发出警告;如果 init 连接时所用的角色可以写入,它会打印角色配置片段。
这不能防范的情况。 函数检查会查看查询直接调用的函数,并信任每个函数的属性标记。声明为 STABLE、却仍然写入数据的用户自定义函数会被允许;通过间接方式调用的函数也会被允许,例如位于视图中、隐藏在运算符后,或通过类型转换调用。只读事务仍会拒绝以这些方式更改表数据的操作;可能漏过的是较少见的副作用,例如向 WAL 写入消息。第 4 层负责阻止这类情况,因此推荐使用只读角色,而不是把它当作可选项。只读也不等于保密:连接角色能读取的任何内容,模型都能读取,因此只授予 Agent 你希望其查看的内容。
从 @modelcontextprotocol/server-postgres 迁移
替换软件包名称即可。工具仍名为 query,仍接收 sql,仍返回 JSON 行,连接字符串仍是第一个参数。
有四项刻意的差异:
- 拒绝多语句 SQL 和
SET/RESET,并给出明确错误。在已归档的服务中,这些操作“能用”,而这正是漏洞所在。 - 结果默认限制为 1000 行和 50,000 个字符,超过 2,000 个字符的单个值会缩短。输出会注明截断,不会静默处理。返回的每行各占一行,而非美化打印,因此令牌开销大约减半;结果仍是 JSON 数组。日期和时间戳与 Postgres 返回的完全一致,不会按服务的时区重新渲染。
- 架构发现是一个工具,而不只是资源。 大多数客户端不会自动附加资源,因此使用旧服务的模型常常不知道架构。
- 所有非系统架构都可见,而不只是
public,并且会提供COMMENT ON中的列说明。
选项
--max-rows <n> Maximum rows returned per query (default: 1000)
--max-output-chars <n> Character budget for one query result (default: 50000)
--statement-timeout <ms> Server-side statement timeout (default: 30000)
--context-file <path> Curated schema context (default: .contextflo/context.md)
--no-context-writes Do not offer add_table_context; the context file is only read
--log-file <path> Query audit log (default: .contextflo/log.md once that directory exists)
--no-log Never write a query log
--http Serve over streamable HTTP instead of stdio
--port <n> HTTP port (default: 8080)
--host <addr> HTTP bind address (default: 127.0.0.1)
如果不传入连接字符串,DATABASE_URL 会从环境变量或当前目录中的 .env 提供连接字符串。服务会在首次使用时连接:即使没有连接字符串,它也会启动并列出其工具,每次工具调用都会说明缺少什么。使用 --http 时,AUTH_TOKEN 会要求请求携带该值作为 bearer token。
连接池代理。 PgBouncer(以及 Supabase、Neon 等提供的连接池连接字符串)会拒绝此服务通常发送的启动参数。发生这种情况时,服务会在不带这些参数的情况下重新连接,并在 stderr 中说明。安全性不会降低:每条语句仍会在 BEGIN READ ONLY 中执行,并设置独立的语句超时;CI 也会通过事务模式下的 PgBouncer 运行只读测试套件。
上下文文件
# Database context
Revenue means gross, before refunds.
"Active customer" means an order in the last 90 days.
## Tables
### public.orders
One row per customer order. Source of truth for revenue, not `orders_legacy`.
- revenue_usd: Gross revenue, before refunds. Net lives in `order_refunds`.
- status: One of pending, paid, refunded.
## Tables 以上的所有内容都会交给模型:既作为服务指令,也会再次出现在 list_tables 的顶部,因为有些客户端从不向模型显示服务指令。HTML 注释会被排除。在 ## Tables 下,### 标题用于命名表,其下的文字用于描述表,- column: meaning 行用于描述列(也可以用破折号分隔)。你写的内容优先于 COMMENT ON。编辑会在下一次工具调用时生效;无需重启。
相对路径的 --context-file 会相对于客户端启动服务的目录解析。Claude Desktop 会在 / 中启动服务,因此在那里会退而使用你的主目录(~/.contextflo/context.md)。如果文件位于代码仓库中,请传入绝对路径。服务启动时会打印正在使用的路径。
Agent 会补充文件内容。 当模型发现架构未说明的信息(amount 的单位是分,旧记录中的 status 也可能是 'void',每个查询都需要 deleted_at IS NULL)时,add_table_context 会将其追加到此文件中,让之后的每个会话都能了解这些信息。备注只会追加,绝不会替换你写的内容,而且仅针对实际存在的表和列。请像对待其他变更一样处理这些备注:将文件保存在 git 中并查看差异。模型可能会出错,而且数据中的文本可能会引导它写入内容,因此在有人检查之前,备注都只是建议。--no-context-writes 可关闭此工具。
init 会根据现有注释生成初始内容,并且只处理已有注释的列。为所有 4000 列都放入空白占位符的文件,没人会去编辑。
contextflo.com 可根据你的代码和文档生成更丰富的上下文,并在团队中共享。
查询日志
.contextflo/ 存在后,每条语句都会连同执行结果、行数和耗时追加到 .contextflo/log.md。此日志供你查看,不会反馈给模型。--no-log 可关闭此功能。
Docker
docker run -i --rm ghcr.io/contextflo/postgres-mcp postgresql://host.docker.internal/mydb
如需远程端点,请参阅 docker-compose.example.yml。绑定到 0.0.0.0 前,请先了解自己在做什么:这会在一个端口上建立与实时数据库的连接。请设置 AUTH_TOKEN,将其限制在私有网络内,并在其前面终止 TLS,否则令牌会以明文形式通过网络传输。服务器在未设置令牌却暴露时会打印警告。
使用默认的环回绑定时,Host 或 Origin 非本地的请求会被拒绝。这样可以防止网页利用 DNS 重绑定,通过浏览器访问 127.0.0.1 上的服务。
开发
npm install
npm run build
npm test
npm run db:up # throwaway Postgres for the integration suite (needs Docker)
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:55432/postgres npm test
npm run db:down
集成测试套件会刻意绕过解析器,直接驱动数据库,因此它验证的是网络传输层和事务层,而不是验证器。未设置 TEST_DATABASE_URL 时会跳过,并且必须在 CI 中运行。
安全
请私下报告漏洞。请参阅 SECURITY.md。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。