关于 qh
qh 是面向 AI Agent 的数据库 MCP 服务,用少量工具查询多种数据库。它提供结构查看与 SQL 查询,并支持本地 stdio 和 HTTP 服务模式。
主要功能
- 01查看表、列、键及完整结构定义
- 02执行查询并以 TSV 返回结果
- 03支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MongoDB
- 04支持 stdio MCP 与 HTTP 服务模式
- 05连接字符串加密存储
适用场景
- 01让 Agent 检查数据库表结构
- 02让 Agent 对数据库执行只读查询
- 03通过 HTTP 为团队提供数据库访问
MCP 相关
qh 向 MCP 客户端提供 `schema` 和 `query` 两个工具,用于查看数据库结构和运行语句。可通过 stdio 或服务模式下的 Streamable HTTP 接入。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm -e QH_DSN -e QH_ENGINE -e QH_NAME -e QH_DESCRIPTION -e QH_MODE ghcr.io/anderson-0/qh:1.0.3QH_DSN密钥Connection string of the database to query, e.g. postgres://reader:[email protected]:5432/shopQH_ENGINEDatabase engine, needed only when QH_DSN does not start with a known scheme (postgres://, postgresql://, mysql://, mariadb://, mongodb://, mongodb+srv://, sqlite://) or end in .db, .sqlite or .sqlite3QH_NAMEName the agent sees for the connection (default db)QH_DESCRIPTIONOne line telling the agent what the database holdsQH_MODEro (default) for read-only, or rw to allow writes本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/anderson-0/qh:1.0.3stdio工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
qh
一款面向 AI Agent 的数据库服务,旨在尽可能减少 Token 消耗。它只暴露两个 MCP 工具, 无论连接数多少,并会在 MCP 指令中列出连接名称:
| 工具 | 功能 |
|---|---|
schema | 概览表及其列、键和外键,或显示完整定义,包括类型、双向外键、枚举值和索引。在包含多个连接的工作区中不带参数时,会列出连接 |
query | 执行一条语句并返回 TSV,默认 100 行,末尾会说明是否截断了结果 |
目前支持:PostgreSQL 13+、MySQL 8.0.16+、MariaDB 10.6+、SQLite 和 MongoDB(7 和 8)。
qh serve 会启用 HTTP 服务模式,并提供 REST API。
bench/ 会使用相同的问题、模型和提示词,将 qh 与其他数据库 MCP 服务进行对比(参见 bench/README.md)。每次对比的准确率都相同:
| 对比对象 | 数据集 | qh 每项任务减少的 Token 数 |
|---|---|---|
| Google 的 MCP Toolbox for Databases 1.14.0 | Postgres、MySQL、SQLite | 83.6% |
| dbhub 1.5.0 | Postgres、MySQL、SQLite | 40.2% |
| mongodb-mcp-server 3.0.5 | MongoDB | 95.2% |
安装
使用 Homebrew,在 macOS 或 Linux 上安装:
brew install anderson-0/tap/qh
或者从发布页面下载适用于 Linux、macOS 或 Windows(amd64 或 arm64)的二进制文件,解压后将 qh 放入 PATH。
每个版本都会在 checksums.txt 中列出 SHA-256 校验和。二进制文件未签名:在 macOS 上,使用浏览器下载的二进制文件首次运行前需要执行 xattr -d com.apple.quarantine qh。
或者运行服务镜像,该镜像会在端口 8765 上启动服务模式,并将数据存储在 /data 卷中。
创建主密钥后请妥善保存:它用于解密已存储的连接字符串,因此每次重新创建容器时(例如升级时)都要传入相同的密钥:
openssl rand -base64 32 > qh-master.key && chmod 600 qh-master.key
docker run -d --name qh -p 127.0.0.1:8765:8765 -v qh-data:/data \
-e QH_MASTER_KEY="$(cat qh-master.key)" ghcr.io/anderson-0/qh:1
docker logs qh # shows the admin API key once, on first start
- 暴露服务。 API 密钥通过未加密的 HTTP 传输,因此示例仅监听 localhost。在将 qh 暴露给其他机器之前,请在其前面配置 TLS 代理。
- 参数。 镜像名称之后的参数会替换默认的
serve --http :8765。 - 连接主机数据库。 在容器内,
localhost指向容器本身。要连接 Docker 主机上的数据库,请使用host.docker.internal;在 Linux 上,添加--add-host=host.docker.internal:host-gateway。 - 健康检查。
GET /healthz无需密钥即可返回ok,可用于存活和就绪探针。 - 存储。 镜像以 uid 65532 运行:
- 命名卷可按示例使用;
- 绑定挂载的目录必须允许该 uid 写入(
chown 65532 DIR); - 在 Kubernetes 上,设置
securityContext.fsGroup: 65532。
或者使用 Go 构建:
go install github.com/anderson-0/qh/cmd/qh@latest
添加连接
除非传入 --mode rw,否则连接均为只读。传入 --dsn - 可从 stdin 读取连接字符串,
以免密码留在 shell 历史记录中:
qh conn add shop --engine postgres --dsn - --description "Orders and customers"
qh conn add local --engine sqlite --dsn ./app.db
qh conn add app --engine mysql --dsn - --description "Product catalogue"
连接字符串使用 AES-256-GCM 加密,存储在 ~/.config/qh/qh.db 中;密钥位于 ~/.config/qh/master.key
(或 $QH_MASTER_KEY)。如果设置了 $XDG_CONFIG_HOME,则使用 $XDG_CONFIG_HOME/qh,而不是 ~/.config/qh;
设置 $QH_HOME 可使用其他目录。
在编辑器中使用
qh 通过 qh mcp 使用 stdio 进行 MCP 通信。在其运行期间,通过 qh conn 添加或编辑的连接会在 Agent 下次调用时生效(设置了 QH_DSN 时除外;见下文)。有些应用不会继承 shell 的 PATH;如果服务无法启动,请改用 which qh 输出的完整路径,而不是 qh。
Claude Code
claude mcp add qh -- qh mcp
Codex CLI
codex mcp add qh -- qh mcp
Gemini CLI
gemini mcp add -s user qh qh mcp
VS Code(GitHub Copilot Agent 模式)
code --add-mcp '{"name":"qh","command":"qh","args":["mcp"]}'
Claude Desktop、Cursor、Windsurf(Devin Desktop):将以下内容添加到应用的 MCP 配置文件中:
{
"mcpServers": {
"qh": { "command": "qh", "args": ["mcp"] }
}
}
| 应用 | 配置文件 |
|---|---|
| Claude Desktop | macOS:~/Library/Application Support/Claude/claude_desktop_config.json,Windows:%APPDATA%\Claude\claude_desktop_config.json(设置 → 开发者 → 编辑配置);之后重启应用 |
| Cursor | ~/.cursor/mcp.json,或项目中的 .cursor/mcp.json |
| Windsurf(Devin Desktop) | ~/.config/devin/mcp_config.json,或 Windows 上的 %APPDATA%\devin\mcp_config.json;较旧的 Windsurf 安装使用 ~/.codeium/windsurf/mcp_config.json |
Zed:将其添加到 ~/.config/zed/settings.json 中的 context_servers 下(Windows:%APPDATA%\Zed\settings.json):
{
"context_servers": {
"qh": { "command": "qh", "args": ["mcp"] }
}
}
从环境变量配置单个连接
Docker 的 MCP 目录和官方 MCP 注册表仅通过环境变量配置服务。
设置 QH_DSN 后,qh mcp 只提供这一个连接,并忽略通过
qh conn add 添加的连接:
export QH_DSN='postgres://reader:…@db.example.com/shop'
docker run -i --rm -e QH_DSN ghcr.io/anderson-0/qh:1 mcp
不指定值传入 -e QH_DSN 会从 shell 复制该变量,因此密码不会出现在 ps 输出或 shell
历史记录中。在容器内,localhost 指的是容器本身;如需连接 Docker 主机上的数据库,请使用
host.docker.internal(参见安装)。
- **
QH_DSN:**连接字符串。引擎根据其协议方案推断(postgres://、postgresql://、mysql://、mariadb://、mongodb://、mongodb+srv://、sqlite://、file:),也可以根据以.db、.sqlite或.sqlite3结尾的路径推断。对于其他字符串(例如user:pw@tcp(host)/db),请设置QH_ENGINE。 QH_ENGINE:postgres、mysql、mariadb、mongodb或sqlite。设置后始终优先于推断出的引擎。- **
QH_NAME:**Agent 作为source传入的连接名称(默认为db)。 - **
QH_DESCRIPTION:**可选文本,用于告知 Agent 数据库中存储的内容。 QH_MODE:ro(默认)或rw。
此模式不会在进程结束后留下任何磁盘文件:存储和查询日志使用临时文件,并在退出时删除;随机主密钥只存在于内存中,也不会创建数据目录或 master.key。设置 QH_DSN 时,不会使用通过 qh conn 添加的连接。启动时会检查字符串,但不会连接数据库,因此无法访问的数据库会在每次查询时失败,而不会导致服务停止。QH_DSN 不能与 --store、QH_STORE_DSN、--server 或非默认的 --workspace 同时使用。
使用 docker run --read-only 时,请为临时存储挂载可写的 /tmp:--tmpfs /tmp。
服务模式
为团队运行 qh,或将其作为产品的后端:
export QH_MASTER_KEY=$(openssl rand -base64 32) # keep it: it decrypts stored connection strings
qh serve --http :8080 --data-dir /var/lib/qh
首次启动时,qh 会打印一个管理员 API 密钥(admin API key (shown once): qh_…);如果你设置了 QH_BOOTSTRAP_ADMIN_KEY,则会使用该变量。该密钥必须是一个密钥秘密部分至少有 32 个字符的 qh_ 密钥(例如,使用 qh key create --scopes admin 创建的密钥)。每个请求都需要 Authorization: Bearer <key>:
- 工作区用于隔离连接。管理员密钥用于管理工作区和密钥,并通过
X-QH-Workspace标头选择工作区。 - 权限范围:
query(MCP 工具、架构、查询)、manage(创建、更改和删除连接;为同一工作区创建查询/管理密钥,新密钥的有效期不会晚于创建它的密钥)、admin(所有操作)。 - 撤销密钥(
qh key rm ID或DELETE /api/v1/keys/{id})只会删除该密钥。它创建的密钥,以及这些密钥创建的密钥,会继续有效,直到被撤销或过期:qh 会单独检查每个密钥,绝不会检查创建该密钥的密钥。qh key rm会在 stderr 上说明这一点,API 也会在QH-Warning标头中发送相同的提示。使用已撤销密钥接受的请求仍可能完成。 - MCP通过
/mcp(Streamable HTTP)提供服务。REST API 位于/api/v1下;请参阅/api/v1/openapi.json。 - **
qh key和qh workspace**作用于--home指定的本地存储,或通过--server/--key指定的服务(参见远程 CLI)。
curl -s -X POST localhost:8080/api/v1/workspaces -H "Authorization: Bearer $ADMIN" -d '{"name":"team"}'
curl -s -X POST localhost:8080/api/v1/keys -H "Authorization: Bearer $ADMIN" \
-d '{"workspace":"team","scopes":["query","manage"],"name":"team"}'
curl -s -X POST localhost:8080/api/v1/connections -H "Authorization: Bearer $TEAM" \
-d '{"name":"shop","engine":"postgres","dsn":"postgres://reader:…@db.internal/shop"}'
claude mcp add --transport http qh http://localhost:8080/mcp --header "Authorization: Bearer $TEAM"
服务模式会拒绝环回和链路本地范围内的数据库地址,包括云元数据地址 169.254.169.254。它还会拒绝三个内嵌 IPv4 地址的 IPv6 范围:64:ff9b::/96、64:ff9b:1::/48 和 ::/96。它只接受 TCP 连接。如果你通过 NAT64 访问 IPv4 数据库,请放行所需的转换后地址:203.0.113.0/24 会转换为 --allow-cidr 64:ff9b::cb00:7100/120。如果你的网络使用其他转换前缀(例如 6to4 或 Teredo),请使用 --deny-cidr 拒绝这些前缀。使用 --allow-cidr 127.0.0.0/8 可放行本地数据库(如果 qh 在容器中运行,这样做并无帮助,因为主机上的数据库不在容器的环回地址上;请参阅安装部分中关于 host.docker.internal 的说明);使用 --deny-cidr 可阻止更多地址(例如你的私有地址范围)。除非传入 --sqlite-root DIR,否则 SQLite 不可用;传入后,文件必须位于 DIR/<workspace>/ 下,这样每个工作区只能访问自己的目录。每个 API 密钥可同时运行 4 个查询;额外请求最多等待 10 s 以获取空闲槽位,之后会收到 HTTP 429。
服务模式会忽略 PG* 环境变量(启动时会将其取消设置,并在 stderr 上列出变量名);Postgres 连接字符串只允许使用以下设置:host、port、dbname、user、password、sslmode、sslsni、sslnegotiation、connect_timeout、target_session_attrs、application_name、channel_binding、require_auth、min_protocol_version 和 max_protocol_version。需要在服务端读取文件的设置(passfile、service、sslcert、sslrootcert 等)会被拒绝,并且不会使用 ~/.pgpass 和 ~/.postgresql/。
如果不使用 --http,qh serve 会以本地模式在 127.0.0.1:8765 上运行,不需要密钥,供你自己机器上的工具使用。本地模式不进行身份验证:机器上的每个进程都能使用该模式及你的所有连接。它只接受发往 localhost 的请求,并拒绝跨源浏览器请求。
运行多个实例
默认情况下,qh serve 会将存储保存在 --data-dir 中的 SQLite 文件里。要在负载均衡器后运行多个实例,请让它们全部指向同一个 Postgres 数据库:
export QH_STORE_DSN='postgres://[email protected]:5432/qh?sslmode=verify-full'
export QH_STORE_PASSWORD_FILE=/run/secrets/qh-store-password # or put the password in the URL
export QH_MASTER_KEY=… # the same key on every instance
qh serve --http :8765 --allow-cidr 10.0.0.0/8
- 存储内容。 qh 首次启动时会在
qh模式中创建数据表。多个实例同时启动时,每项迁移只会应用一次,并且只创建一个管理员密钥;只有创建该密钥的实例会打印它(如要自行指定密钥,请在每个实例上设置相同的QH_BOOTSTRAP_ADMIN_KEY)。 - 更改会共享。 通过任一实例更改的连接、密钥或工作区,都会在下一次调用时应用到所有实例。在一个实例上撤销的密钥会立即被所有实例拒绝。通过一个实例进行的模式更改(向
query发送写入或 DDL 语句、更改连接、执行refresh或删除工作区)会清除所有实例上的缓存模式。直接对数据库运行的 DDL 仍需等待缓存过期。 - 无需会话粘滞。 MCP over HTTP 是无状态的,因此负载均衡器无需会话粘滞。
- 限制按实例计算。 这包括每个密钥的并发限制、每个连接的
pool_size(因此数据库最多会看到 实例数 ×pool_size个连接),以及连接总数上限。请根据实例数量设置这些限制。 - 查询日志中的时间来自各实例的时钟。 请保持时钟同步。
- 存储的 URL 必须包含密码以外的所有信息。 服务模式会在启动时清除
PG*变量,因此主机、端口、数据库、用户和 TLS 设置都必须放在 URL 中;密码可以改由QH_STORE_PASSWORD_FILE提供。 - 主密钥仅从
QH_MASTER_KEY获取。 使用共享存储时,qh 永远不会读取或在数据目录中创建master.key:一个主机上的密钥文件可能会使用其他实例没有的密钥封存数据。如果设置了--store或QH_STORE_DSN,所有打开存储的命令(serve、mcp、conn、query、masterkey rotate等)都必须设置QH_MASTER_KEY,否则会失败。 - 每个进程的连接数。 每个 qh 进程(每个
qh serve,以及每个qh mcp)最多会持有 10 个存储连接池连接,外加 1 个用于监听更改的连接。请根据进程数 × 11 设置数据库的max_connections,并注意这一点,尤其是许多qh mcp进程共用存储时。
**迁移现有安装:**先停止服务,然后将其存储复制到一个空数据库中。保留相同的主密钥(复制会原样迁移已封存的数据,因此复制过程本身不需要密钥)。目标数据库的密码应放在 --to URL 中,或由 QH_STORE_PASSWORD_FILE 提供。复制操作会读取由 --store 或 QH_STORE_DSN 选择的存储,因此运行此命令时请取消设置 QH_STORE_DSN,并使用 --to 指定新数据库:
qh store copy --home /var/lib/qh --to 'postgres://[email protected]:5432/qh?sslmode=verify-full' --with-query-log
在不停机的情况下轮换主密钥:
- 使用
QH_MASTER_KEY=<new>和QH_MASTER_KEY_OLD=<old>重启每个实例。每个实例都可以打开使用任一密钥封存的连接。请等到所有实例都已重启后再更改任何连接(conn add、conn edit等),因为使用新密钥封存的连接无法由仍只使用旧密钥运行的实例打开。 - 在任意主机上,设置
QH_MASTER_KEY=<new>和存储密码(放在 URL 中或由QH_STORE_PASSWORD_FILE提供),然后运行qh masterkey rotate --store "$QH_STORE_DSN" --old-key-file old.key,其中old.key保存旧密钥。(同时使用两个密钥运行的实例可以继续运行;单实例场景下先停止qh serve的建议不适用。) - 重启每个实例,并取消设置
QH_MASTER_KEY_OLD。
在这些步骤之间从管理员主机运行的命令也会读取 QH_MASTER_KEY_OLD,因此可以打开使用任一密钥封存的连接。
连接详情和 TLS
除了连接字符串,你也可以提供各个部分,qh 会组装并转义这些内容:
printf '%s\n' "$PASSWORD" | qh conn add shop --engine postgres --host db.internal --database shop \
--user reader --password-stdin --param application_name=qh
通过 REST 发送 host、port、database、username、password 和 params,而不是 dsn。
TLS 设置会替换连接字符串中有关 TLS 的所有设置:
qh conn add shop --engine postgres --dsn - --tls-mode verify-full --tls-ca ca.pem --tls-cert client.pem --tls-key client.key
- 模式:
disable;require,加密连接但不验证证书;verify-ca,验证证书链;verify-full,验证证书链和主机名,或使用--tls-server-name。
- 私钥会与连接字符串一起加密,之后不会再次显示。
- 在服务模式下,这是使用客户端证书的唯一方式,因为系统会拒绝读取服务端文件的连接字符串设置。
qh conn edit NAME --no-tls会移除这些设置。
更新已存储的密码
若要仅替换 qh 存储的数据库密码,请保留其他连接设置,并从 stdin 读取新密码:
printf '%s\n' "$PASSWORD" | qh conn edit shop --password-stdin
这会更新 qh 存储的凭据,不会更改数据库账户的密码。此功能适用于 PostgreSQL、MySQL、MariaDB 和 MongoDB。SQLite 没有数据库密码,因此会拒绝此项编辑。保存前,qh 会测试更新后的连接,除非你传入 --skip-test。测试失败时,已存储的连接不会更改。
如果首行为空或立即遇到 EOF,则会清除内联密码。CLI 会去除首行的空白字符,接受末尾没有换行符的行,并忽略后续行。单条命令中,--dsn -、--password-stdin、--ssh-password-stdin 和 --ssh-passphrase-stdin 只能有一个读取 stdin。设为 false 的布尔标志不会读取 stdin。
远程模式通过服务执行相同的编辑。使用管理密钥时,请明确指定工作区:
printf '%s\n' "$PASSWORD" | qh --server https://qh.example.com --key "$QH_KEY" \
--workspace team conn edit shop --password-stdin
使用 --dsn-ref 或 --password-ref 的连接必须在同一次编辑中移除这些数据库引用。对于密码引用,请将 --password-ref '' 与 --password-stdin 一起传入。对于 DSN 引用,请传入不含密码的字面量 --dsn;这会替换该引用,而 --password-stdin 则会为新的 DSN 设置密码。如果两种引用都存在,还要传入 --password-ref ''。不能同时传入字面量 --dsn 和 --dsn-ref,即使该引用为空也是如此。SSH 引用保持不变。
通过 REST,PATCH /api/v1/connections/{name} 接受 password。未提供该字段或将其设为 null 时,密码保持不变;空字符串会清除密码。REST 会保留传入的空白字符。show/list 输出和响应中会隐藏密码。较新的 CLI 使用此功能连接较旧的服务时,会收到服务返回的未知字段错误;请先升级服务,再编辑密码。
SSH 隧道和机密引用
通过堡垒机访问数据库:
qh conn add prod --engine postgres --dsn 'postgres://[email protected]:5432/app' --password-ref env:PROD_DB_PASSWORD \
--ssh-host [email protected] --ssh-key ~/.ssh/id_ed25519
首次运行会失败,并显示堡垒机的主机密钥指纹。请验证指纹(ssh-keyscan -t ed25519 bastion.example.com | ssh-keygen -lf -),然后添加 --ssh-host-key SHA256:…。与 OpenSSH 一样,qh 优先使用堡垒机的 Ed25519 主机密钥,并拒绝任何其他密钥。密钥文件由 CLI 读取并以密封形式存储,与 --tls-key 一样。使用 --ssh-user 指定 SSH 用户,或在 --ssh-host 中写为 user@。使用密码认证时,请传入 --ssh-password-stdin;使用加密密钥时,请传入 --ssh-passphrase-stdin。
连接可以引用密码或连接字符串,而不必存储它们:
| 引用 | 值 |
|---|---|
env:NAME | qh 进程的环境变量 |
file:/path | 文件内容(最多 64 KiB,并去除末尾换行符),例如 Docker 或 Kubernetes 机密 |
aws-sm://<name or ARN>[#key] | AWS Secrets Manager 机密,或 JSON 机密中的一个字段;使用 qh 进程的 AWS 凭据读取 |
使用 --dsn-ref、--password-ref、--ssh-key-ref、--ssh-passphrase-ref 或 --ssh-password-ref 指定引用。qh 连接时才会解析引用,且绝不会存储引用值。机密轮换后,已打开的连接会继续工作,直到身份验证失败。该次调用会返回 connection_failed,下一次调用则会使用新值重新连接。
服务端模式。 租户不得读取服务端的环境变量或云机密,也不得通过隧道进入其网络:
- 除非操作员明确允许,否则会拒绝引用:
qh serve --http :8765 --allow-secret-ref 'env:TENANT_DB_*' --allow-secret-ref 'aws-sm://arn:aws:secretsmanager:us-east-1:123456789012:secret:qh/*'。 - 服务器上每个持有管理密钥的租户都可以使用获准的引用,租户也可以读取其值(例如,将连接指向其控制的服务端,该服务端随后会收到密码)。只允许使用你愿意提供给所有租户的机密。不得共享机密的租户必须使用不同的服务器。
- 对于
file:模式,如果挂载的机密是符号链接(例如 Kubernetes 机密卷中的情况),则必须使用目录模式(如file:/var/run/secrets/db/*)允许该机密,因为符号链接背后的实际路径也必须匹配。 - 堡垒机和其后的数据库都会根据服务器的网络策略(
--allow-cidr/--deny-cidr)进行检查。默认情况下,只会拒绝环回地址、未指定地址和链路本地地址(包括云元数据地址),以及64:ff9b::/96、64:ff9b:1::/48和::/96;因此,10.0.0.0/8等私有网段仍可访问。添加--deny-cidr 10.0.0.0/8 …可禁止租户访问这些网段。在 DNS64 网络上,请提供数据库的 IP 地址:如果名称解析结果包含转换后的地址,则会被拒绝。位于堡垒机自身环回地址上的数据库不受限制。 - 租户的加密 SSH 私钥最多可使用 128 轮 KDF(
ssh-keygen -a;默认值为 16)。如果密钥使用了更多轮数,请使用ssh-keygen -p -a 100 -f KEY重新加密。RSA 密钥最多可为 8192 位,DSA 密钥则会被拒绝。qh 连接时才会解密密钥,因此,密码短语错误会在连接测试时报告,而不会在未提供密码短语保存连接时报告。 --deny-ssh会关闭隧道。
查询日志
qh 会记录每次 query 调用:连接、API 密钥、语句(最多 8 KiB)、行数、耗时和错误类别。
- 使用
qh log [--connection NAME] [--since 24h] [--limit N]查看,或调用GET /api/v1/query-log(作用域为manage)。 qh serve和qh mcp会保留 30 天(--query-log-retention 7d),也可使用--no-query-log不记录日志。- 日志会按原样存储语句。如果语句包含敏感字面值,请关闭日志或缩短保留期限。
轮换主密钥
使用默认的 SQLite 存储时,请先停止 qh serve。使用共享的 Postgres 存储时,请勿停止实例:请改为按照运行多个实例中的步骤操作。
printf '%s\n' "$OLD_QH_MASTER_KEY" > old.key # the previous key
export QH_MASTER_KEY=$(openssl rand -base64 32) # the new key
qh masterkey rotate --old-key-file old.key --home /var/lib/qh
在本地模式下,将 master.key 移到其他位置(例如移为 old.key),然后运行 qh masterkey rotate --old-key-file old.key --generate;
--generate 会创建新的 master.key。如果不使用此选项,在没有当前密钥时,轮换操作会拒绝运行,因此缺失的
QH_MASTER_KEY 绝不会导致系统悄悄使用一个非你所选的密钥重新封装所有内容。
所有已存储的连接都会在一个事务中使用新密钥重新封装。如果有任何连接无法使用旧密钥打开,则不会进行任何更改。使用新密钥重启服务。之后不再需要旧密钥。
远程 CLI
每个 conn、query、schema、log、workspace 和 key 命令都可以对服务运行:
export QH_SERVER=https://qh.example.com QH_KEY=qh_…
qh conn ls
qh query shop "SELECT count(*) FROM orders"
qh --key "$ADMIN" --workspace team key create --scopes query,manage # admin keys name a workspace
--workspace(或QH_WORKSPACE)也可为本地命令选择工作区。- CLI 不会通过纯
http://向另一台机器发送密钥。 - 请优先使用
QH_KEY,而非--key:命令行参数会被机器上的其他用户看到,也会保存在 Shell 历史记录中。 mcp、serve和masterkey始终在本地运行。
MongoDB
qh conn add events --engine mongodb --dsn - # paste mongodb://user:pw@host:27017/events
qh query events 'db.orders.find({status: "paid"}).sort({total: -1}).limit(5)'
Agent 使用 mongosh 语法,通过相同的 schema 和 query 工具查询 MongoDB。qh 会解析固定的语法子集,且绝不运行 JavaScript:
- 读取:
find、findOne、aggregate、countDocuments、estimatedDocumentCount、distinct、getIndexes和db.getCollectionNames()。find支持.sort、.limit、.skip、.project、.hint、.collation、.count()和.explain()。 - **写入和管理调用:**这些操作需要读写连接。若写入仅部分完成,系统会报告已写入的数量。
- 文档写入:
insertOne/Many、updateOne/Many、replaceOne、deleteOne/Many和findOneAnd*。 - 包含
$out/$merge的聚合操作。 - 管理调用:
createIndex(es)、dropIndex(es)、drop、renameCollection和db.createCollection。
- 文档写入:
- **字面量:**宽松 JSON,以及
ObjectId()、ISODate()、new Date("…")、NumberLong()、NumberInt()、NumberDecimal()、UUID()、Timestamp()、MinKey()、MaxKey()和/regex/flags。 - **结果:**TSV,
_id位于首列。嵌套文档和数组采用紧凑 JSON 格式,缺失字段显示为NULL。 - **架构:**每个集合最多抽样 100 个文档。
- 字段以点分路径和类型显示;概览会将嵌套字段折叠为
address{city,country,…}。 - 仅当至少 50 个抽样文档包含该字段、不同值不超过 8 个且每个值长度不超过 40 个字符时,字符串字段才会列出已发现的值,并以
…结尾。 - 名为
<x>_id、<x>Id、<x>_key或<x>_ref的字段(例如customer_id)会显示为引用(→customers)。目标集合是名为<x>或<x>s的集合;如果没有,则是唯一一个名称以_<x>或_<x>s结尾的集合。字段类型必须与目标集合的_id类型一致,并且至少 5 个抽样值中的 4 个必须存在于目标集合中(若抽样值少于 5 个,则所有值都必须存在)。
- 字段以点分路径和类型显示;概览会将嵌套字段折叠为
- **数据库:**连接字符串中的数据库是默认数据库,也是 qh 唯一会检查的数据库。如果未指定数据库,则会列出所有非系统数据库,并像 mongosh 一样将
db.<collection>指向test。不使用--namespaces(REST API 中的allowed_namespaces)时,db.getSiblingDB("x")仍可访问其他数据库,是否允许由服务器自身的授权机制决定。使用该选项时,如果查询通过getSiblingDB或$out、$merge、$lookup或$unionWith阶段指定了其他数据库,就会以forbidden拒绝查询。 - **服务器模式:**连接字符串只能使用允许列表中的选项(选项名称的匹配方式与驱动程序相同):身份验证和副本集设置、
tls/ssl、超时、连接池大小、读写关注级别、压缩等。会读取服务器上文件的选项(例如tlsCAFile、tlsCertificateKeyFile、tlsCertificateKeyFilePassword及其ssl*别名)会被拒绝;tlsInsecure、tlsAllowInvalidCertificates和tlsAllowInvalidHostnames也会被拒绝。请改用 qh 的 TLS 设置。authMechanism可以是SCRAM-SHA-1、SCRAM-SHA-256、PLAIN或MONGODB-X509;其他机制(MONGODB-AWS、GSSAPI、MONGODB-OIDC)使用 qh 主机自身的凭据。authMechanismProperties会被拒绝。驱动程序的 OCSP 检查也会经过与数据库连接相同的地址保护机制。 - **服务器端 JavaScript:**qh 不会阻止
$where和$function;它们会作为读取操作的一部分运行。在 MongoDB 7 和 8 上,它们没有数据库句柄,因此无法访问其他数据库。对于有权执行find或aggregate的用户,MongoDB 角色无法禁止这些操作;只有在服务器上禁用服务器端脚本(security.javascriptEnabled: false)才能做到这一点。
只读连接请使用具有 read 角色的 MongoDB 用户;qh 会检查每次调用,而该角色是第二道防线。
只读安全
在只读连接上,每个查询都会在只读事务中运行(Postgres、MySQL/MariaDB),或在以只读方式打开的文件上运行(SQLite);此外,qh 还会在语句到达数据库之前拒绝可能写入数据的语句。只读事务无法阻止所有操作。在 Postgres 中,dblink 或 postgres_fdw 等扩展可以打开另一个非只读连接。在 MySQL/MariaDB 中,只读事务不会限制服务器端的文件访问,因此不要授予 FILE(LOAD_FILE、SELECT ... INTO OUTFILE)。为 qh 提供一个只能读取数据的数据库用户:
CREATE ROLE qh_reader LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE shop TO qh_reader;
GRANT USAGE ON SCHEMA public TO qh_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO qh_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO qh_reader;
对于 MySQL/MariaDB,只授予 SELECT(以及视图所需的 SHOW VIEW):
CREATE USER 'qh_reader'@'%' IDENTIFIED BY 'change-me';
GRANT SELECT, SHOW VIEW ON shop.* TO 'qh_reader'@'%';
不要授予 FILE 权限:拥有该权限后,SELECT LOAD_FILE(...) 可以读取数据库服务器上的文件。
开发
make test # needs Docker for the Postgres, MySQL/MariaDB and MongoDB tests
make test-short # skips Docker tests
make build # static binary in bin/qh
许可证
MIT。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。