mcpField
P
PostgreSQL (hardened, read-only)

PostgreSQL (hardened, read-only)

本地服务
github.com数据库32 周前更新

关于 PostgreSQL (hardened, read-only)

这是一个面向 AI Agent 的 Rust PostgreSQL MCP 服务,通过 SQL AST 校验、只读事务和函数拒绝列表限制写入,并提供查询成本控制、OAuth 2.1 与审计日志。

主要功能

  1. 01SQL AST 校验与只读事务
  2. 02查询超时与成本限制
  3. 03OAuth 2.1 身份验证
  4. 04防篡改审计日志
  5. 05表结构与键作为 MCP 资源

适用场景

  1. 01允许 AI Agent 在只读权限下查询 PostgreSQL
  2. 02在执行前拒绝多语句及写入查询
  3. 03为数据库访问添加认证和审计
  4. 04向客户端提供表结构信息

MCP 相关

它通过 stdio 和 Streamable HTTP 向 MCP 客户端提供 PostgreSQL 查询及表结构资源,并支持多种 MCP 协议修订版本。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"eszetael-postgres-mcp-hardened": {
"command": "npx",
"args": [
"-y",
"postgres-mcp-hardened"
],
"env": {
"DATABASE_URL": "<DATABASE_URL>",
"MCP_STATEMENT_TIMEOUT": "<MCP_STATEMENT_TIMEOUT>",
"MCP_ALLOW_TABLES": "<MCP_ALLOW_TABLES>",
"MCP_AUDIT_LOG": "<MCP_AUDIT_LOG>",
"MCP_ADDR": "<MCP_ADDR>"
}
}
}
}
启动命令npx -y postgres-mcp-hardened
需要配置的环境变量
DATABASE_URL密钥Connection string for the role the server connects as. Use a role that cannot write — the server refuses writes twice, but a read-only role is the layer that does not depend on us being correct. `--print-setup-sql` prints the SQL that creates one.
MCP_STATEMENT_TIMEOUTServer-side statement timeout, e.g. `5s`. A question that would pin the database is cancelled by PostgreSQL, not by hope.
MCP_ALLOW_TABLESComma-separated allowlist. A table off the list is refused by name, and hiding it inside a CTE, a view or a join does not help.
MCP_AUDIT_LOGPath to the tamper-evident audit log. Entries are chained by hash and survive a restart; `--verify-audit` checks the chain against an off-host anchor.
MCP_ADDRAddress to bind, default 127.0.0.1:8080.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmpostgres-mcp-hardened0.1.10
ocighcr.io/eszetael/postgres-mcp-hardened:0.1.10

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

postgres-mcp-hardened

🚧 版本 0.1.10 —— 外部审查的后续发现,以及一个矫枉过正的修复

已发布:面向五个平台的二进制文件,附带校验和、Sigstore 签名和构建来源证明; .mcpb 安装包,可一键安装;ghcr.io 上面向 amd64 和 arm64 的镜像;npm 上的软件包; 以及官方 MCP 注册表中的条目。

0.1.8 和 0.1.9 修复了四位独立审查人员在 0.1.7 中发现的六个绕过漏洞,我们自己一个都没发现。 前一天,我们花了两天进行自我对抗性测试,结果大多一切正常。通过你以为该写的测试,并不等于真正检查过。最重要的那个漏洞完全不需要任何权限:当某列被屏蔽时,通过 USING 对该列进行联接,就能判断给定值是否存在;对于本项目建议配置的最小权限读取者来说,这就构成了完整的相等性预言机。

0.1.10 修复了剩余的三个问题。 设置 MCP_ALLOW_SCHEMAS=public 时,SELECT * FROM secret.salaries 会被拒绝,但 describe_table 却会泄露同一张表的所有列、类型和默认值。普通连接字符串若未携带 sslmode,会采用驱动程序的 prefer 模式;只要服务器拒绝 TLS,所有数据就会以明文传输,而线路上的任何人都能让服务器拒绝 TLS。MCP_SSLROOTCERT 还会在 242 个公共证书颁发机构之外再添加一个私有证书颁发机构,而不是用它替换这些机构;因此,自以为已将信任固定到自有颁发者的运维人员,实际上并没有做到这一点。

第一个版本的 TLS 修复有误,值得一读。 它判断“是否为环回地址”,并要求其他所有连接使用 TLS,结果一次提交就让六个 PostgreSQL 版本任务、一轮一致性测试、容器检查和对抗性测试集全部失败。这些测试都像普通部署那样连接数据库:postgres://user:pass@postgres:5432/db,即私有网络上的服务名称。Docker Compose 和 Kubernetes 不是公共互联网;如果服务器要求桥接网络中的容器使用 TLS,人们就会干脆把服务器关掉。现在要判断的是是否有不可信任的人能够接入网络链路,而不是地址是否为环回地址;测试套件中也加入了一个通过真实套接字连接 PostgreSQL 的测试,该 PostgreSQL 只能通过服务名称访问,因为有关网络的规则就应该通过网络进行测试。

THREAT_MODEL.md 中记录了一项资源限制,但尚未解决:49 字节的 SQL 就能让 PostgreSQL 在规划期间将一个常量展开为 5.9 GB 的后端内存,而五秒的 statement_timeout 无法阻止它。常见形式会被拒绝;这个问题普遍存在,其根源超出了本服务所能处理的范围。

这里的每项修复都先在运行中的服务器上复现,再予以修复;每项修复都连同查询记录在 CHANGELOG.md 中。

本项目目前所有内容都处于 0.1.x 阶段,因为项目外还没有人用自己的数据运行过它。

官方 Postgres MCP 服务已于 2024 年弃用,但每月仍有 467k 次下载。它的全部防护仅限于数据库级只读事务——而这本身无法阻止所有写入操作。这是一个持续维护、采用纵深防御的 Rust 替代方案。

一个可直接替换的 Model Context Protocol 服务,可让 AI Agent 查询 PostgreSQL——在数据库层面强制只读,并提供真实的 SQL 验证、超时、成本限制、OAuth 2.1 和审计跟踪。支持 Streamable HTTP 和标准输入/输出,并协商 MCP 修订版本:2026-07-28(当前版本,自上游于 2026-08-03 发布后即为默认版本)、2025-11-25 和 2025-06-18——目前大多数已发布的客户端仍在使用这些版本。客户端会请求自己了解的版本;不会协商降级。

来试着突破它——一条命令,无需数据库

只读防护有离线模式。给它一条语句,它就会告诉你它的判断:无需数据库、无需配置,也无需永久安装任何内容。

npx postgres-mcp-hardened --validate "/* comment */ DROP TABLE users"
# REJECT: non-read-only statement: Drop

npx postgres-mcp-hardened --validate "SELECT 1; DROP TABLE users"
# REJECT: multiple statements are forbidden

npx postgres-mcp-hardened --validate "WITH d AS (DELETE FROM t RETURNING *) SELECT * FROM d"
# REJECT: non-read-only statement: non-read-only query (CTE / SELECT INTO / FOR UPDATE)

npx postgres-mcp-hardened --validate "SELECT * FROM orders WHERE id = 1"
# ALLOW

如果某条会写入数据的语句返回了 ALLOW,那就是任何人都能发给我们的最有价值的内容。 不需要可用的漏洞利用代码,也不需要详细报告——一行 SQL,再加上“这不应该被允许”,就是一份完整的报告。任何绕过防护的情况都应通过 SECURITY.md 报告;其他情况都是普通问题,提交问题的标准是我觉得这不对劲,而不是我确定有问题。

模糊测试器是确定性的,并会打印种子,因此它发现的任何问题都能在一台从未见过你的机器上复现——一百万次变异大约需要一分钟:

npx postgres-mcp-hardened --fuzz 1000000
# fuzz: 1000000 iterations, seed 1592594996, slowest validation 8 ms
# RESULT: 0 invariant violations

要针对真实数据库测试完整流程,运行 docker compose -f examples/docker-compose.yml up -d,即可启动带有示例数据的 PostgreSQL,以及连接到它的服务;连接所用的角色只有 SELECT 权限,没有其他权限。

迄今发现的所有绕过方式都记录在 src/validate.rs 的 MUST_REJECT 测试集里,每次提交都会运行,并保留其代价记录,而不是被整理掉。你发现的问题也会加入其中。

为什么

@modelcontextprotocol/server-postgres 已在 npm 上弃用(最后一次发布于 2024 年 12 月),但在截至 2026 年 8 月 9 日的 30 天内仍有 475,790 次下载。该肯定的功劳还是要肯定:它的方法并不草率——它会用 BEGIN TRANSACTION READ ONLY 包裹每条查询,并始终执行 ROLLBACK。这是一种真正有效的防护,本服务现在也采用了它。

问题在于,这是唯一的防护,而且并不完善:

  • 只读事务不会阻止所有写入,回滚也无法撤销它放行的所有操作。 这是两个独立的事实,第二个才是关键。

    gin_clean_pending_list() 可以在 SET TRANSACTION READ ONLY 中运行,其操作会在回滚后依然生效:一个有 25 个待处理页面的索引,在事务回滚后会变为 0。pg_backup_start() 会使会话进入备份状态,即使执行 DISCARD ALL 也无法清除;默认情况下,fast => false 会等待分散检查点,同时强制开启 full_page_writes,这会给繁忙的服务器带来实际开销。pg_import_system_collations() 同样可以执行,且不会引发 SQLSTATE 25006,但要谨慎看待它的影响:它会被回滚撤销,因此对于始终执行回滚的服务来说,它只是个有趣的现象,而非绕过方式。

    可以复现,但请先阅读两个前提条件,否则你会看到结果为 0 或出现错误,然后认定我们是在编造。上述三个函数都需要超级用户权限或对象所有权。导入操作只会恢复缺失的排序规则,因此必须先移除某个排序规则:

    -- as superuser, and note these are three separate transactions: a statement that errors
    -- inside a block aborts the whole block, so they cannot be run as one.
    DELETE FROM pg_collation WHERE oid IN (SELECT oid FROM pg_collation ORDER BY oid DESC LIMIT 200);
    
    BEGIN READ ONLY;
      DELETE FROM pg_collation WHERE collname LIKE 'zu%';  -- ERROR: cannot execute DELETE ...
    ROLLBACK;
    
    BEGIN READ ONLY;
      SELECT pg_import_system_collations('pg_catalog');    -- 200, no error
    COMMIT;                                                -- and now the rows are there
    

这些都是写入操作,也都发生在只读事务中,但其中一个会被拒绝,另一个却不会。这种不对称性,正是本服务不把事务当作唯一防护的原因。也正因如此,任何这些因素都不如角色重要:以上示例都需要最小权限读者所不具备的权限;当被指定的角色拥有写入权限时,本服务会拒绝以网络监听模式启动。它无法控制的是,有人会把哪一个连接字符串粘贴到客户端配置中,而通常他们粘贴的就是自己原本在用的那个。

  • 没有语句超时、成本限制或行数限制——单条查询可以一直运行,直到服务器放弃。
  • 没有身份验证、审计记录,也无法处理通过返回的行数据进行的提示词注入。
  • 一个包含 143 行的源文件,自 2024 年 12 月以来无人维护,也没有测试套件。

本服务保留回滚机制,在其前面增加 AST 验证,并补上原项目从未具备的运维保障层。

postgres-mcp-hardened 与已归档的原始版本对比

已归档的 server-postgrespostgres-mcp-hardened
只读强制机制BEGIN TRANSACTION READ ONLY + ROLLBACK——只有一层保护,而且 PostgreSQL 允许部分写入操作通过这一层AST 验证(sqlparser) 加上 相同的只读事务和回滚,再加上 针对会绕过该机制执行写入的函数的拒绝列表
通过 CTE 执行多语句 / DROP请求会到达数据库,且只有事务能阻止它由解析器拒绝,不会到达数据库
语句超时无强制执行 statement_timeout + idle_in_transaction_session_timeout
失控 / 高成本查询不受限制地运行EXPLAIN 成本保护机制会在执行前拒绝这类查询
通过行数据进行提示词注入原始输出包装为 trusted="false" + 转义分隔符
错误消息泄露架构信息(relation X does not exist)结构化、不泄露信息且可据此采取操作
身份验证无OAuth 2.1(RS256 JWT、范围 + 受众 + 签发者)
审计无防篡改哈希链日志
作为 MCP 资源的架构✅✅ — 另含注释、主键和外键
测试 / CI无针对实时 PostgreSQL 的单元测试 + 端到端测试套件、确定性模糊测试框架、由官方 MCP SDK 驱动的一致性测试,以及每次推送时执行 clippy + cargo audit + 容器构建
传输方式stdio / 已弃用的 SSEStreamable HTTP + stdio
是否维护❌ 自 2024 年起已弃用✅

安装

五种接入方式,按大多数人偏好的顺序排列。

一键安装,适用于支持 .mcpb 捆绑包的客户端:从最新版本下载 postgres-mcp-hardened-<your-platform>.mcpb 并打开。捆绑包会要求提供连接字符串,并将其存储在操作系统密钥链中,而不是明文配置文件中。无需安装,也无需编辑任何内容。

通过 npm 安装——步骤最少,也是 MCP 客户端配置可以直接指向的方式。运行时不会用到 Node:这个包是一个启动器,会获取适用于你平台的原生二进制文件,并在运行前验证其校验和。

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "postgres-mcp-hardened", "--stdio"],
      "env": { "DATABASE_URL": "postgres://readonly_user:PASSWORD@localhost:5432/mydb" }
    }
  }
}

连接字符串应放在 env 中,而不是 args 中,这是有意为之:在共享机器上,参数会出现在 ps 输出和 shell 历史记录中,而数据库密码不应该出现在那里。

从发布页面获取二进制文件——只有一个文件,无需持续更新;如果你的机器上完全没有 Node,这是可选的方式。(它不是静态二进制文件,尽管本页面在 0.1.7 之前一直这么说:-gnu 和 macOS 目标平台会像其他原生程序一样链接系统 C 库。除此之外无需安装任何东西。)每个版本都提供适用于 Linux、macOS 和 Windows 的 x86-64 和 arm64 构建,每个构建都附带校验和与签名;下一节将介绍如何验证它们。

作为容器运行,如果你平时就是这样运行程序的。该镜像采用 distroless 设计,并以非 root 用户运行;二进制文件和镜像都由同一组签名覆盖。

docker run --rm -p 127.0.0.1:8080:8080 --memory=512m \
  -e DATABASE_URL="postgres://readonly_user:PASSWORD@db-host:5432/mydb" \
  -e MCP_ADDR=0.0.0.0:8080 \
  -e MCP_BEARER_TOKEN="$(openssl rand -hex 32)" \
  ghcr.io/eszetael/postgres-mcp-hardened:latest

--memory 并非摆设。服务空闲时占用 7.7 MB,普通请求只需个位数 MB,但调用方可以写入 SELECT repeat('x', 100000000),将峰值内存推高到 400 MB——这并非由结果导致,结果始终不超过 300 字节;问题出在成本限制器自己的 EXPLAIN 上,PostgreSQL 会在规划时将折叠后的常量填入其中。这是 THREAT_MODEL.md 中列出的残余风险,并记录了尝试过的三种修复方案及其各自造成的问题。在此问题解决之前,内存限制是有效的防护措施,因此请设置它:此处使用 --memory,systemd 下使用 MemoryMax=。

MCP_ADDR 必须绑定到 0.0.0.0,而不能绑定到 127.0.0.1,否则服务会监听一个仅存在于容器内部的接口,发布的端口将无法响应。另一个容易踩的坑是:DATABASE_URL 中的 localhost 指的是容器,而不是你的机器;因此,运行在主机上的 PostgreSQL 需要使用 host.docker.internal(Docker Desktop)或主机在网桥上的地址(Linux 上默认为 172.17.0.1)。写入本文前,我们已针对发布的镜像端到端验证了这两种情况,包括读取操作会返回行,而 DROP TABLE 会返回 -32602 non-read-only statement: Drop。

从源代码构建——在克隆的仓库中运行 cargo build --release。不要运行 cargo install:这个 crate 不在 crates.io 上;会失败的说明比缺少说明更糟糕。

检查下载的文件

每个发布的二进制文件都使用 Sigstore 无密钥签名进行签名——我们没有可能丢失的私钥,证书会注明生成该文件的工作流、仓库和标签。每个文件旁边都会附带一个 .sig 和一个 .pem 文件:

F=postgres-mcp-hardened-x86_64-unknown-linux-gnu.tar.gz
cosign verify-blob "$F" --bundle "$F.bundle" \
  --certificate-identity-regexp '^https://github.com/Eszetael/postgres-mcp-hardened/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

固定身份,而不只是签名。若不指定 --certificate-identity-regexp 和 --certificate-oidc-issuer,检查结果只会回答“有人签署了这个文件”,但这并不是我们要问的问题。经过验证的证书会注明构建该文件的工作流、仓库和标签——你可以通过 base64 -d "$F.pem" | openssl x509 -noout -text 读取证书(cosign 写入的证书采用 base64 编码,这会让那些直接尝试对其运行 openssl 的人感到意外)。

较早版本的 cosign 尚不支持 --bundle;它们使用单独的 .sig 和 .pem 文件,这两个文件会一并发布,并通过 --signature "$F.sig" --certificate "$F.pem" 指定。当前版本的 cosign 已将这些参数标记为弃用,因此建议使用捆绑文件。

公开发布版本还附带 SLSA 构建来源证明,可使用 gh attestation verify <file> --repo Eszetael/postgres-mcp-hardened 进行验证。

在 Claude Desktop / Cursor 中使用(stdio)
{
  "mcpServers": {
    "postgres": {
      "command": "postgres-mcp-hardened",
      "args": ["--stdio"],
      "env": { "DATABASE_URL": "postgres://readonly_user:YOUR_PASSWORD@localhost:5432/mydb" }
    }
  }
}
或将其作为远程服务运行(Streamable HTTP)
DATABASE_URL="postgres://readonly_user:YOUR_PASSWORD@host:5432/mydb" \
MCP_ADDR="0.0.0.0:8080" \
postgres-mcp-hardened
# POST /mcp   ·   GET /health   ·   GET /ready   ·   GET /metrics

TLS:只要 PostgreSQL 服务支持,加密连接始终会启用;同时接受 sslmode=require、verify-ca 和 verify-full(始终验证证书链 和 主机名,因此 require 的行为与 verify-full 相同)——因此,托管的 Postgres(RDS、Supabase、Neon、Render) 开箱即用。证书和主机名始终会经过验证——已验证(接受标准:“若证书指向其他主机,则会按名称拒绝”);对于私有 CA,请将 MCP_SSLROOTCERT 指向 PEM 文件包。没有“信任所有内容”的开关。

提示:将 DATABASE_URL 指向权限最小化的只读角色。服务本身会强制执行只读限制,但限定范围的数据库角色还能提供纵深防御。

或在容器平台上运行(Apify Standby)

无需更改代码,即可将此服务作为 Apify Actor 以 Standby 模式运行。它会从 ACTOR_WEB_SERVER_PORT 读取平台分配的端口,并绑定到 0.0.0.0——该端口的优先级高于 MCP_ADDR,并会在标准错误流中明确提示,因为绑定到其他地址会导致运行始终无法标记为就绪,故障看起来就像是神秘的超时。GET / 会响应平台的就绪探测(x-apify-container-server-readiness-probe),且不会访问数据库:容器就绪不等于数据库就绪,如果探测因连接池繁忙而等待,就会让缓慢的数据库导致容器永远无法启动。

端点方法用途
/mcpPOSTMCP 端点(Streamable HTTP)。DELETE 会结束会话。
/GET就绪探测;否则显示指向真实端点的说明
/healthGET进程仍在运行
/readyGET进程和数据库连接均可用
/metricsGET计数器(需要 MCP_METRICS_TOKEN)
/.well-known/mcp/server-card.jsonGET注册表读取的信息:修订版本、传输方式、工具

输入是 POST 请求体中的 JSON-RPC 请求——initialize、tools/list、tools/call、resources/list、resources/read、server/discover。输出是 JSON-RPC 响应;从 2025-11-25 起,被拒绝的语句会作为工具执行错误返回(isError: true),并在内容中说明原因,以便模型重写查询。tools/list 是每个参数的权威说明。

此处的身份验证由平台负责,而非我们负责。 Apify 会在将请求路由到容器之前检查调用方的令牌,因此服务不会额外要求 MCP_BEARER_TOKEN——如果要求提供第二个密钥,找到此服务的 Agent 就无法调用它。此豁免范围有限:必须同时设置 APIFY_IS_AT_HOME 和 ACTOR_WEB_SERVER_PORT,仅设置其中一个不会产生任何影响;此时服务卡会报告 "type": "apify-platform",而不会声称存在我们并未持有的锁。其他情况下,服务仍会拒绝在没有身份验证的网络地址上启动。如果你想在同一扇门上再加一道锁,也可以设置 MCP_BEARER_TOKEN。

另一项启动门槛保持不变,而且在此场景中更为重要:具有写入权限的角色不得启动网络监听器。请将 DATABASE_URL 指向只读角色——--print-setup-sql 会写入语句。

从已弃用的服务迁移

针对 @modelcontextprotocol/server-postgres 报告最多的问题,也在此服务上进行了复现;以下是每个问题在此处的表现:

用户报告的问题此处的表现
两个实例(生产环境 + 开发环境)无法区分,客户端会选中其中一个资源 URI 中包含数据库名称(postgres:///mydb/public/orders/schema),而 MCP_SERVER_LABEL 会在客户端 UI 中显示实例名称
每个实例只能连接一个数据库,因为连接字符串是命令行参数MCP_DATABASE_URLS="prod=…;dev=…" 可让一个服务连接多个数据库;每个工具都接受可选的 database,资源也涵盖所有数据库
no pg_hba.conf entry … SSL off错误会指出服务要求 TLS,并说明修复方法(?sslmode=require)
通过注入 COMMIT / END 绕过只读限制会被拒绝——多语句检查在解析器之前基于令牌进行,单独的 COMMIT 也会被拒绝为写操作
spawn npx ENOENT、Node 版本问题只需一个原生二进制文件;无需 Node、npx 或 node_modules
连接 RDS 时无限期挂起,没有输出或错误有超时限制:无法访问的主机会在约 8 s 内返回原因,不会悄无声息
self-signed certificate in certificate chain将 MCP_SSLROOTCERT 指向 CA 证书包;错误会指出该变量
连接字符串只能作为命令行参数传入可使用 DATABASE_URL 或位置参数——原有调用方式仍然有效
密码中含特殊字符时出现 INVALID_URL错误会说明哪些字符需要进行百分号编码,以及如何编码
分区子表涌入表列表和资源列表默认隐藏;设置 MCP_SHOW_PARTITIONS=1 即可显示
-32601 Method not found、Unexpected end of JSON input已实现 ping 和资源;多行 JSON 会先缓冲,直到内容完整;批处理请求会被拒绝并返回清晰错误,而不是毫无响应
没有行数限制——一次查询就能塞满上下文自动添加 LIMIT、设置 8 MB 字节上限,并明确标记 truncated

测试

除了单元测试外,仓库还包含两个测试工具,它们会在 CI 中针对每次变更运行:

  • --fuzz — 一个确定性模糊测试器,会对已知写操作语料应用不改变 SQL 含义的变换(注释、大小写、美元引用、不可见 Unicode 字符、括号),并断言这些语句绝不会变成允许执行的语句。
  • tests/acceptance.sh — 一个端到端测试套件,会启动自己的 PostgreSQL,并检查 317 项行为:针对已弃用服务报告的每种写操作绕过方式(包括注入 COMMIT / END)、结果是否真实、模式内省、协议一致性、配置错误是否明确报错、审计篡改检测、负载下的公平使用,以及多数据库部署。

对所有已报告问题逐一作答

docs/COMMUNITY_ISSUES.md 是完整的问题记录:针对已弃用服务报告的每个问题,以及针对仍在维护的替代方案提出的每个未解决问题,都列出了此处的处理方式——包括少数无法通过代码修复的问题,并如实说明。

我们从其他方案中学到了什么

这个领域中的每个服务都有问题跟踪器,而这些跟踪器记录了问题所在。 我们有意针对以下问题进行了改进:

  • 发布的镜像落后于代码。 这是针对领先替代方案支持度最高的公开投诉。我们的容器使用生成二进制文件的同一标签构建并推送,因此不会出现版本脱节。
  • 查询超时写死。 这也是他们最受欢迎的功能请求之一。MCP_STATEMENT_TIMEOUT 可配置,并会在启动时验证。
  • 默认不限制访问。 有些服务默认允许读写,并依赖运维人员加以限制。而本服务根本没有写入路径。
  • 客户端配置中包含凭据。 MCP_PASSWORD_FILE 可让密码不出现在配置中。
  • 非默认架构中的表会被静默地视为不存在。 MCP_SEARCH_PATH 可修复查找问题,而且工具也可以显式指定 schema。
  • 传输方式已弃用。 HTTP+SSE 于 2025-03-26 被 Streamable HTTP 取代,至今已过去三个修订版本(本页面在 0.1.7 之前一直写着 2025-06-18,比实际晚了一个修订版本;规范自身的 2025-03-26 更新日志记录了这项替换)。我们使用的是当前传输方式。

故障排查

以下是人们在已弃用的服务上实际提出的问题及其解答,让大家不必为了查找答案而提交问题。

spawn npx ENOENT /“需要哪个 Node 版本?”——都不需要。这是一个原生二进制文件,无需另外安装运行时。从发布页面下载,并让客户端指向该文件即可。没有 node_modules,没有 npx,也没有需要保持更新的东西。

“服务已启动,但没有端口在监听。”——这是 stdio 模式,对 Claude Desktop 和 Cursor 来说是正常的:客户端通过进程的标准输入和输出进行通信,而不是通过套接字。如果需要网络端点,请在不带 --stdio 的情况下启动;此时它会打印 MCP HTTP listening on http://…,并使用 Streamable HTTP。

“另一台机器上的客户端能访问数据库吗?”——可以:在数据库旁边以 HTTP 模式运行服务,公开该服务,并启用 OAuth(JWT_PUBKEY_PEM、JWT_AUD、JWT_ISS)。这样,数据库凭据就不会离开服务运行所在的主机。

“无法连接到 MCP 服务。”——进程在握手前就已退出。请在终端中运行相同的命令:配置错误会打印原因并以状态码 2 退出,而不是悄无声息地终止;连接问题则会在首次查询时报告并说明原因。

self-signed certificate in certificate chain / unable to verify the first certificate——你的服务提供商使用私有 CA(Supabase、GCP 和 RDS 都是如此)。请下载其 CA 捆绑包,并将 MCP_SSLROOTCERT 设置为该文件。错误消息会指出对应服务提供商的处理步骤。我们不提供“信任所有内容”的开关。

托管服务提供商

此服务始终验证数据库证书,包括使用 sslmode=require 时。这是有意偏离 libpq 的做法:在 libpq 中,require 只加密而不验证,因此中间人可以读取并篡改每条查询和结果,却不会被察觉。严格验证的代价是,使用私有 CA 的服务提供商需要多做一步;放宽验证的代价则是,你永远不会知道自己是否遭到攻击。如果你不同意这种权衡,使用下面的证书包配置 verify-full,工作量相同,也能彻底消除疑虑。

服务提供商预期情况
Supabase私有 CA。在 Dashboard → Project Settings → Database → SSL configuration 中下载证书,然后将 MCP_SSLROOTCERT 指向该证书。直连主机(db.<ref>.supabase.co)仅支持 IPv6——在 IPv4 网络中使用 Supavisor 连接池字符串(端口 6543),它也适用于无服务器环境和短连接。
Amazon RDS / Aurora私有 CA:https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem。支持 IAM 身份验证——将生成的令牌填入密码字段,并记住它会在 15 分钟后过期。
Google Cloud SQL私有 CA:Connections → Security → server-ca.pem。通过 Cloud SQL Auth Proxy 连接时,连接到本地主机上的代理即可,TLS 由代理负责。
Azure Database for PostgreSQL公共 CA——无需下载。Azure 在 2026 年第 1 季度将根证书轮换为 DigiCert Global Root G2;我们随附 Mozilla 根证书库,因此此次轮换无需你进行任何操作。
Neon公共 CA(ISRG Root X1,Let's Encrypt)——无需下载。连接池端点和直连端点均可使用。
DigitalOcean私有 CA:从集群的 Overview 页面下载证书。

不确定自己属于哪种情况?在进行任何配置之前,先询问服务本身:

echo | openssl s_client -starttls postgres -connect YOUR_HOST:5432 2>/dev/null \
  | openssl x509 -noout -issuer

如果颁发者是知名机构(DigiCert、ISRG、Google Trust Services),说明可以直接使用;如果名称中包含你的服务提供商,则需要使用其证书包。

使用事务模式的连接池(Supavisor、PgBouncer)时,请注意,此服务会为每个会话设置 statement_timeout 和 idle_in_transaction_session_timeout,并在显式只读事务中运行每条查询。这两项都兼容事务池化;在事务之外进行会话级 SET 则不兼容,因此我们不会这么做。

no pg_hba.conf entry … no encryption——服务器只接受该主机和用户的 TLS 连接。在连接字符串中添加 ?sslmode=require。

INVALID_URL / invalid connection string——密码中包含 @、:、/、# 或 ? 时,必须进行百分号编码(@ → %40,: → %3A,/ → %2F,# → %23)。

“我的表有数百个分区,列表无法使用。”——默认会隐藏分区子表,只列出父表。如需显示分区子表,请设置 MCP_SHOW_PARTITIONS=1。

“我需要同时连接生产环境和预发布环境。”——可以为每个数据库分别运行一个服务(通过设置 MCP_SERVER_LABEL 区分),也可以使用 MCP_DATABASE_URLS 在一个服务中配置两者,并在工具参数中传入 database。

资源

每个表和视图都作为 MCP 资源(postgres:///<schema>/<table>/schema)公开,因此客户端无需执行查询即可浏览架构——这与已弃用的服务端提供的能力相同,此外负载中还包含列注释、主键和外键。

数据库尚未响应时,resources/list 会返回空列表,并在 _meta 中说明原因,而不是返回协议错误。检查此服务端的目录会在完全没有数据库的情况下启动它,并在 initialize 后立即调用 resources/list;如果对此返回错误,就像是在说服务端无法正常工作。空列表并不代表没有表——initialize 会说明数据库尚未响应,security_posture 会提供详细信息,原因也会随列表一同返回。数据库若确实已响应但拒绝了请求,仍然属于错误,因为对权限缺失却报告“没有资源”正是本服务端旨在避免的静默故障。 已验证(验收标准:“主机在没有数据库且前置 mcp-proxy 的情况下也能检查服务端”)

工具

  • explain_query — 执行计划;启用 analyze 后会运行语句并报告实际耗时和缓冲区使用情况。这里是安全的,因为语句经过验证,只读,并在始终会回滚的事务中运行。计划还附带 summary:指出哪个节点耗费了时间(自身耗时,不含子节点),以及规划器的行数估算与实际情况偏差最大的地方——因为估算错误通常就是计划不佳的原因。
  • database_health — 缓存命中率、连接数(此数据库和整个集群)、运行时间最长的语句和被遗弃时间最长的事务(分别统计)、清理积压、无效索引、接近上限的序列、复制延迟、从未分析过的表(没有规划器统计信息——这是数据库看似健康却运行缓慢的常见原因),以及计数器涵盖的时间范围。角色无权查看的内容会明确标示,而不会以确定的零值返回。
  • analyze_indexes — 未使用的索引、重复索引,以及因频繁进行顺序扫描而值得创建索引的表。
  • top_queries — 来自 pg_stat_statements 的开销最大的语句。
  • security_posture — 通过询问 PostgreSQL,而非凭空假设,确认此部署实际上能对数据库执行哪些操作:角色是否能写入、绕过行级安全策略或访问服务端文件;传输是否经过身份验证;审计链是否使用密钥;连接是否加密。返回一个等级——取最严重的问题,绝不取平均值;若有问题,还会提供修复命令。同一摘要也会通过 initialize 传达给模型,因为使用 stdio 时无人能看到 stderr,而 Agent 是操作员唯一能收到消息的途径。
  • query — 执行只读 SQL 查询(经过验证、自动添加 LIMIT、受成本限制)。响应会说明执行情况:returnedRows、appliedLimit、truncated;如果较大的请求被限制为最多 10000 行,还会包含 requestedLimit;分页时包含 offset;配置了遮蔽时包含 redactedColumns——因此 Agent 无需猜测是否收到了完整结果。
  • list_schemas、list_tables、describe_table — 逐步发现架构(参数化,可防注入)。describe_table 会返回架构注释(COMMENT ON TABLE/COLUMN)、主键、外键和默认值,让 Agent 能了解列的含义及其指向,而不是根据列名猜测;表不存在时会报错,而不会返回空列列表。

协议修订版本

如果服务端实现了客户端请求的修订版本,就会在 initialize 中返回该版本;否则返回最新版本。通过 HTTP 发送的请求会从 MCP-Protocol-Version 标头中读取修订版本,每个请求各自协商——一个客户端的协商结果不会改变另一个客户端所使用的协议。

如果请求没有携带标头,服务端不会直接退回到最旧的协议。它会读取本会话在 initialize 时协商的修订版本,这正是传输规范的要求:默认版本仅适用于“如果服务端未收到 MCP-Protocol-Version 标头,且没有其他方式识别版本——例如,依赖初始化期间协商的协议版本”。会话就是这种其他方式,因此,协商了 2025-11-25 的客户端即使随后省略标头,也会继续使用协商好的协议,而不会被悄然降级。

无法解析的标头与缺少标头是两回事,规范对此有明确规定:“如果服务端收到的请求包含无效或不受支持的 MCP-Protocol-Version,它必须以 400 Bad Request 响应。”我们未实现的版本——2025-03-26 或 not-a-date——会被拒绝,并返回我们支持的修订版本列表,而不是按客户端从未同意的协议处理。只有未提供版本时才会回退,存在分歧时不会回退。

只有请求既没有标头,也没有会话时才会回退;此时会回退到 2025-06-18——这是本服务端实现的最旧修订版本——而不是规范指定的 2025-03-26。这里并未实现那个版本,按服务端无法遵守的协议作答,比按它能遵守的最旧协议作答更糟。

拒绝请求时,关键区别在于拒绝如何传达。在 2025-06-18 下,“此语句不是只读的”会作为 JSON-RPC 错误返回:客户端看到的是一次失败调用,而模型通常根本看不到原因。从 2025-11-25(SEP-1303)开始,它会作为工具执行错误返回——isError: true,原因包含在内容中——因此模型会改写查询,而不是把失败直接交给用户。不变的是审计记录:拒绝请求的代码会记录拒绝信息,验收套件也会同时断言这两点,因此,更友好的错误永远不会悄然变成更少的日志记录。

Mcp-Method 和 Mcp-Name 的要求是内容一致,而非必须存在。草案要求提供它们;早期修订版本并未要求,强行要求会破坏所有现有客户端。但如果网关依据 Mcp-Method 进行路由或授权,而服务端却执行请求正文,那么它做出决定的请求与实际执行的请求就不一致——无论当前采用哪个修订版本,都是如此。因此,无论采用哪个修订版本,只要标头存在,就必须与请求正文相符;未发送这些标头的客户端则不受影响。

协议错误仍然是协议错误。格式错误的信封、未知方法或缺少令牌,都不是模型通过改写 SQL 就能修复的问题;客户端的错误处理也依赖这些错误一如既往地出现。

发布前的下一版

2026-07-28 是 MCP 迄今最大的一次破坏性变更:没有 initialize、没有会话标头,也没有 ping。这个标识符来自草案模式中的 LATEST_PROTOCOL_VERSION,它不是发布日期——MCP 会以最近一次引入不兼容变更的日期为版本命名,因此它描述的是草案的历史,而非发布时间表。每个请求都会在 _meta 中携带自己的协议版本,而新的 server/discover 则取代了握手。我们提前实现了这一版,并用开关控制,因为草案会不断变化,而公开宣称支持一个不断变化的目标,最终必然出错。上游于 2026-08-03 发布了 schema/2026-07-28——发布的模式与我们验证过的草案相比,只有四个文档 URL 不同,其他内容完全一致——因此我们移除了开关,这就是服务默认使用的协议版本。对于使用 2025-11-25 和 2025-06-18 的客户端,响应方式与之前相同。

无论使用哪个版本,server/discover 都会响应,因为规范希望客户端用它来探测向后兼容性——而这只有在旧服务也会响应时才行。我们的响应会列出支持的版本,并在 _meta 中提供完整的安全态势。这是刻意为之:客户端可以在发送查询之前,得知自己连接的是以超级用户身份连接的服务;这些信息以结构化数据形式呈现,而不是依赖模型从文字中自行察觉。

草案中的两条规则在这里是安全控制措施,而非形式要求。Mcp-Method 和 Mcp-Name 必须与请求正文一致;若不一致,我们会拒绝请求(-32020)——这些标头的存在,是为了让网关无需解析正文就能路由并授权;如果标头与正文可能不一致,那么负责授权的组件和负责执行的组件看到的就是两个不同的请求。而客户端声明的版本如果我们并未实现,也会收到明确告知(-32022),而不是在它从未同意的协议约定下悄悄提供服务。

安全措施的代价

测量结果,而非断言:tests/bench/,使用 pg 驱动在同一台机器上运行相同查询(Docker 中的 PostgreSQL 18.6、5 万行表、300 个顺序请求、关闭速率限制)。2026-08-17 在一台平均负载为 2.6 的共享 VPS 上重新测量——两次运行的中位数:

查询驱动本服务差值
单点查询0.43 ms5.9 ms+5.5 ms
小范围扫描1.0 ms8.3 ms+7.3 ms
聚合查询4.7 ms11.3 ms+6.6 ms

本页早先的一张表写的是 +3.6/+5.2/+3.7,并称「约 4 ms」。这些数据是在更空闲的机器上测得的,驱动的基准耗时也随之变化——同样的单点查询,之前是 0.28 ms,而现在是 0.43 ms——所以这是硬件的影响,不是代码造成的。更改数据前,我们检查了两件事,因为最明显的怀疑对象是我们自己的构建:在启用 lto 之前构建的 0.1.6 二进制文件,在这台机器上同一小时内测得的差值为 +5.4/+7.8/+5.9。两者完全一致。发布配置文件将二进制文件大小减半,但在这里没有带来任何性能损失。

预计每次查询会增加 5 到 8 ms,并将本页上的任何单个数值视为某台机器在某一天的一次测量结果。比数值大小更重要的是变化趋势:开销几乎恒定。如果开销来自 AST 验证,它应该会随查询复杂度增长,但事实并非如此。耗时来自多次往返——重置会话、设置超时和只读标志、开启只读事务、由成本防护机制规划语句,然后执行查询并回滚事务。驱动只需一次交互,而这里需要五到六次。

这是刻意的取舍,它带来的好处也一目了然。对于会发出几十次调用的 Agent 来说,这种开销可以忽略;如果你要将它放在对延迟极其敏感的服务路径前面,那就是用错了工具,而且它也不是为此设计的。

在并发场景下,值得关注的不是吞吐量,而是触及限制后会发生什么:有 8 个并发客户端时,它处理了每秒 373 个请求,并拒绝了另外 192 个请求,提示「in-flight 请求过多」;这说明并发请求上限正在发挥作用,而不是让队列不断增长,直到系统崩溃。

无需创建索引,也能回答:这个索引有帮助吗?

领先的替代方案真正以一项功能著称,那就是索引调优:它能在你创建索引之前,告诉你创建索引是否值得。它默认使用能够创建真实索引的连接来实现这一点——只有在你记得限制权限时才安全。

simulate_index 通过一个无法写入任何内容的连接,回答同一个问题。 hypopg 会在后端内存中注册一个假设索引:规划器能看到它,存储层看不到它;调用返回后,它就消失了。你可以获得有无该索引时的执行计划和成本;此外,还能单独得知规划器是否实际采用了它,因为成本几乎没有变化和规划器忽略了索引,是两种不同的答案。

该工具接收一个表和一个列名列表。不是一条 CREATE INDEX 语句。 索引定义由服务端根据系统目录确认存在的标识符组装,并由 PostgreSQL 自行引用,因此工具参数无法转化为任意 DDL——包含 SQL 的列名会在系统目录查询时被拒绝,而且有一项测试专门验证这一点。这些数字是规划器的估算值:大幅改善是测试该索引的理由,而非证明。

由其他人的客户端检查符合性

此处其他所有测试都是我们的测试框架与我们的服务端交互。如果我们误读了规范,那么双方都会以同样的方式误读,一切测试都会通过。因此,CI 还会通过 stdio 和 Streamable HTTP,使用官方 MCP SDK——生态系统所用的客户端库——驱动服务端:握手、工具列表和架构、读取、作为工具执行错误而非协议错误返回的写入拒绝、资源列表和读取。协议错误会表现为客户端无法与我们通信。tests/conformance/。

开发

git config core.hooksPath .githooks   # once, per clone

.githooks/pre-push 会在任何内容离开你的机器之前运行格式检查、clippy、单元测试和文档声明检查。设置它是因为发生过这样一件事:一次提交带着未通过的 clippy 检查发出去了,而大家直到收到失败邮件才知道。请注意,cargo test 在这段代码上会通过——clippy 检查不是编译器错误——所以“本地能编译”并不等于“CI 会通过”。

它会有意跳过验收测试套件和 PostgreSQL 矩阵:这些需要 Docker,而且大约要花二十分钟;如果钩子耗时长到无法接受,人们就会绕过它。CI 会运行所有内容。想故意看看 CI 失败时,可以运行 git push --no-verify。

设置角色

DATABASE_URL=postgres://admin@host/mydb postgres-mcp-hardened \
  --print-setup-sql --role mcp_reader --schemas public --redact ssn,email > setup.sql
# read it, then:
psql -v pw="$(openssl rand -base64 24)" -f setup.sql mydb

使用连接字符串运行时,表和列列表会从系统目录中读取;不提供连接字符串时,你会得到带占位符的同一份文档。这一区别在数据脱敏方面尤为重要:要授予回读权限的列必须从数据库中读取,因为凭记忆填写列名,正是把本该隐藏的列交还出去的原因。

输出末尾包含一些检查;操作成功时,这些检查不会返回任何行。此外还有提醒:只要将服务端连接到数据库,它就会告知你该角色可以执行哪些操作。

限制服务可访问的内容

MCP_ALLOW_SCHEMAS 和 MCP_ALLOW_TABLES 会限制查询可以访问哪些关系。设置其中任意一个即可启用允许列表;schema.* 和 schema.table 两种格式均可使用。

MCP_ALLOW_TABLES='public.customers,public.orders,analytics.*'

检查针对的是查询计划,而不是 SQL。这就是整个设计的关键:规划器已经应用了 search_path、解析了所有别名、将视图展开为基表,并且知道名为 customers 的 CTE 并不是 customers 表。因此,WITH customers AS (SELECT 1) SELECT * FROM customers 可以执行且不会访问任何内容,而 WITH x AS (SELECT * FROM salaries) SELECT * FROM x 会被拒绝。读取语句本身的做法在三轮对抗性审查中都未能过关。

启用前,有两点值得了解:

  • 分区依附于父表。 允许访问 events 后,由 PostgreSQL 决定读取哪些子表。
  • 视图的基表也必须加入允许列表,因为计划中列出的是这些基表。将它们都加入允许列表,并通过数据库权限确保无法直接访问基表——无论如何,这才是真正的边界。

除非设置 MCP_ALLOW_CATALOG=1,否则 pg_catalog 和 information_schema 不在可访问范围内:如果 Agent 仍能读取系统目录,就可以准确枚举出允许列表本想隐藏的内容。架构工具仍可正常工作,因为它们执行的是固定查询,而不是调用方提供的 SQL。

这是通往同一事实的三条路径,因为有一段时间它只涵盖其中一条。目录视图会被规划为扫描实际关系,计划中也会列出这些关系;但 pg_settings 会被规划为对 pg_show_all_settings 的单个 Function Scan,其中没有列出任何关系;而 current_setting() 是标量调用,不会作为扫描出现。过去,即使启用了允许列表,这两者也都能返回服务配置。现在,名称以 pg_ 开头的函数,以及 current_setting、inet_server_addr 和 inet_server_port,都会与目录关系一同被拒绝;设置 MCP_ALLOW_CATALOG=1 即可同时开放所有这些内容。普通的返回集合函数——generate_series、jsonb_each、unnest、regexp_split_to_table——不带此前缀,因此不受影响。current_user、session_user、current_database 和 version 则仍可读取,这是有意为之:Agent 已经知道自己连接到了什么,以及以谁的身份连接。

成功绕过控制的案例集

审查期间绕过控制的每一种情况,都记录在 tests/adversarial/ 中,并注明了在哪一轮修复。每次构建都会运行这些测试;由于案例针对的是占位符而不是我们的测试数据,你也可以将其用于自己的数据库:

ADV_URL='postgres://…' \
  ADV_TABLE=people ADV_TABLE2=orders ADV_REDACT_COL=ssn \
  ./tests/adversarial/run.sh

ADV_TABLE 需要包含敏感列,ADV_TABLE2 可以是任何其他可读取的表,ADV_REDACT_COL 是要脱敏的列。这三项都很重要:这个示例直到 0.1.7 才补上 ADV_TABLE2,因此它一直使用默认值 film——Pagila 示例数据库中的一张表——而严格按照说明操作会产生三个“匹配失败”,实际原因却只是缺少关系。现在,测试框架会预先检查这三项并指出错误项,因为如果安全测试集把拼写错误报告成控制失效,就会让人忽视真正重要的失败。

如果一项安全声明只能通过阅读我们的源代码来验证,那就只能选择相信它;而这个项目自身的历史恰恰说明了这种做法的问题。

安全模型

关于此服务能保证什么、不能保证什么,以及每项承诺由哪项控制来落实,完整说明见 THREAT_MODEL.md——其中也包括在审查中被绕过、因此被视为纵深防御而非安全边界的控制措施。

  • **加密传输:**通过 rustls 使用 TLS 连接 PostgreSQL(镜像中不包含 OpenSSL),始终启用证书验证,可通过 MCP_SSLROOTCERT 使用私有 CA。

  • **两种方式实现只读:**使用 sqlparser 解析每条语句,除非语句是 SELECT/WITH/EXPLAIN/SHOW,否则予以拒绝;此外,还会将数据库会话设置为 default_transaction_read_only = on。

  • **防止拒绝服务:**强制执行 statement_timeout,自动注入 LIMIT,并通过基于 EXPLAIN 的成本防护在运行前拒绝开销过高的计划。

  • 敏感列——纵深防御,并如实说明其局限:MCP_REDACT_COLUMNS 会在每一层屏蔽值,并拒绝执行引用这些列的查询,包括对抗性评审小组实际发现的绕过方式——重命名(SELECT password AS pw)、包装(md5(password))、序列化整行(row_to_json(t)、t::text、json_agg(t))、将列名作为字符串而非标识符(to_jsonb(t) ->> 'password'、#>> '{password}'、$.password)、整行通配符(ROW(t.*)::text)以及按位置重命名((SELECT * FROM staff) AS x(c1, …, c9))。这仍是基于名称的过滤;基于名称的过滤无法成为覆盖整个 SQL 语言的防护边界——四轮对抗性测试都通过某种此前未列出的形式绕过了它。

    在 0.1.7 中,第四轮没有找到新的名称形式,而是完全绕过了名称:SELECT get_raw_page('people', 0) 会返回表在磁盘上存储的 8192 字节,其中包含所有值,包括被屏蔽的值。这不是理论推测,而是实际演示:设置 MCP_REDACT_COLUMNS=ssn 后,SELECT ssn FROM people 被拒绝,但返回的原始页中却以纯 ASCII 文本包含了社会安全号码。现在,pageinspect 及其相关扩展会作为单独类别被拒绝,因为“返回的是存储内容,而不是列”与“执行写入”是不同的问题;明确告知操作员触发的是哪种情况,值得单独给出提示。

    同一轮还发现了更隐蔽的情况。PostgreSQL 的规划器会保存每列真实值的样本,并通过 pg_stats 公开这些样本:在 3,000 行数据中,SELECT * FROM pg_stats WHERE tablename='people' 返回了 {123-45-6789,555-00-1111,987-65-4321},而 SELECT ssn FROM people 则被拒绝。该查询根本没有提到被屏蔽的列,因此基于名称的规则无从处理。现在,无论你配置了什么值,凡是配置了任何屏蔽项,包含值的统计信息列——most_common_vals、histogram_bounds、most_common_elems、stavalues1…stavalues5——都会一并屏蔽。视图中的其他内容不受影响:n_distinct、null_frac 和 correlation 是下文索引建议的依据,不包含任何值;删除整个关系就会为了处理四列而破坏十列。

    另外还有两扇门,最终通向同一个地方。对于长到无法存进其所在行的值,PostgreSQL 会将其存入 TOAST 表,而该表可以通过名称读取:SELECT chunk_data FROM pg_toast.pg_toast_16384 返回了未加密的被屏蔽文本。pg_largeobject 对大对象也做了同样的事——它存储的是 lo_get 底层的字节内容。现在两者都会被拒绝,并作为关系处理,而非函数;提示信息也会说明原因:它们保存的是物理存储内容,而不是列。仅用于描述数据库的目录则不受影响,包括 pg_tables、pg_stat_activity、pg_largeobject_metadata,以及索引建议所需的统计信息列。 这四种情况说的都是同一件事,而且比任何补丁清单都更准确地描述了此功能的限制: 列过滤器保护的是列,因此任何绕过列读取数据的方式都不在它的承诺范围内。 原始页面、规划器采样、TOAST 块和大对象字节,是进入这个范围的四扇门;只需寻找模式而非名称,一个下午就能在同一个地方发现这四种情况。 坦诚的假设是,肯定还有更多,因此真正的边界是数据库角色和只读事务,而这仍然只是它所说的那样:纵深防御。 因此,服务不再妄加断言,而是询问数据库:启动时,它会报告所有已连接角色仍能读取经过脱敏的列的表,并给出修复所需的确切语句;设置 MCP_REDACT_REQUIRE_REVOKE=1 则会让它报告后拒绝运行。请注意,修复方法是先执行表级 REVOKE,再对保留的列执行 GRANT——如果角色对整张表持有 SELECT 权限,那么单独执行 REVOKE SELECT (password) ON staff 会悄无声息地不起作用。设置列级授权后,PostgreSQL 会拒绝对该表执行 SELECT *,因此调用方需要显式指定列;describe_table 会列出这些列,并标记经过脱敏的列。

  • **可识别提示词注入:**行数据会放在带有 trusted="false" 的来源信息块中返回,并对分隔符进行转义,因此恶意单元格无法劫持 Agent。

  • 它会生成你应该使用的角色:--print-setup-sql 会输出一个角色的 DDL:该角色不继承任何权限、不能绕过限制、不能创建对象,只能读取你指定的关系;并且在你指定了敏感列时,会按实际有效的顺序撤销这些列的权限。它只会打印,绝不执行。应用这些语句需要管理员权限,而一个全部职能都围绕“只读”的工具,绝不应该持有管理员密码。

  • **它不会暴露具有写入权限的角色:**当监听地址可从网络访问时,服务会向 PostgreSQL 查询已连接角色实际获准执行的操作——超级用户权限、BYPASSRLS、pg_write_all_data 等角色的成员身份,以及有限抽样的一些表上的写入权限——如果答案不只是“读取”,就会拒绝启动,列出每项原因并指向 --print-setup-sql。出于同样的原因,它也会拒绝未认证的网络监听。回环地址和 stdio 不受影响:在这些情况下,调用方就是操作员。覆盖设置(MCP_ALLOW_EXCESSIVE_ROLE、MCP_ALLOW_ANONYMOUS_NETWORK)必须使用字面值 i-accept-the-risk,以免因拼写错误而意外启用,并且启用情况会记录在审计日志中。本服务会自行强制只读,但这种强制依赖代码,而代码过去也出过错;使用无法写入的角色,才是我们任何 Bug 都无法绕过的保障。

  • **浏览器无法访问它:**除非操作员列出了某个 Origin,否则携带该 Origin 的请求会被拒绝并返回 403;在回环监听器上,非 localhost 的 Host 也会被拒绝——DNS 重绑定攻击正是以这种方式瞄准你笔记本上的数据库服务器。

  • **审计记录包含配置信息:**日志链以一条 startup 记录开头,其中列出版本、传输方式和所有生效设置;连接密码会被剥除,机密会被缩减为指纹,并附带一个操作员可在重启时固定校验的 config_fp。日志若只记录发生了什么,却不记录当时使用了哪些设置,就无法回答事件调查时首先会问的问题。

  • **审计会发现日志被截短:**哈希链可以证明条目未被篡改,但如果日志末尾被截掉,链内部仍然自洽——重新计算也查不出问题。因此,除了 MCP_AUDIT_LOG,服务还会维护 <log>.hwm,其中用一行记录最后写入的序列号和哈希,且只会在条目持久化追加后更新。启动时会对比两者,并报告不一致的情况:末尾条目缺失、最后一条记录被改写,或日志整个消失。这并不能证明发生了篡改——非正常关机也会造成相同现象——但具有篡改迹象的记录链应该让你提出疑问,而不是直接下结论。归档或移动日志时,请将旁路文件一并保留;删除它只会失去截短检测功能,不会丢失任何条目。离线验证器保持不变,仍然需要外部锚点: --verify-audit <file> --expect-last <hash>。 已验证(验收标准:“缩短的日志会在启动时被发现,且无需任何外部锚点”)

  • **错误设置会导致致命错误,而不只是产生错误结果:**无法解析的监听地址、无法写入的审计文件、连接另一台机器上的数据库时使用 sslmode=disable、与数据库凭据相同的指标令牌、拼写为 yes 的布尔值——这些设置过去都会被接受,随后悄悄执行与预期不同的操作。现在启动会停止,并指出具体设置。

  • **设置拼写错误会导致致命错误,但其他程序的设置不会:**过去,MCP_REDACT_COLUMN(单数)会让服务启动时悄悄关闭脱敏,因此与真实设置相近的拼写错误现在仍会阻止启动,并指出正确拼写。看起来与我们定义的设置都不像的名称,可能是共用环境变量的其他程序所设:服务会报告并忽略它。各目录在服务器前都放置了用于检查服务器的 mcp-proxy,它会导出 MCP_PROXY_DEBUG——直到 0.1.6 版本,这个变量都会导致本服务在读取请求前退出。MCP_X_* 仍保留供操作员自行使用。已验证(验收标准:“拼写错误仍会导致致命错误”)

  • **不泄露架构信息:**数据库错误会被映射为结构化且可操作的消息,绝不会回显表名或列名。

  • **OAuth 2.1:**可选的 RS256 Bearer 令牌验证(签名、exp、aud、iss),并强制执行作用域;未配置时会禁用,适用于本地使用或自托管。

  • **审计:**每项工具决策都会记录为具有篡改迹象、由哈希链连接的 JSON 行(不含原始 SQL)。

  • **供应链:**在 CI 中强制检查依赖项许可证、来源和安全公告(cargo deny、 cargo audit);每个发行版本都附带 CycloneDX SBOM。

  • **运行时:**以 distroless、非 root 容器形式发布——在 0.1.6 版本中,linux/amd64 的下载大小为 14.8 MB,磁盘占用为 41 MB,并在 CI 中构建和进行冒烟测试。两个数字都列出来,因为只列一个总是会显得更好看:docker images 显示的是第二个数字,而消耗你带宽的是第一个数字。

资源占用

在普通 VPS 上使用包含 1.6 万行的示例数据库测得,你可以亲自核验「使用 Rust 编写」这一说法,而不必盲信:

空闲时的常驻内存7.7 MB——分别启动 5 次的中位数,各次结果相差不超过 0.1 MB
处理 200 个请求后的常驻内存9.4 MB,之后保持平稳
请求延迟中位数约 8 ms——包括测量时启动的 curl 进程,因此服务本身的耗时更低
启动到首条经过验证的语句5 ms——5 次 --validate 运行的中位数,观察到的范围为 5 至 7 ms
二进制文件9.2 MB(linux x86_64,0.1.7 起)。无需额外安装任何东西——没有 Node、Python,也没有我们随附的共享库。它并非静态链接:与任何 -gnu 目标一样,它使用系统的 libc、libm 和 libgcc_s。
容器镜像压缩后 12.6 MB,解压后 31.8 MB(linux/amd64,0.1.7),distroless,以非 root 用户运行

直到 0.1.7,这里有两行数据都是错的。空闲内存原先写的是 5.2 MB,实测为 7.7 MB——在相同条件下启动 5 次,结果相差不超过 0.1 MB,因此旧数据并非测量噪声,而是另一种未说明测量方法的结果。二进制文件一行写着「11 MB,静态链接」,而用户实际下载的文件有 18.9 MB,且采用动态链接。我们从未在发布构建中测量过大小,因为这个 crate 根本没有 [profile.release],所以每个用户下载的版本都带有超过 5 MB 的调试符号。设置 strip、lto 和 codegen-units = 1 后,文件大小降至 9.2 MB。「静态链接」这个说法对我们发布的 5 个目标都不成立,它们没有一个是 musl 构建。镜像大小也随二进制文件一同缩小,从 0.1.6 的压缩后 14.8 MB 降至 12.6 MB——这两个数字都取自已发布镜像的注册表清单,而非本地构建,因为用户拉取的并不是本地构建。

对混合流量(读取、拒绝、错误、中止的请求、会话频繁更替、未认证的请求)进行 12 分钟压力测试,共处理了 51,499 个请求,结束时打开的文件描述符仍为 15 个,与开始时相同。常驻内存从 8.1 MB 增至 11.2 MB,值得关注的是变化趋势:前 400 个请求期间从 8.1 MB 增至 10.7 MB,之后的 51,000 个请求只增加了 0.5 MB,且增长曲线逐渐趋平。这是分配器进入稳定状态,而非内存泄漏。本页面过去写内存保持「平稳」,这对最初几秒之后的情况来说属实,但与实际数据不符,因此这里给出具体数字。

与其相信这段描述,不如运行 tests/soak.sh 复现测试。

配置

环境变量用途
DATABASE_URLPostgreSQL 连接字符串(使用只读角色)
MCP_ADDRHTTP 监听地址(默认 127.0.0.1:8080)
MCP_MAX_COST拒绝 EXPLAIN 成本超过此值的查询(默认 1,000,000)
JWT_PUBKEY_PEM、JWT_AUD、JWT_ISS启用 OAuth 2.1 令牌验证(省略则禁用身份验证);密钥可以是 PEM 文本,也可以是 PEM 文件的路径
MCP_AUDIT_LOG仅追加审计日志的路径(采用哈希链);使用 --verify-audit <file> [--expect-last <hash>] 验证。服务还会在其旁边写入 <log>.hwm——最后一个序列号和哈希,用于在启动时检测日志是否被截短
MCP_AUDIT_HMAC_KEY / MCP_AUDIT_HMAC_KEY_FILE将审计链转换为 HMAC-SHA256 的密钥——将其保存在主机外,以防日志被重写(文件末尾的换行符会被忽略)
MCP_AUDIT_HMAC_KEYS_OLD逗号分隔的旧密钥,以便在密钥轮换后仍能验证保留下来的日志。已验证(验收条件:“跨越密钥轮换的链可使用两个密钥验证”)
MCP_REDACT_COLUMNS不在结果中显示的列,例如 password, ssn, card_number——会在任意深度进行遮蔽,且若查询引用这些列则会被拒绝。纵深防御,不是安全边界:应配合 REVOKE SELECT (col) 使用
MCP_BEARER_TOKEN每个请求都必须提供的共享令牌,适用于没有身份提供方的部署。配置 OAuth 时会忽略此项——将其作为替代方式接受会让持有者获得完整权限范围,并使审计日志中没有身份信息
MCP_STATEMENT_TIMEOUT查询时间限制(PostgreSQL 间隔,默认 30s)
MCP_SEARCH_PATH表名未限定时要搜索的模式,例如 analytics, public
MCP_PASSWORD_FILE从文件读取数据库密码,而不是将其放入连接字符串
MCP_DATABASE_URLS通过一个服务连接多个数据库:prod=postgres://…;dev=postgres://…(此时工具会接收 database 参数)
MCP_SERVER_LABEL客户端界面中显示的名称,例如 production → postgres-mcp-hardened (production)
MCP_SHOW_PARTITIONS设为 1 时也列出分区子表(默认隐藏)
MCP_ALLOW_FUNCTIONS允许使用的目录函数(逗号分隔);我们无法确认这些函数是否只读
MCP_SSLROOTCERT用于与 PostgreSQL 建立 TLS 连接的 PEM CA 证书包路径(例如 AWS RDS 证书包);默认信任系统和 Mozilla 根证书
MCP_MAX_INFLIGHT_PER_CLIENT单个客户端的最大并发请求数(默认 4;0 表示禁用)
MCP_RATE_RPM每个客户端的请求速率限制(默认每分钟 120 次;0 表示禁用)
MCP_RATE_RPM_STDIOstdio 的相同限制(默认每分钟 600 次):Agent 合理地探索模式时每分钟会调用几十次,但 DBA 最担心的仍是失控循环不断请求生产数据库
MCP_CLIENT_IDstdio 模式下在审计日志中标识此客户端的名称,例如 claude-desktop@ada-laptop;未设置时,身份信息会回退为操作系统用户和进程
MCP_RATE_BURST该速率限制的突发请求额度(默认 MCP_RATE_RPM / 4,最小值为 5)
MCP_METRICS_TOKEN访问 /metrics 所需的令牌。未设置时,/metrics 遵循服务本身的身份验证要求:服务未启用身份验证时开放,设置了 bearer 令牌时要求提供该令牌,配置 OAuth 时则关闭(JWT 不适合抓取器——请改为设置此项)
MCP_REDACT_REQUIRE_REVOKE设为 1 时,如果数据库仍允许角色读取已遮蔽的列,就拒绝提供服务——将上面的设置从建议变为保证
MCP_STRUCTURED_CONTENT设为 1 时还会返回 MCP structuredContent;默认关闭,因为忽略它的客户端会为每个结果付出双倍成本。来源标记包含在对象中,但保护文本块的分隔符转义不适用——如果客户端将结构化输出直接粘贴到提示词中,就会失去这一层保护
MCP_RESERVED_AUTH_SLOTS为经过身份验证的流量保留的数据库连接槽位,防止匿名请求洪流占满连接池(默认:四分之一)
MCP_PUBLIC_URL此服务的公开基础 URL,用于 OAuth 发现元数据
MCP_AUTH_SERVERS在该元数据中公布的授权服务 URL
MCP_PROTOCOL_PREVIEW已弃用。 在 2026-07-28 仍为草案期间,此项用于启用该版本;上游于 2026-08-03 发布该版本,服务现在默认使用它。此名称仍会被识别,以免现有配置项被报告为拼写错误;启动时会提示一次,此项已不再生效
MCP_ALLOW_SCHEMAS查询可以访问的模式,例如 public,analytics;设置此项或下一项会启用允许列表
MCP_ALLOW_TABLES查询可以访问的关系,例如 public.orders,analytics.*
MCP_ALLOW_CATALOG设为 1 时,在启用允许列表期间仍允许访问 pg_catalog
MCP_ALLOW_PLAINTEXT_DB设为 i-accept-the-risk 时,允许使用 sslmode=disable 连接不在此机器上的数据库
MCP_ALLOW_EXCESSIVE_ROLE设为 i-accept-the-risk 时,允许使用具有写入权限的角色启动网络监听
MCP_ALLOW_ANONYMOUS_NETWORK设为 i-accept-the-risk 时,允许在未启用身份验证的情况下启动网络监听
MCP_ALLOWED_ORIGINS允许调用此服务的浏览器源,例如 https://my-client.example。留空表示不允许任何浏览器页面访问:用户仅仅访问的网页就能让其浏览器向 localhost 发送 POST 请求,这正是 DNS 重绑定的全部伎俩
MCP_ALLOWED_HOSTS监听环回地址时额外接受的 Host 值(始终接受 localhost 和 127.0.0.1)
MCP_FUZZ_VERBOSE仅用于开发:使 --fuzz 打印尝试过的每种变更
MCP_TRUST_PROXY仅在反向代理后面时设为 1——此时速率限制器会根据 X-Forwarded-For 而不是对端地址进行限制

许可证

MIT — 详见 LICENSE。核心部分采用 MIT 许可证,并将一直如此。

商业和团队使用

将其连接到组织内部的生产数据库,会引发 MIT 核心部分无法解答的问题:绑定到身份而非范围的策略、发送到无法被悄悄修改的位置的审计日志、合规审查人员认可的证据,以及有人需要为其负责的部署。

目前正在规划涵盖这些需求的团队版,具体内容尚未确定。如果你的组织有这类需求,请写信至 [email protected],说明其中必须包含哪些内容。目前还没有可购买的产品——你们的回复将决定是否开发,以及开发顺序。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。