mcpField
S
sw-postgres-mcp

sw-postgres-mcp

ローカル
github.com/jpkaデータベース08 週間前に更新

sw-postgres-mcp について

sw-postgres-mcpは、Agent向けに読み取りと書き込みを分離するPostgres MCPサーバーです。書き込みはロールバック付きトランザクションでプレビューされ、別途承認された内容一致のプランだけが実行されます。

主な機能

  1. 01読み取り専用のスキーマ・クエリ・計画ツール
  2. 02ロールバック方式の書き込みプレビュー
  3. 03独立したプラン実行手順
  4. 04データベースロールの分離
  5. 05読み書きの許可リスト
  6. 06承認しきい値と行数上限

活用シーン

  1. 01スキーマの確認と許可テーブルの検索
  2. 02コミット前のデータベース変更の確認
  3. 03大規模な変更への人手による承認

MCP 対応

MCPクライアントに8つのツールを提供します。内訳は読み取り専用3つ、書き込みプレビュー4つ、プレビュー済み変更のみをコミットする唯一のツール`execute_plan`です。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"jpka-sw-postgres": {
"command": "npx",
"args": [
"-y",
"sw-postgres-mcp"
],
"env": {
"DATABASE_URL_READONLY": "<DATABASE_URL_READONLY>",
"DATABASE_URL_WRITER": "<DATABASE_URL_WRITER>"
}
}
}
}
起動コマンドnpx -y sw-postgres-mcp
設定が必要な環境変数
DATABASE_URL_READONLYシークレットPostgres connection string for the read-only role.
DATABASE_URL_WRITERシークレットPostgres connection string for the writer role.

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npmsw-postgres-mcp0.3.1

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

セーフライト Postgres MCP サーバー

エージェントは、取り返しのつかない事故を起こせない状態で、データベースの読み取りと変更ができます。差別化要因はツールの対応範囲ではなく、安全性のレイヤーです。8 つの MCP ツールのうち 3 つは読み取り専用で、4 つの書き込みツールは変更をプレビューするだけです。そして、プレビュー済みの変更だけを確定する execute_plan が 1 つあります。

アーキテクチャ

 Claude (agent)
      │  MCP stdio (tools/call)
      ▼
 sw-postgres-mcp
 │
 ├─ describe_schema / query / explain_plan ──► readonly pool ──► Postgres "readonly" role (SELECT only)
 │
 ├─ delete_rows / insert_rows / update_rows / run_migration
 │     │
 │     ▼
 │  TwoPhaseWrite.preview()
 │     BEGIN → run the real statement → ROLLBACK
 │     DML (delete_rows/insert_rows/update_rows): capture exact RETURNING count + sample
 │     DDL (run_migration): no RETURNING to capture — reports 0 affected rows, a `target`
 │                           table/schema extracted from the statement text, and always
 │                           goes to "awaiting_approval" regardless of that row count
 │     └─► plan_token = sha256(statement + params)   ("statementFingerprint")
 │           ├─ affected_rows ≤ approvalRequiredAboveRows  → status: "previewed"
 │           ├─ affected_rows >  approvalRequiredAboveRows → status: "awaiting_approval"
 │           └─ affected_rows >  hardMaxRows               → refused outright, no token issued
 │           (run_migration ignores both thresholds — every DDL preview is "awaiting_approval")
 │
 ├─ execute_plan(plan_token, statement, params) ──► writer pool ──► Postgres "writer" role (DML + DDL)
 │     re-derives the fingerprint from what was passed back and refuses on any mismatch
 │     (STATEMENT_MISMATCH) or an affected-row-set that changed since preview (ROWSET_CHANGED)
 │
 └─ every preview / approval / execution / rejection / refusal ──► mcp_audit.log
                                                                     (INSERT-only grant;
                                                                      UPDATE/DELETE/TRUNCATE revoked
                                                                      — see "Audit log" below)

  an "awaiting_approval" plan surfaces at:
  localhost approval UI — http://127.0.0.1:4319/?token=<per-session-token>
    (bound to 127.0.0.1, human-only; the token is printed once on stderr at
    startup and every route requires it as `Authorization: Bearer <token>`
    or `?token=` — see "Localhost approval UI" below)
    approve() / reject() are called directly on the shared TwoPhaseWrite instance —
    never exposed as an MCP tool the agent itself can reach

それぞれ異なるロールで認証する 2 つの Postgres 接続プールを使用します(詳しくは以下の脅威モデルをご覧ください)。describe_schema/query/explain_plan には readonly を、4 つの書き込みプレビューツールと execute_plan には writer を使用します。4 つの書き込みツール(delete_rows、insert_rows、update_rows、run_migration)はすべて、共通のコアである TwoPhaseWrite(src/writeCore.ts)を使用します。どのツールも、必ずロールバックするトランザクション内でプレビューを行い、実際に確定するには、正確なプラントークンを指定した別個の execute_plan 呼び出しが必要です。5 つ目の書き込みツールも、プレビューステップを省略するツールもありません。このサーバーで変更を確定できるのは execute_plan だけであり、プレビュー済みのステートメントを再実行することしかできません。各ツールで受け取る値と返す値については、以下のツールをご覧ください。仕組みについては2 フェーズ書き込みをご覧ください。

脅威モデル

ここでのリスクはSQL インジェクションではありません。 delete_rows、insert_rows、update_rows は構造化された引数(table、where とパラメーター化された params、set オブジェクト)を受け取り、これらの構造化入力に含まれるすべての値は文字列連結ではなく $n プレースホルダーを通じて渡されます(詳しくはツールにある update_rows の注記をご覧ください)。run_migration は異なります。DDL は DML の値のようにパラメーター化できないため、エージェントから渡された生のステートメントを Postgres に直接送信します。これは、query/explain_plan が生の SQL を処理する方法と同じです。安全性はパラメーター化ではなく、行数に関係なく必ず人間の承認を求めることによって確保されます(詳しくはツールをご覧ください)。存在しない $n プレースホルダー保証によるものではありません。エージェントは送信する SQL の作成者であり、信頼できるものの誤りを起こし得る作成者です。引用符をエスケープしようとしているわけではありませんが、構文的に完璧で整形式のステートメントでも、問題はその対象範囲にあるという事態は十分に起こり得ます。たとえば、たまたま 40,000 行に一致する DELETE FROM users WHERE active = false や、エージェントが WHERE 句を付け忘れたため、気付かないうちにそれを省略してしまう UPDATE です。このプロジェクトは、そうした障害に耐えられるよう設計されており、次の 3 つの仕組みがその要となっています。

1. EXPLAIN ではなく、プレビューしてロールバックします。 EXPLAIN が返すのは、ステートメントが変更する行数に関する Postgres のプランナーによる推定値だけです。この推定値は、テーブル統計が古い場合(特に一括ロード直後で、まだ ANALYZE が実行されていない場合)や、プランナーがうまくモデル化できない相関述語がある場合には、単に誤っている可能性があります。推定値を基準にした承認ゲートは、エージェントが(あるいは通常のデータの偏りが)悪意なしに誤って回避できてしまうゲートです。実際の影響行数が 40,000 でも、プランナーが 80 と推定すれば、しきい値を下回ってしまう可能性があります。そのため、ここではすべての書き込みツールが、代わりに実際のステートメントを BEGIN … ROLLBACK 内で実行します。プレビューで示される行数は、推測値ではなく、実際の実行で得られた正確な行数です。EXPLAIN にも役割はあります。独立した explain_plan ツールを使えば、エージェントは 2 フェーズ書き込みを試す前に、安価で副作用のない事前確認を行えます。ただし、承認しきい値との比較に使われることはありません。 2. ロールの分離であり、パースではありません。 readonly と writer は、それぞれ異なる権限が付与された2つの別個の Postgres ロールです(docker/init/01-roles.sql)。readonly が持つのは SELECT のみであり、そのスキーマでは CREATE も明示的に取り消されています。writer には SELECT, INSERT, UPDATE, DELETE が付与されていますが、さらにこのプロジェクト独自の書き込み許可リストによって制限されています。readonly プールを通じて変更を伴うステートメントを送信すると、コードでそう主張しているだけではなく、実稼働データベースで確認済みのとおり(tests/roles.test.ts)、Postgres 自身が permission denied で拒否します。SQL テキストをパースしたり、正規表現で照合したりして「これは書き込みか」を判断する方法は、意図的に安全性の境界として採用していません。パーサーは、想定していなかった形式によって常に回避される可能性があるためです(WITH x AS (DELETE FROM ... RETURNING *) SELECT * FROM x のように CTE でラップされたもの、変更を伴う関数呼び出し、引用符の扱いに関する特殊ケースなど)。この判定を誤れば、見た目だけの不具合ではなく、セキュリティホールになります。query/explain_plan は、ステートメントテキストからテーブル参照を抽出して(src/tools/sqlGuard.ts)、引き続き SELECT 以外のステートメントを拒否し、読み取り許可リストを適用します。ただし、これはロール権限に加える第2の多層防御であり、安全性を担保する本質的な仕組みではありません。引用符付き/Unicode エスケープされた識別子への対応のため、テキストベースの許可リスト解析には複数回の強化が必要でした。その解析に不備があった場合のリスクは、範囲の限られた許可リストの回避であり、「読み取りツールが書き込みを実行する」リスクではない理由も含め、詳しくは DECISIONS.md を参照してください。

3. プランのトークンはステートメントハッシュのフィンガープリントに紐付けられます。 プランのトークンがランダムな不透明 ID にすぎなければ、それによって証明できるのは「ある時点で何らかのプレビューが行われた」ということだけです。どのステートメントがプレビューされたのかはわからないため、トークンだけでは、より広い WHERE 句や別のテーブル、追加の行に差し替えて、同じように見えるトークンを execute_plan に渡すといった、すり替えを防げません。そのため、各トークンは statementFingerprint(statement, params) に紐付けられます。これは、トリミングしたステートメントテキストと JSON シリアライズしたパラメーターリストの SHA-256 ハッシュです。プレビュー時に計算され、execute_plan に実際に渡された内容からも再計算されます。不一致があれば、実行前に STATEMENT_MISMATCH として拒否されます。これにより、localhost 承認 UI での人による承認に意味が生まれます。承認するのは、後から別の SQL に対して再利用される可能性があるトークンではなく、表示されたものとまったく同じステートメントとパラメーターです。(これとは独立した2つ目のチェックである、影響を受けた行数のダイジェストは、同じステートメントでも並行処理によって別の行セットに一致するケースを個別に検出します。詳しくは、以下の2フェーズ書き込みを参照してください。)

このモデルで意図的に対象外としている事項については、制限事項を参照してください。また、これら3つの決定と承認メカニズムのスパイク(#1)の詳細については、DECISIONS.md を参照してください。

クイックスタート

docker compose up -d
npm install
npm test
npm run build

Claude Desktop の接続先をこのサーバーに設定します(config.example.json と以下の Claude Desktop セクションを参照)。node dist/index.js を実行すると、localhost 承認 UI も http://127.0.0.1:4319/ で同時に起動します。すべてのルートでセッションごとのベアラートークンが必要なため、サーバーが stderr に一度だけ出力する ?token=... 付きの完全な URL を開いてください。

設定

config.example.json を config.json にコピーします(または SW_POSTGRES_CONFIG にカスタムパスを設定します)。

{
  "database": {
    "readonlyConnectionString": "postgres://readonly:readonly_password@localhost:5432/mcp_test",
    "writerConnectionString": "postgres://writer:writer_password@localhost:5432/mcp_test"
  },
  "allowlist": {
    "read": { "schemas": ["public"], "tables": [] },
    "write": { "schemas": [], "tables": [] }
  }
}
  • allowlist.read — エージェントが describe_schema/query で参照できるスキーマ/テーブルです。空の場合、すべてのテーブルを読み取れます。tables が空でない場合、完全修飾名のテーブルのみが対象になります。
  • allowlist.write — エージェントが変更できるスキーマ/テーブルです。デフォルトでは拒否されます。schemas と tables がどちらも空の場合、書き込み可能なものはありません。エントリーを明示的に追加してください。
  • write.planTtlMs — plan_token の有効期間(デフォルトは 60000)です。SW_PLAN_TTL_MS で上書きできます。
  • write.statementTimeoutMs — 書き込み実行時の接続ごとの statement_timeout(デフォルトは 10000)です。SW_STATEMENT_TIMEOUT_MS で上書きできます。
  • write.approvalRequiredAboveRows — 正確なロールバックプレビューで影響を受ける行数がこの値以下のプレビューは、これまでと同様に execute_plan が直ちに受け入れるトークンを返します。この値を超える場合、プレビューは代わりに status: "awaiting_approval" を返し、人間が別途承認するまで execute_plan はそのトークンを拒否します(下記の承認しきい値とハード行数上限を参照してください。承認は意図的にエージェント向け MCP ツールではありません)。デフォルトは 100 です。SW_APPROVAL_REQUIRED_ABOVE_ROWS で上書きできます。
  • write.hardMaxRows — もう一つの、より高い独立したしきい値です。正確な影響行数がこの値を超えるプレビューは、完全に拒否されます。トークンは一切発行されず、承認の経路もありません。応答は、エスカレーションで回避できるものではなく、単純な構造化エラー(HARD_MAX_ROWS_EXCEEDED)になります。デフォルトは 10000 です。SW_HARD_MAX_ROWS で上書きできます。write.approvalRequiredAboveRows 以上である必要があります。そうでない場合、loadConfig は例外をスローします。
  • approvalServer.enabled — MCP サーバーとともにローカルホスト承認 UIを起動するかどうか(デフォルトは true)です。SW_APPROVAL_SERVER_ENABLED("true"/"false")で上書きできます。無効にすると、プランを承認する方法がなくなるため、承認が必要となるプレビュー(write.approvalRequiredAboveRows を超えるもの、または run_migration)は、構造化エラー APPROVAL_UNAVAILABLE とともに完全に拒否されます。plan_token は発行されません。期限切れになるまで execute_plan をブロックするだけのプランを発行することもありません。
  • approvalServer.port — ローカルホスト承認 UI が待ち受けるポートです。127.0.0.1 のみにバインドされます(デフォルトは 4319)。SW_APPROVAL_SERVER_PORT で上書きできます。
  • approvalServer.requireAuth — ローカルホスト承認 UI が、読み取り専用の GET ルートを含むすべてのルートで、セッションごとのベアラートークンを要求するかどうか(デフォルトは true)です。ループバックへのバインドに加え、Host/Origin/Sec-Fetch-Site の送信元チェックにより、悪意あるブラウザーページを防ぎます。ただし、想定されるヘッダーを送信するだけの別のローカルプロセスは防げません。以前はプラントークンだけでプランを承認または拒否できましたが、このトークンがそれを制限します(safe-write-mcp-core#20)。推奨されませんが、0.4.0 より前の動作に戻す場合に限り false に設定してください。SW_APPROVAL_SERVER_REQUIRE_AUTH("true"/"false")で上書きできます。
  • approvalServer.authToken — ローカルホスト承認 UI 用の明示的なベアラートークンです。Authorization: Bearer <token> で送信するか、クエリー文字列の ?token= を代替手段として使用します。省略した場合(デフォルト)、サーバーは起動ごとにランダムなトークンを生成し、承認 URL とともに stderr に一度だけ出力します。ページ上の Approve/Reject ボタンにはすでにトークンが付与されています。固定トークンが必要な場合(スクリプトからのアクセスなど)に設定してください。SW_APPROVAL_SERVER_AUTH_TOKEN で上書きできます。
  • callerId — すべての監査ログ行で caller_id として記録される識別子(デフォルトは "unknown")です。SW_CALLER_ID で上書きできます。監査ログを参照してください。
  • 環境変数 DATABASE_URL_READONLY/DATABASE_URL_WRITER はファイルの設定を上書きします。 設定はサーバー起動時に一度だけ読み込まれます。実行中のサーバーが config.json を再読み込みすることはありません。編集した後は、MCP サーバーを再起動してください(Claude Desktop/MCP クライアントを終了して再度開きます)。そうしないと、古い設定が引き続き適用されます。起動時、サーバーは読み込んだ設定ファイルと有効な書き込み許可リストを標準エラー出力に記録します。設定ファイルが見つからない場合は、警告を明確に出力します。ファイルがないと書き込み許可リストが空になり、デフォルトで拒否されるため、すべての書き込みが拒否されます。

Postgres のロールが異なる 2 つの接続プール(readonly と writer)が作成されます。読み取り専用の制約は SQL の解析ではなく、データベースの権限付与によって適用されます。そのため、コードにバグがあっても、読み取りツールが書き込みツールに変わることはありません。

Claude Desktop

claude_desktop_config.json に追加します。

{
  "mcpServers": {
    "sw-postgres-mcp": {
      "command": "node",
      "args": ["/absolute/path/to/sw-postgres-mcp/dist/index.js"],
      "env": {
        "SW_POSTGRES_CONFIG": "/absolute/path/to/sw-postgres-mcp/config.json",
        "DATABASE_URL_READONLY": "postgres://readonly:readonly_password@localhost:5432/mcp_test",
        "DATABASE_URL_WRITER": "postgres://writer:writer_password@localhost:5432/mcp_test"
      }
    }
  }
}

MCP クライアントは、プロジェクトディレクトリを作業ディレクトリとしてサーバーを起動しないため、デフォルトの ./config.json の検索ではファイルが見つからず、気づかないまま設定が読み込まれません。そのため、SW_POSTGRES_CONFIG を明示的に設定しています。

Claude Desktop を再起動します。「このデータベースには何がありますか?」と尋ねると、describe_schema が、許可リストに登録されたスキーマ/テーブルのみを対象に、テーブル、型付きの列、外部キー、行数の推定値を返します。

Docker

docker compose up を実行すると、docker/init/01-roles.sql によって両方のロールが設定された使い捨ての Postgres(postgres:16-alpine)が起動します。また、docker/init/02-audit-log.sql によって監査スキーマが作成され、承認ワークフロー用に docker/init/03-approval-workflow.sql によって status 列挙型が拡張されます。テストやローカル開発で手動のセットアップは不要です。

デモ用データベース

docker compose up -d --wait
npm run seed:demo

空のデータベースに、合成した EC サイト用データセット(customers、products、orders、order_items、合計約 208,000 行)を投入します。これにより、実際の本番データセットがなくても、describe_schema/query/書き込みツールで扱える現実的なデータが用意されます。スキーマ(docker/init/03-demo-schema.sql)は使い捨ての Docker Postgres に自動で適用されます。通常のローカル Postgres では npm run seed:demo がスキーマも適用するため、どちらの場合も手動でマイグレーションする必要はありません。データ生成は決定論的です。シード付き PRNG(Math.random() ではなく mulberry32)を使うため、同じ --seed からは常にまったく同じ行が生成されます。

npm run seed:demo -- --seed=7
npm run seed:demo -- --connection="postgres://user:pass@host:5432/db"

接続先の解決には、プロジェクト内の他の箇所と同じ優先順位が適用されます。--connection フラグ > DATABASE_URL_WRITER > POSTGRES_WRITER_URL > DATABASE_URL > docker-compose の writer デフォルト。実行のたびに 4 つのデモ用テーブルを切り詰めて再生成するため、データがすでに入っているデータベースに対しても安全に再実行できます。

行数の内訳(デフォルトのシード):

グループ行数備考
customers50,000
products2,000
orders60,000
order_items約 96,0001 件の注文につき 1~4 件のアイテム
非アクティブな顧客(last_login < 2025-01-01)40,000顧客全体の約 80%
テストテナント(customers.segment = 'test_tenant')8 人の顧客/320 件の注文非アクティブな顧客の範囲内に完全に含まれる、小規模で限定的にクエリ可能なテナント
orders.status = 'cancelled'13,20010,000 行のハード上限を超えるため、ハード上限による拒否のテストに使用できます

テスト

docker compose up -d --wait
npm test

統合テストでは、稼働中の Postgres に対して、ロールの分離、読み取り専用ロールでは書き込みができないこと、describe_schema のフィールド、許可リストによるフィルタリング、デモデータベースのシーダーが生成する行数の形式を検証します(tests/seedDemo.test.ts は実際に npm run seed:demo を実行し、結果をクエリで取得します)。tests/approvalUi.test.ts は、ローカルホストの承認 HTTP サーバーを実際に(OS が割り当てたポートで)起動し、セッションごとのベアラートークンを付けた通常の fetch() で操作します。ブラウザーの自動化は使用せず、承認待ちプランの一覧表示、承認/拒否 HTTP エンドポイントのエンドツーエンドの動作(ロック解除と execute_plan の恒久的な無効化を含む)、ループバック限定のバインド、ベアラートークンによるゲート(401、?token= フォールバック、requireAuth: false によるオプトアウト、403 が 401 より先に返される順序)、監査行、期限切れプランのフィルタリング、MCP クライアントがまったく接続されていなくてもこのインターフェースが動作することを対象とします。tests/insertRows.test.ts と tests/updateRows.test.ts では、2 つの新しい書き込みツールについて、同じプレビュー→トークン→実行の手順、承認しきい値/ハード上限、書き込み許可リスト、監査ログ、SQL インジェクションを模した入力、さらに(挿入の場合は)シーケンス番号の欠落動作をテストします。tests/writeStatements.test.ts では、2 つの DELETE/UPDATE ツールで共有される WHERE 句なしの操作を防ぐガードを単体テストします。tests/runMigration.test.ts は、同じ実際のローカルホスト承認サーバーを介して run_migration を実行し(TwoPhaseWrite を直接呼び出すことはありません)、行数のしきい値が決して参照されないこと、拒否が PLAN_REJECTED として execute_plan に届くこと、複数文の入力と許可リスト外の対象がデータベースに触れる前に拒否されることを検証します。また、稼働中の Postgres に対して、CREATE TABLE、ALTER TABLE、DROP TABLE、CREATE INDEX がすべて、プレビュー後に正しくロールバックされ、実行後に正しくコミットされることも検証します。さらに、src/tools/ddlTarget.ts の文の対象抽出を直接単体テストします。

安全性の検証

上記の各テストファイルでは、それぞれが導入したツールについて、そのチケットで追加したガードが機能することを検証します。一方、tests/safetyCase.test.ts では、固定された安全性プロパティのマトリックス(しきい値超過、ハード上限による拒否、期限切れ/再利用/改変/拒否されたトークン、許可リストの適用、読み取り専用ロール、監査証跡の完全性、監査ログの不変性、プレビューによって痕跡が残らないこと)を実行し、各プロパティが該当するすべての書き込みツールを検証します。ツールごとのテストを複製するのではなく、共通の it.each 駆動型ヘルパーを使うことで、あるツールには組み込まれたガードが別のツールでは見落とされ、機能しないままになる事態を特に検出します。1 つのブロックでは、npm run seed:demo が使用するものと同じ決定論的ジェネレーターで生成したデータセットに対して、しきい値/ハード上限のテストを実行します(issue #10)。これにより、小規模な合成件数だけでなく、現実的なシードデータの規模でもガードが機能することを実証します。

ツール

  • describe_schema — テーブル、型付きのカラム、外部キー、行数の推定値を返します(読み取り許可リストを適用します)。
  • query — 読み取り専用の SELECT を実行し、{ columns, rows, row_count } を返します。読み取り専用ロールで実行されるため、SQL の記述にかかわらず、変更を伴うステートメントはデータベースによって拒否されます。1 回の呼び出しにつき 1 つのステートメントのみを許可し、読み取り許可リストを適用します。limit と params は任意です。
  • explain_plan — 実行候補の読み取りステートメントに対して EXPLAIN (FORMAT JSON) を実行し、実際には実行せずに、プランナーが推定した cost と rows を返します。高コストになる可能性のある処理を実行する前に、低コストで事前確認できます。
  • delete_rows — 2 段階の削除です。トランザクション内でステートメントを実行し、影響を受ける正確な行数と影響を受ける行のサンプルを返した後、ロールバックします。レスポンスには plan_token、正確な statement、params が含まれます。また、承認しきい値と行数のハード上限で説明するように、status は previewed または awaiting_approval になります。confirm_full_table: true が渡されない限り、WHERE 句のないステートメントは拒否します。
  • insert_rows — 2 段階の挿入です。table、カラム名の配列である columns、各行の値の配列を格納した配列であり、columns の順に対応する rows、および reason を受け取ります。トランザクション内で INSERT ... VALUES (...), (...) RETURNING * を実行し、正確な行数と挿入される行のサンプルを返した後、ロールバックします。知っておくべき副作用については、以下のシーケンス値とロールバックされた挿入を参照してください。例:
    {
      "table": "customers",
      "columns": ["email", "active"],
      "rows": [
        ["[email protected]", true],
        ["[email protected]", false]
      ],
      "reason": "seeding two test accounts"
    }
    
  • update_rows — 2 段階の更新です。table、変更内容を示す { "column": value, ... } オブジェクトの set、delete_rows と同じ規則に従うパラメーター化された WHERE 条件の where と params、confirm_full_table、および reason を受け取ります。トランザクション内で UPDATE ... SET ... WHERE ... RETURNING * を実行し、影響を受ける正確な行数と更新後のサンプルを返した後、ロールバックします。confirm_full_table: true が渡されない限り、WHERE 句のないステートメントは拒否します。これは delete_rows が使用するものとまったく同じガードであり(src/tools/writeStatements.ts)、再実装ではありません。例:
    {
      "table": "customers",
      "set": { "active": false },
      "where": "last_login < $1",
      "params": ["2025-01-01"],
      "reason": "deactivating accounts inactive since before 2025"
    }
    
    set 内のカラム名は常に引用符で囲まれた識別子として扱われ、値は常に $n パラメーターとして扱われます。ステートメントに文字列連結されることはないため、細工されたカラム名や値によって SQL がインジェクションされることはありません。不正なカラム名は、単に未知のカラムとしてエラーになります。
  • run_migration — 2 段階の DDLです。statement(単一の CREATE TABLE、ALTER TABLE、DROP TABLE、または CREATE [UNIQUE] INDEX ... ON <table> ステートメント)と reason を受け取ります。トランザクション内で実行してからロールバックします。Postgres の DDL はトランザクションに対応しているため、問題なくロールバックできます。必ず人間による承認が必要です。すべての run_migration のプレビューは、例外なく常に status: "awaiting_approval" で返されます。このツールでは、マイグレーションがどれほど小規模に見えても、行数が関係しないように見えても、write.approvalRequiredAboveRows/hardMaxRows は参照されません(以下のマイグレーションは常に承認が必要を参照)。DDL には RETURNING に基づく行数がないため、レスポンスの affected_rows/sample_rows は常に 0/[] です。これは偽の件数ではありません。代わりに、ステートメントから抽出したスキーマ修飾付きのテーブル/インデックス(例:"public.customers")を示す target フィールドが含まれ、人間が具体的な対象を判断できます。複数のステートメントを含む入力(例:セミコロンで区切られた 2 つの CREATE TABLE)は、データベースに到達する前に拒否されます。例:
    {
      "statement": "ALTER TABLE customers ADD COLUMN loyalty_tier text",
      "reason": "adding a column the new loyalty feature needs"
    }
    
  • execute_plan — 以前にプレビューした書き込みをコミットします。プレビューのレスポンスから plan_token、statement、params(delete_rows、insert_rows、update_rows、または run_migration のもの)を渡します。プランがまだ承認待ちの場合、人間がlocalhost の承認 UIで承認するまで呼び出しはブロックされ、その後実行されます。人間が拒否した場合は、人間の理由を含む構造化された PLAN_REJECTED エラーを返します。プランの有効期限が切れた場合は EXPIRED_TOKEN を返します。 この一覧には、意図的に approve_plan や reject_plan(その他の承認用ツールも含む)はありません。awaiting_approval 状態のプランを承認または却下する機能は、エージェントには公開されていません。その理由と、代わりに想定されている使い方については、以下のローカルホストの承認 UIを参照してください。

すべてのツールは reason 文字列を受け取ります(監査ログに記録されます。詳細は以下を参照)。エラーは構造化された { code, message, hint } 形式で返され、Postgres の例外がそのまま返されたり、複数ステートメントのバッチが返されたりすることはありません。

2 フェーズ書き込み

delete_rows、insert_rows、update_rows、run_migration はすべて、まったく同じコア(src/writeCore.ts の TwoPhaseWrite)を経由します。エージェントは、実行する前にプレビューを確定する必要があります。

  1. ツールはトランザクション内でステートメントを実行し、RETURNING を使って、影響を受けた行または挿入された行の正確な件数とサンプルを取得したあと、ロールバックします(run_migration については、DDL には取得可能な RETURNING がないため、以下を参照してください)。データベースには何も変更されません。
  2. execute_plan は同一のステートメントを再実行してコミットします。ただし、トークンが有効で、有効期限内で、未使用であり、プレビュー時のステートメントとパラメーターに正確に紐づいている場合に限ります。delete_rows/update_rows では、プレビュー以降に 一致した 行セットが変化していた場合もコミットを拒否します(ROWSET_CHANGED)。プランがまだ承認待ちの場合、execute_plan はトークンの状態が決まるまで待機します。人間が別の手段で承認すると実行され、却下すると PLAN_REJECTED、有効期限が切れると EXPIRED_TOKEN を返します。詳細は以下を参照してください。

WHERE 句のない DELETE/UPDATE は、confirm_full_table: true が渡されない限り拒否されます。このガードは両方のツールで共有されており(src/tools/writeStatements.ts)、ずれが生じる可能性のある重複実装ではありません。INSERT には WHERE 句がないため、このガードは insert_rows には適用されません。すべての書き込みは writer プールを経由し、ミューテーションに readonly プールが使われることはありません。

ROWSET_CHANGED チェックは、insert_rows では意図的に省略されています。このチェックは、「プレビューと実行の間に、WHERE 句に一致する行が変化した」ことを検出するためのもので、INSERT に相当するものはありません。一致させる既存の行セットがないためです。それ以外にコアが提供する機能は、insert_rows にもすべて適用されます(正確な statementFingerprint への紐付け、使い捨てで有効期限のあるトークン、承認のしきい値と上限、完全な監査ログ)。サーバー生成列のあるテーブルに対して INSERT のプレビューと実行の RETURNING ダイジェストを比較すると、すべての呼び出しで書き込みが拒否されてしまう理由については、DECISIONS.md を参照してください。

run_migration の DDL ステートメントも、同じ根本的な理由(照合対象となる既存の行セットがない)から同じチェックを省略しますが、さらに強い理由があります。DDL は RETURNING 句をまったくサポートしていないため、そもそもダイジェストも、影響を受けた行数も、サンプル行も計算できません。run_migration はプレビュー時の BEGIN/ROLLBACK 内で生のステートメントを実行します(Postgres の DDL はトランザクション対応のため、CREATE TABLE/ALTER TABLE のプレビューも DELETE/UPDATE のプレビューと同様にロールバックされます)。そして affected_rows: 0、sample_rows: [] を報告します。これは実際の件数の代わりではなく、返す行がないステートメントに対する正確な結果です。さらに、人間が承認を判断する際の具体的な情報として、ステートメントから抽出したスキーマ修飾済みのテーブル名またはインデックス名を示す target フィールドも返します。以下のマイグレーションには常に承認が必要と DECISIONS.md を参照してください。

シーケンス値とロールバックされた挿入

insert_rows のプレビューは、実際の INSERT ... RETURNING * を実行した後にロールバックするため、対象テーブルの列にある serial/identity/その他のシーケンスに基づくデフォルト値は引き続き進みます。Postgres のシーケンスはトランザクション対象ではないため、ロールバックしても消費されたシーケンス値は戻りません。つまり、次のようになります。

  • 挿入をプレビューすると(実行しない場合も含めて)、その列のシーケンス値が1つ以上恒久的に消費されます。
  • プレビューの sample_rows に表示される id(または同様の値)は参考値であり、保証されるものではありません。プレビュー自体のロールバックですでに表示値が消費されているため、execute_plan で実際にコミットされる行には、別のシーケンス生成値が割り当てられる可能性が非常に高くなります。
  • プレビューが実行されたかどうかにかかわらず、プレビューだけで長期的にはシリアル列に欠番が生じることがあります(例:1, 2, 3, 4 ではなく 1, 2, 5, 6)。これは問題ありません。このツールがなくても、Postgres のシーケンスが欠番のない値を保証したことはありません。ただし、シリアル列を連続したカウンターとして扱う前に知っておくとよい点です。
承認しきい値とハード行数上限

プレビューで得られる正確なロールバックプレビューの影響行数(EXPLAIN による推定値ではありません。write.approvalRequiredAboveRows と write.hardMaxRows は、実際にロールバックされた行数との比較にのみ使用されます)によって、同じツール呼び出しで次に行われる処理が決まります。

正確な影響行数結果
<= approvalRequiredAboveRows(デフォルトは100)変更なし:status: "previewed"。トークンはすぐに execute_plan で使用できます
> approvalRequiredAboveRows、かつ <= hardMaxRowsstatus: "awaiting_approval" — トークンとサンプル行が返され、担当者が別経路で承認するまで execute_plan はトークンを保留します(承認後に実行)。担当者は拒否することもでき(PLAN_REJECTED)、プランが期限切れになることもあります(EXPIRED_TOKEN)(下記参照)
> hardMaxRows(デフォルトは10,000)無条件に拒否されます — plan_token は発行されず、delete_rows 自体が構造化された HARD_MAX_ROWS_EXCEEDED エラーを返します。これはゲートではなく壁です。承認する方法はなく、エージェントには再度尋ねるのではなく、影響行数が少なくなるようにステートメントを書き直すことが求められます

承認メカニズム(#6) プレビューによって発行されたプラントークンには、既存のフィンガープリント/有効期限/1回限りの使用状態とともに、内部フラグ requiresApproval/approved/rejected が保持されます。これらはインメモリのプランストア(src/writeCore.ts 内で TwoPhaseWrite が保持する safe-write-mcp-core の PlanStore)に保存されます。承認すると approved が true に切り替わり、approved の監査行が書き込まれます。拒否すると rejected が永続的に設定され、rejected の監査行が書き込まれます(プログラムから呼び出す場合のエントリーポイントは TwoPhaseWrite.approvePlan()/rejectPlan() です)。どちらも MCP ツールとして公開されていません — エージェント向けのツールには approve_plan/reject_plan(またはその他の承認)エントリーはありません。承認を必要とする書き込みを要求したエージェント自身が、自分のプランを承認できてはならないためです。詳しくは DECISIONS.md を参照してください。承認専用のデータベーステーブルがないのは意図的です。プラントークンは execute_plan の対象範囲に適した単位であり、2フェーズ書き込みの残りの状態(フィンガープリント、有効期限、1回限りの使用)と同じ場所に保存されます。localhost approval UI(#7)が唯一の承認手段であり、エージェント以外の独自の HTTP インターフェースから同じストアを直接操作します。

マイグレーションには常に承認が必要です

run_migration (#9) は上記の表を使用しません。行数のしきい値は一切参照されず、すべての run_migration プレビューは、affected_rows が 0 で approvalRequiredAboveRows にまったく届かないマイグレーションであっても、例外なく status: "awaiting_approval" を返します。これは意図的な仕様です。行に触れないマイグレーション(列の追加、行のないテーブルの削除、インデックスの作成)でも、このサーバーが実行する処理の中で最も破壊的なものになり得ます。DELETE/UPDATE/INSERT が「どれだけのものが危険にさらされているか」の指標として使う行数は、スキーマ変更には当てはまらないため、行数を基準に扱うことはありません。

仕組みとしては、src/tools/runMigration.ts がすべての TwoPhaseWrite.preview() 呼び出しで alwaysRequireApproval: true を渡します。WriteMeta のフィールドである preview() は、この値を、ほかのツールで行数のしきい値によって決まるものと同じ requiresApproval の判定に OR 条件で加えます(src/writeCore.ts)。これはツールモジュール内のコードにハードコードされており、run_migration の引数から読み取られるものではありません。MCP の inputSchema(src/server.ts)にこれを受け取るフィールドはないため、呼び出し元のエージェントには、この要件を弱めたり回避したりするパラメーターがありません。また、write.approvalRequiredAboveRows/hardMaxRows の設定ミスによって、マイグレーションが誤って承認なしで実行されることもありません。そもそも、このフラグはどちらの設定も参照しないためです。さらに、run_migration は delete_rows/insert_rows/update_rows と同じく、ほかの承認メカニズムの保証もすべてそのまま受けられます。承認または却下できるのはlocalhost の承認 UIのみで、却下された場合、次回の execute_plan の試行時に構造化された PLAN_REJECTED エラーが返されてトークンは永久に無効になります。また、すべてのプレビュー/承認/実行/却下は、エージェントの reason とともに監査記録に残されます。

run_migration は preview() を呼び出す前に、書き込み許可リストも適用します。ステートメントの対象テーブル(DROP TABLE の場合は、ステートメントに含まれるすべてのテーブル)がステートメントのテキストから抽出され、データ書き込みツールとまったく同じ isTableWritable のロジックで確認されます。サポート対象の DDL 形式と、現在 DROP INDEX がサポートされていない理由については、src/tools/ddlTarget.ts と DECISIONS.md を参照してください。

localhost 承認 UI

小さなプレーン HTML ページです。React もビルド手順もなく、承認しきい値を超える操作をエージェントが実行しようとしているとき、人間が内容を確認して承認するか拒否するかを選べます。MCP の stdio トランスポートとは別の、ローカル専用 HTTP サーバー(src/approvalServer.ts)として動作します。src/index.ts によって MCP サーバーと同時に起動され、127.0.0.1 のみにバインドされます(0.0.0.0 には決してバインドされません。詳しくは DECISIONS.md を参照してください)。MCP サーバーと同じメモリ内 TwoPhaseWrite インスタンスを共有するため、ここでの承認または拒否は、MCP 接続上の execute_plan に直ちに反映されます。

  • アクセス: 起動時にサーバーが stderr に一度だけ出力する完全な URL(http://127.0.0.1:4319/?token=<per-session-token>。または、設定された approvalServer.port の値)を、サーバーが動作しているマシンのブラウザーで開きます。他のマシンからはアクセスできません。意図的に、ホスト/バインドアドレスの設定オプションはありません。すべてのルートで bearer token が必要です(0.4.0、safe-write-mcp-core#20)。Authorization: Bearer <token> として送信するか、貼り付けた URL にすでに含まれている ?token= のクエリ文字列フォールバックを使用してください。ページ上の[承認]/[拒否]ボタンは、自動的にヘッダー経由でトークンを送信します。Host/Origin/Sec-Fetch-Site の出所チェックに失敗したリクエストは、トークン確認前に 403 を返します。これらのチェックを通過しても、トークンがない(または誤っている)場合は 401 UNAUTHORIZED を返します。GET /api/plans への curl/fetch() リクエストにもヘッダーが必要です。例:curl -H "Authorization: Bearer <token>" http://127.0.0.1:4319/api/plans。認証を無効にするには approvalServer.requireAuth: false を設定します(推奨しません)。生成されたトークンの代わりに固定トークンを使うには approvalServer.authToken を設定します。
  • 表示内容: 現在 awaiting_approval 状態にあるすべてのプランについて、ツール、正確なステートメント、エージェントが指定した reason、ロールバックプレビューでの正確な影響行数、影響を受ける行のサンプル(プレビューの sample_rows)、およびプレビューから抽出できた場合(現時点では run_migration のみ)は、ステートメントの適用先であるスキーマ修飾されたテーブル/インデックスを示す target を表示します。これは、DDL マイグレーションで人間が行数の代わりに確認する項目です。有効期限が切れたプランは、この一覧から消えるため、承認可能な状態で残り続けることはありません。機械可読な同等の情報は GET /api/plans(JSON)で取得でき、通常の fetch()/curl でアクセスできます。ブラウザーも MCP クライアントも不要です。tests/approvalUi.test.ts および tests/runMigration.test.ts の受け入れテストでも、この方法で確認しています。
  • 承認(POST /api/plans/:token/approve、任意の { approvedBy } ボディ)すると、MCP ツールを介さずに、インプロセスでメモリ内のプランストア上のプランを直接承認します。その直後から、そのトークンでの execute_plan が成功します。
  • 拒否(POST /api/plans/:token/reject、任意の { rejectedBy, reason } ボディ)すると、ストア上でトークンは恒久的に無効になります。その後は、後から[承認]をクリックしても、2 回目に[拒否]をクリックしても、承認や実行はできません(どちらも安全に冪等に処理され、再監査以外は何も行いません)。プランの承認待ち中は execute_plan がブロックされるため、拒否すると実行中の呼び出しに即座に反映されます。ブロックされていた execute_plan は、他と区別可能な構造化 PLAN_REJECTED エラーを返します(AWAITING_APPROVAL、EXPIRED_TOKEN、汎用エラーではありません)。人間が拒否理由を指定していた場合、エラーメッセージにはその理由が含まれるため、エージェントは無闇に再試行するのではなく、具体的な対応としてステートメントを絞り込んで再プレビューできます。
  • 監査: 承認と拒否のたびに、mcp_audit.log に 1 行書き込みます(status: "approved"/"rejected"、approved_by は approvedBy/rejectedBy の値を設定し、既定値は "unknown")。MCP 経由のプレビュー/実行ですでに書き込まれているものと同じテーブルおよびカラムを使用します。
  • セキュリティ境界: 承認/拒否が可能なのはこの HTTP インターフェース経由のみで、接続中のエージェントが呼び出せる MCP ツール経由ではできません。approve_plan についてチケット #6 で修正された自己承認の抜け穴は reject_plan にも同様に当てはまるため、どちらも src/server.ts の MCP ツール一覧には含まれていません。

監査ログ

2 フェーズ書き込みコアが処理するプレビュー、承認、実行、拒否はすべて、mcp_audit スキーマの mcp_audit.log に 1 行ずつ記録されます。

カラム意味
id, ts行 ID とタイムスタンプ
tool書き込みを実行した MCP ツール(例:delete_rows)
reason呼び出し元が指定した reason 文字列
statement正確な SQL 文(スキーマ/テーブルは許可リストと照合済み)。approve_plan/reject_plan の行では空です(これらの tool には元の書き込みツールではなく、そのままの文字列が設定されます)。これらの行は文を再記述せず、plan_token でプランを参照します
params_redactedリテラル値ではなく、形状です。パラメーターごとに { type, length } を記録するため、オペレーターは渡されたパラメーターの数とおおよその種類を確認できますが、ステートメントに含まれる顧客のメールアドレス、トークン、その他のリテラル値は記録されません
preview_rowsプレビュー時点で取得された影響行数(正確なロールバックプレビューの件数であり、EXPLAIN による推定値ではありません)
actual_rows実行時に実際にコミットされた影響行数(実行が成功するまでは null)
plan_token, approved_bypreviewed/awaiting_approval の行と、後続の approved/rejected/executed/failed の行を関連付けます。approved_by は approved 行に設定されます(TwoPhaseWrite.approvePlan() または承認 UI の承認エンドポイントに渡された approvedBy に基づきます)。同じ「このトークンに対して誰が操作したか」という目的で、rejected 行にも設定されます(rejectPlan() または拒否エンドポイントに渡された rejectedBy に基づきます)。いずれも ID が指定されていない場合のデフォルトは "unknown" です
statuspreviewed | awaiting_approval | approved | executed | rejected | hard_cap_refused | failed — awaiting_approval と hard_cap_refused については承認のしきい値とハード行数上限、approved/rejected についてはローカルホストの承認 UIを参照してください
duration_msデータベースとの往復にかかった実時間
caller_idサーバーインスタンス/デプロイを識別します(config.callerId、環境変数 SW_CALLER_ID、デフォルトは "unknown")。v1 にはリクエストごとのエンドユーザー認証がないため、個人ではなくデプロイに帰属します

監査行の書き込みが、それが記述する書き込みの結果を妨げたり覆い隠したりすることはありません。監査行の挿入に失敗した場合(例:一時的な接続障害)は、stderr に記録して握りつぶし、例外を送出しません。そのため、監査行の欠落が、実際に失敗したデータベース書き込みと混同されることはありません。

追記専用の保証は、アプリケーションコードではなく Postgres によって強制されます。 docker/init/02-audit-log.sql(コミット済みのマイグレーション。使い捨ての Docker テストデータベースと、その他の接続先 Postgres の両方に適用されます)は、writer ロールに mcp_audit.log への INSERT のみを許可し、UPDATE、DELETE、TRUNCATE は明示的に REVOKE します。

GRANT INSERT ON mcp_audit.log TO writer;
REVOKE UPDATE, DELETE, TRUNCATE ON mcp_audit.log FROM writer;

このサーバーのバグであっても、writer ロールを通じてエージェントが作成できる SQL であっても、いったん書き込まれた行を書き換えたり消去したりすることはできません。Postgres が UPDATE/DELETE を permission denied として明示的に拒否します。これは文書に記載されているだけでなく、Postgres に対する実際のテスト(tests/auditLog.test.ts)で検証されています。readonly には SELECT のみが許可されているため、オペレーターはログを読み取れますが、書き込むことはできません。

制限事項

隠さず、率直に記載します。

  • ロールバックで取り消されるのはテーブルへの書き込みであり、外部への副作用ではありません。 delete_rows/insert_rows/update_rows/run_migration のプレビューは実際にロールバックされます。ただし、対象テーブルの BEFORE/AFTER トリガーが NOTIFY を発行したり、外部サービスを呼び出したり、外部データラッパー(FDW)を通じて別のデータベースに書き込んだりした場合、その副作用はすでに発生しており、プレビューの ROLLBACK では取り消されません。Postgres のトランザクションが保証するのは、トランザクション対象のテーブルの状態が元に戻ることだけです。データベースの外部にすでに送信されたメッセージを取り消すことはできません。エージェントが書き込めるテーブルにそのようなトリガーがある場合、プレビューのステップ自体にも副作用があるものとして扱ってください。無料で実行できるわけではありません。
  • ロールバックされた挿入(およびプレビューされた DDL)でもシーケンス値は消費されます。 Postgres のシーケンスはトランザクション対象ではないため、ロールバックされた insert_rows のプレビュー、または CREATE TABLE ... GENERATED ... AS IDENTITY 形式の DDL のロールバックされたプレビューによって、対象の列のデフォルト値の生成元である serial/identity シーケンスは恒久的に進みます。そのため、実行されたかどうかにかかわらず、プレビューだけが原因で serial 列に値の欠番が生じることがあります。実害はありません(そもそも Postgres のシーケンス値は欠番がないことを保証していません)が、知っておくとよいでしょう。上記の[シーケンス値とロールバックされた挿入](#sequence-values-and-rolled-back-inserts)を参照してください。
  • 監査ログの追記専用の保証は、このサーバーのコードではなく writer ロールへの権限付与によるものであり、そのロールを経由する接続にのみ適用されます。 docker/init/02-audit-log.sql は mcp_audit.log に対して writer に INSERT のみを許可し、UPDATE/DELETE/TRUNCATE を取り消します。上記の[監査ログ](#audit-log)を参照してください。ただし、スーパーユーザー接続はこのモデルの対象外です。Postgres のスーパーユーザー、テーブルの所有者、または mcp_audit.log に対する UPDATE/DELETE/TRUNCATE の権限を持つ(または付与できる)ロールは、テーブルの所有権、直接付与された権限、あるいは writer ロール自体に対する ALTER/GRANT 権限を通じて、writer 自身の権限設定にかかわらず監査行を書き換えたり消去したりできます。保証されるのは「このサーバーが、設定された認証情報を使用する場合、自身の監査履歴を改ざんできない」ということであり、「データベース管理者から監査履歴を改ざんされない」ということではありません。
  • マルチテナントには対応していません。 1 つのサーバープロセスが、1 組の readonly/writer ロールと 1 つの許可リストを使って、1 つの Postgres データベースと通信します。テナントごとの認証情報のスコープ設定や監査ログの分割には対応していません。複数のテナントで実行するには、個別の設定を用意して複数のサーバーインスタンスを実行する必要があります。
  • ローカル設定以外の認証には対応していません。 リクエストごとのエンドユーザー認証はありません。caller_id([監査ログ](#audit-log)を参照)が識別するのはデプロイ環境/設定であり、MCP セッションの背後にいる個人ではありません。このサーバーの stdio トランスポートと通信できる人、または承認に関しては、実行マシン上の 127.0.0.1 に接続できる人なら誰でも、設定されたロールとして利用できます。
  • v1 ではクラウドへのデプロイに対応していません。 localhost の承認 UI は意図的に 127.0.0.1 にバインドされているため([localhost の承認 UI](#localhost-approval-ui)を参照)、別のマシンからはアクセスできません。リモート承認の仕組みも、TLS 終端も、複数ユーザーへの承認の振り分けもありません。これはホスト型サービスとしてではなく、1 台のマシン上でエージェントの隣で実行するように設計されています。
  • プランのトークンと承認状態はメモリ上に保持され、プロセスの存続期間に限定されます。 TwoPhaseWrite のトークンストアは監査ログ以外に永続化されません。サーバーを再起動すると、保留中の awaiting_approval プランはすべて失われ(最初からプレビューし直す必要があります)、各プランの承認状態も失われます。再起動後も mcp_audit.log に残るのは、プレビュー/承認/実行された内容の履歴のみです。「現時点で execute_plan がこのトークンを受け付けるか」という実行時の状態は残りません。これが見落としではなく意図的な選択である理由については、DECISIONS.md (#6) を参照してください。
  • run_migration の許可リストによる強制適用は、あらゆる DDL 形式を対象としているわけではありません。 CREATE TABLE、ALTER TABLE、DROP TABLE、CREATE [UNIQUE] INDEX ... ON <table> はサポートされています。一方、単独の DROP INDEX <name> は、インデックスが属するテーブルをその文のテキストだけから特定できないため、許可リストの確認を行わずに拒否され(UNSUPPORTED が返され)ます。DECISIONS.md (#9) を参照してください。
  • 単一の文、単一のデータベースに限られます。 すべてのツールは複数の文を含む入力を拒否します。また、複数のツール呼び出しにまたがる文間/テーブル間のトランザクションもありません。各 delete_rows/insert_rows/update_rows/run_migration の呼び出しは、それぞれ独立したプレビュー/実行のペアです。2 つの関連する書き込み(たとえば、UPDATE とそれに依存する INSERT)をまとめてプレビューし、1 つの不可分な単位として承認する方法はありません。

MCP レジストリへの公開

server.json は、公式の MCP レジストリ に登録するコミット済みマニフェストです。tests/serverJson.test.ts では、レジストリのスキーマ URL と主要なマニフェストプロパティ(逆 DNS 形式の名前、バージョン、npm パッケージのエントリー、stdio トランスポート、必須の環境変数)が含まれていることを確認し、package.json との同期も確認します。name は package.json の mcpName フィールドと一致している必要があります。このフィールドは、レジストリが公開時に確認する npm の所有権検証マーカーです。

公開は CLI で行い(GitHub PR ではありません)、npm パッケージがすでに存在している必要があります。

  1. npm に公開します:npm run build && npm publish(issue #24 を参照)。
  2. パブリッシャーをインストールします:brew install mcp-publisher を実行するか、modelcontextprotocol/registry からリリースバイナリをダウンロードします。
  3. 認証します:mcp-publisher login github(デバイスフロー)。
  4. 公開します:mcp-publisher publish — server.json を読み込み、サーバーを登録します。

マニフェストの name(io.github.jpka/sw-postgres-mcp)は GitHub アカウントの名前空間に含まれているため、GitHub 認証だけで十分です。DNS チャレンジは必要ありません。宣言されている環境変数 DATABASE_URL_READONLY / DATABASE_URL_WRITER は、設定セクションで説明している接続文字列と同じものです。

ライセンス

MIT

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。