mcpField
Q
qh

qh

本地服务
github.com数据库06 天前更新

关于 qh

qh 是面向 AI Agent 的数据库 MCP 服务,用少量工具查询多种数据库。它提供结构查看与 SQL 查询,并支持本地 stdio 和 HTTP 服务模式。

主要功能

  1. 01查看表、列、键及完整结构定义
  2. 02执行查询并以 TSV 返回结果
  3. 03支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MongoDB
  4. 04支持 stdio MCP 与 HTTP 服务模式
  5. 05连接字符串加密存储

适用场景

  1. 01让 Agent 检查数据库表结构
  2. 02让 Agent 对数据库执行只读查询
  3. 03通过 HTTP 为团队提供数据库访问

MCP 相关

qh 向 MCP 客户端提供 `schema` 和 `query` 两个工具,用于查看数据库结构和运行语句。可通过 stdio 或服务模式下的 Streamable HTTP 接入。

配置

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

mcp.json · 通用配置
{
"mcpServers": {
"anderson-0-qh": {
"command": "docker",
"args": [
"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.3"
],
"env": {
"QH_DSN": "<QH_DSN>",
"QH_ENGINE": "<QH_ENGINE>",
"QH_NAME": "<QH_NAME>",
"QH_DESCRIPTION": "<QH_DESCRIPTION>",
"QH_MODE": "<QH_MODE>"
}
}
}
}
启动命令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.3
需要配置的环境变量
QH_DSN密钥Connection string of the database to query, e.g. postgres://reader:[email protected]:5432/shop
QH_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 .sqlite3
QH_NAMEName the agent sees for the connection (default db)
QH_DESCRIPTIONOne line telling the agent what the database holds
QH_MODEro (default) for read-only, or rw to allow writes

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

安装包
ocighcr.io/anderson-0/qh:1.0.3

工具

还没有获取到工具列表

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

详细介绍

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.0Postgres、MySQL、SQLite83.6%
dbhub 1.5.0Postgres、MySQL、SQLite40.2%
mongodb-mcp-server 3.0.5MongoDB95.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 DesktopmacOS:~/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

在不停机的情况下轮换主密钥:

  1. 使用 QH_MASTER_KEY=<new> 和 QH_MASTER_KEY_OLD=<old> 重启每个实例。每个实例都可以打开使用任一密钥封存的连接。请等到所有实例都已重启后再更改任何连接(conn add、conn edit 等),因为使用新密钥封存的连接无法由仍只使用旧密钥运行的实例打开。
  2. 在任意主机上,设置 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 的建议不适用。)
  3. 重启每个实例,并取消设置 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:NAMEqh 进程的环境变量
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。

评论

支持 Markdown

第一次接触 MCP?

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

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