qh について
qh は、少数のツールで複数のデータベースを照会する AI エージェント向け MCP サーバーです。スキーマ確認とクエリ実行に対応し、ローカルの stdio と HTTP サーバーモードを利用できます。
主な機能
- 01テーブル、列、キー、定義の確認
- 02クエリ実行と TSV 形式での結果返却
- 03PostgreSQL、MySQL、MariaDB、SQLite、MongoDB に対応
- 04stdio MCP と HTTP サーバーモード
- 05接続文字列の暗号化保存
活用シーン
- 01エージェントによるデータベーススキーマの確認
- 02エージェントによる読み取り専用クエリの実行
- 03HTTP 経由でのチームへのデータベース提供
MCP 対応
qh は、データベース構造の確認とステートメント実行に使う `schema` と `query` の 2 つの MCP ツールを公開します。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 エージェント向けのデータベースサーバーです。使用するトークンをできるだけ少なくするように設計されています。接続数にかかわらず、2 つの MCP ツールを公開し、MCP の指示内で接続名を示します。
| ツール | 機能 |
|---|---|
schema | テーブルの列、キー、外部キーの概要、または型、双方向の外部キー、列挙型の値、インデックスを含む完全な定義を表示します。複数の接続があるワークスペースで引数を指定しない場合は、接続の一覧を表示します |
query | 1 つのステートメントを実行し、デフォルトで 100 行の TSV を返します。フッターには行が打ち切られたかどうかが表示されます |
現在の対応対象:PostgreSQL 13 以降、MySQL 8.0.16 以降、MariaDB 10.6 以降、SQLite、MongoDB(7 および 8)。
qh serve は、REST API を備えた HTTP サーバーモードを追加します。
bench/ は、同じ質問、モデル、プロンプトを使って、qh と他のデータベース MCP サーバーを比較します(bench/README.md を参照)。すべての比較で精度は同等でした。
| 競合製品 | データセット | qh でタスクごとに削減できるトークン数 |
|---|---|---|
| Google's 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% |
インストール
macOS または Linux では、Homebrew を使います。
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 - を指定して標準入力から接続文字列を読み込みます。
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"
接続文字列は ~/.config/qh/qh.db 内で暗号化されます(AES-256-GCM)。キーは ~/.config/qh/master.key(または $QH_MASTER_KEY)です。$XDG_CONFIG_HOME が設定されている場合は、~/.config/qh の代わりに $XDG_CONFIG_HOME/qh が使われます。別のディレクトリを使うには、$QH_HOME を設定してください。
エディターから使う
qh は qh mcp を使って stdio 経由で MCP と通信します。実行中に qh conn で追加または編集した接続は、エージェントの次回の呼び出しで使用されます(QH_DSN が設定されている場合を除きます。以下を参照)。アプリによってはシェルの PATH を引き継がないことがあります。サーバーの起動に失敗した場合は、qh の代わりに which 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 エージェントモード)
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(Settings → Developer → Edit Config)。その後、アプリを再起動します |
| 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(Windows:%APPDATA%\Zed\settings.json)の context_servers の下に追加します。
{
"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 を渡すと、シェルから値がコピーされるため、パスワードが ps やシェルの
履歴に残りません。コンテナー内の 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:**エージェントがsourceとして渡す接続名です(デフォルトはdb)。 - **
QH_DESCRIPTION:**データベースに何が格納されているかをエージェントに伝える任意のテキストです。 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 キーを 1 つ表示します(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(接続の作成、変更、削除。同じワークスペース用の query/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 アドレスを埋め込む次の 3 つの 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 秒待機し、その後 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 ファイルにストアを保持します。ロードバランサーの背後で複数のインスタンスを実行するには、すべてのインスタンスを 1 つの 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スキーマ内にテーブルを作成します。インスタンスを同時に起動すると、各マイグレーションは 1 回だけ適用され、管理者キーが 1 つ作成されます。作成したインスタンスだけがそのキーを表示します(自分でキーを指定するには、すべてのインスタンスでQH_BOOTSTRAP_ADMIN_KEYに同じ値を設定します)。 - 変更は共有されます。 どのインスタンスから接続、キー、ワークスペースを変更しても、次回の呼び出しからすべてのインスタンスに反映されます。あるインスタンスでキーを失効させると、すべてのインスタンスでただちに拒否されます。あるインスタンスでスキーマを変更すると(
queryに送信した書き込みまたは DDL ステートメント、接続の変更、refresh、ワークスペースの削除)、すべてのインスタンスでキャッシュされたスキーマが破棄されます。データベースに直接実行した DDL は、キャッシュの有効期限が切れるまで反映されません。 - セッション固定は不要です。 HTTP 経由の MCP はステートレスなので、ロードバランサーでセッション固定を行う必要はありません。
- 制限はインスタンスごとに適用されます。 キーごとの同時実行数の上限、各接続の
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 個保持します。データベースのmax_connectionsはプロセス数 × 11 に合わせて設定し、複数の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 では、dsn の代わりに host、port、database、username、password、params を送信します。
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 にはデータベースのパスワードがないため、この編集は拒否されます。--skip-test を指定しない限り、qh は更新後の接続をテストしてから保存します。テストに失敗すると、保存済みの接続は変更されません。
最初の行が空、または直ちに EOF になった場合、インラインパスワードはクリアされます。CLI は最初の行の空白を取り除き、末尾に改行がない行も受け付け、後続の行は無視します。1 つのコマンドで stdin を読み取れるのは、--dsn -、--password-stdin、--ssh-password-stdin、--ssh-passphrase-stdin のいずれか 1 つだけです。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-stdin とともに --password-ref '' を渡します。DSN 参照の場合は、パスワードを含まないリテラルの --dsn を渡します。これにより参照が置き換えられ、新しい DSN に --password-stdin でパスワードが設定されます。両方の参照がある場合は、--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:… を追加してください。qh は OpenSSH と同様に踏み台ホストの Ed25519 ホストキーを優先し、それ以外のキーは拒否します。キーのファイルは CLI で読み込まれ、--tls-key と同様に暗号化して保存されます。SSH ユーザーは --ssh-user で指定するか、--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 シークレットの 1 つのフィールド。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 秘密鍵では、KDF ラウンド数は最大 128 回です(
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 が欠落しているときに、選択していないキーですべてを黙って再シールすることはありません。
保存されているすべての接続は、1 つのトランザクション内で新しいキーを使って再シールされます。古いキーで開けない接続がある場合、何も変更されません。新しいキーでサーバーを再起動してください。古いキーは不要になります。
リモート 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://でキーを送信することを拒否します。 --keyよりQH_KEYを優先してください。コマンドライン引数は同じマシンの他のユーザーから見えるうえ、シェルの履歴にも保存されます。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)'
エージェントは、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。
- ドキュメントの書き込み:
- リテラル: relaxed 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 文字以下の場合に限り、確認された値が
…で終わる形で一覧表示されます。 customer_idのような<x>_id、<x>Id、<x>_key、<x>_refという名前のフィールドは、参照(→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 はすべての呼び出しをチェックし、このロールが第 2 の防御線となります。
読み取り専用の安全性
読み取り専用接続では、すべてのクエリが読み取り専用トランザクション(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。詳細はライセンスを参照してください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。