0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに渡す前の「データ棚卸し」Webアプリをつくってみた:MCPの接続URLづくりと、AIから見えるテーブルの一覧

0
Last updated at Posted at 2026-10-23

対象: 前の記事でMCPの「読み取り専用」を試して、「チームで使うとき、渡す前に何を確認すればいいの?」が気になった方
所要時間: 40〜60分 / 使うもの: ブラウザ、ターミナル、Supabaseのプロジェクト(Freeプランで可)
確認日: 2026年10月(画面や料金は変わることがあります)
前の記事(手順編): https://qiita.com/tomozilla/items/3fd871d70b141b56cd7e
ビジネス編(やさしい解説・note): https://note.com/tomozilla/n/nbc7a8c57296c
サンプルコード一式(GitHub): https://github.com/tomozilla/mcp-least-privilege-demo
記事の中のコードは日本語コメントです。リポジトリの本体は英語コメントで、日本語コメント版(この記事と同じもの)は ja/ フォルダーにあります。

前の記事では、「読み取り専用」のロールが、書き込みは止めるけれど、見える範囲は狭めないことを、SQL Editorで確かめました。

でも、それをチームの人に伝えるのは大変です。「SQLで確かめたら、こうでした」では、AIツールの導入を決める人には届きません。そこで、AIに鍵を渡す前に見る1枚の画面をつくってみました。使うのは、セキュリティ担当や、チームのリーダー、情報システムの人を想像しています。

この画面は2枚で、1つは「棚卸し」(このデータベースで、何がAIから読めてしまうか)、もう1つは「接続URLづくり」(鍵をどこまで狭くして渡すか)です。

0. 先に伝えておきたいこと(正直ベース)

  • AIエージェントは、実際には接続していません。(前の記事と同じです。認証が必要で、見送りました。)この画面が調べるのは、データベースの権限の設定です。AIが実際にどう動くかではありません。
  • 「AIから読める」の判断は、読み取り専用MCPが使うロール(supabase_read_only_user)の権限から出しています。このロールを使うことは、ドキュメントの説明です。私は、このロールで実際にSQLを実行したことはありません(前の記事のとおり、切り替えられませんでした)。
  • 「見え方」は目安です。ポリシーの中身(条件)は読んでいません。
  • 費用:画面を開くと、exposure-report 関数が1回呼ばれます(自動の更新はありません)。Freeプランの月50万回に対して、ほぼ無視できます。他にかかるお金はありません。AIツール側の利用料は、この記事の範囲外です。
  • セキュリティ:この関数が返すのは、テーブル名・列名・行数・権限です(行の中身は返しません)。それでも社外に見せてはいけない情報です。関数はVerify JWTをオフにして、「アプリのpublishableキー」を確認するだけの簡単な守りにしています。社外に出すページには、本物のログインを付けてください。

1. つくったもの①:データ棚卸し

データ棚卸しの画面

テーブルごとに、誰に、どう見えるかを並べます。

列 見えること どこから取る?
RLS RLSがオンか、ポリシーの数 pg_class・pg_policies
ブラウザ(ログインなし) anon ロールに、読める権限とポリシーがあるか has_table_privilege・pg_policies
ログインしたユーザー authenticated ロールに、同じ判定 同上
AI(読み取り専用の接続) 読み取り専用ロールに、読める権限があるか has_table_privilege

全体像

画面の一番上に、赤い警告が出ます。

読み取り専用のAI接続は、RLS(行ごとのアクセス制限)を通りません。このデータベースでは、AIが読めるテーブルが5個あり、その全行が見えます。

前の記事で確かめたとおり、このロールは rolbypassrls = true(RLSをすり抜ける)でした。そのため、このデモのデータベースでは、5つのテーブルすべてが、AIから全行読めるという結果になりました。

一番「おっ」と思ったのは、meeting_notes(面接メモが入っているテーブル)です。

  • ブラウザ(ログインなし):見えない(RLSはオンで、ポリシーが0個だから)
  • ログインしたユーザー:見えない
  • AI(読み取り専用の接続):全行読める

つまり、アプリのAPIからは誰にも見えないのに、AIの接続からは全部見えるテーブルです。面接メモは個人情報です。RLSで守っているから安心、と思っていたデータが、AIの接続からは丸見えになりうる。画面で一覧にして初めて、実感しました。

「見せたくない」と決めてみる

右端の列で、テーブルごとに「見せてよい」「見せたくない」を決められます(ブラウザの localStorage に保存するだけで、サーバーには送りません)。

見せたくないテーブルが、AIから読める、と警告される

invoices と meeting_notes を「見せたくない」にすると、「見せたくないと決めたテーブルが、2個、AIから読めます」と警告が出ます。あなたの判断と、現実のズレを、赤で見せる画面です。

画面の下には、見せたくないデータがあるときの考え方の例を載せました(開発用のプロジェクトにつなぐ、別のプロジェクトに置く、範囲・書き込み禁止・道具を絞る、人が承認する)。「別のプロジェクトに置く」は、私は試していません。

2. つくったもの②:接続URLづくり

2つ目のタブでは、MCPの接続URLを、チェックを入れてつくります。ダッシュボードの Connect → MCP で見たURLの形を、そのまま組み立てています。

範囲・読み取り専用・道具を絞った例(鍵の広さ:狭い)

プロジェクトのID(画像は、架空のIDです)を入れて、読み取り専用と3つの道具(docs / database / debugging)だけにすると、「渡す鍵の広さ:狭い」と出ます。右には、AIツールに追加するコマンド(Claude Codeの例)が出ます。コマンドは画面では実行しません。

わざと広くすると、こうなります。

広くした例(鍵の広さ:広い)

読み取り専用を外し、プロジェクトのIDを空にし、account branching functions を足すと、「広い」になり、危険・注意が並びます。

各グループの説明は、ツール名から私が読み取ったものです(前の記事で、create_project や delete_branch といった名前を見ました)。「project_ref を付けると account が無効になる」は、ドキュメントの説明で、私は確かめていません。画面にも、そう書きました。

3. つくり方

app/index.html                                    画面(HTML 1枚、ビルド不要)
supabase/functions/exposure-report/index.ts       棚卸しのAPI(読むだけ)

APIは、カタログ(データベースの設計情報)を調べるだけです。

select c.relname as name, c.relrowsecurity as rls_enabled,
       has_table_privilege('anon', c.oid, 'select') as anon_select,
       has_table_privilege('authenticated', c.oid, 'select') as authenticated_select,
       has_table_privilege('supabase_read_only_user', c.oid, 'select') as mcp_select
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r';

動かすときは、前の記事のアプリと同じです。

# exposure-report を Edge Function としてデプロイ(Verify JWT はオフ)
cd app
cp config.example.js config.js     # URL と publishable キーを入れる
python3 -m http.server 8765 --bind 127.0.0.1

4. つまずいたこと

① 「ブラウザ(ログインなし)」の判定が、まちがっていた

最初の版では、invoices が、ブラウザ(ログインなし)にも「条件つきで見える」と出ていました。ポリシーが2個あるから、と判断したのです。

不安になったので、実際にログインなしで、APIから読んでみました。

テーブル ログインなしで読めた行数
invoices 0
meeting_notes 0
memberships 0
posts 1(公開済みの記事)
tenants 0

「条件つきで見える」と出していた4つのテーブルのうち、実際に読めたのは**posts の1件だけでした。画面が嘘をついていた**のです。

原因は、ポリシーがどのロール向けかを見ていなかったことでした。invoices のポリシーは to authenticated(ログイン済みの人向け)だけなので、ログインなしの人には当てはまりません。直し方は、「読む(SELECT)ためのポリシーのうち、そのロールに当てはまるものの数」を数えることでした。

(select count(*)::int from pg_policies p
  where p.schemaname = 'public' and p.tablename = c.relname
    and p.cmd in ('SELECT', 'ALL') and (p.roles && array['anon', 'public']::name[])) as anon_select_policies

直したあとは、ログインなしの列が、実際の結果(posts だけ読める)と合うようになりました。ただし、ポリシーの「条件」(using の中身)までは見ていません。 そのため「条件つき」は、「条件に合えば見える」の意味で、実際に何行見えるかは分かりません。

学び:権限を表示する画面は、実際に読んでみて、突き合わせる。 「ポリシーがある」と「そのロールが読める」は、別のことでした。

② 「個人情報かも」の判定は、当てにならない

列名に name mail phone などが入っていると、「個人情報かも」のタグを付けています。tenants の name は会社名なのに、タグが付きました(上の画面)。逆に、中身が個人情報の列でも、名前が content なら、何も付きません(meeting_notes.content がそうです)。

列名での判定は、目印程度です。最終的には、人が中身を見て決めるしかなさそうです。

③ 画面を直したら、一覧の幅があふれた

判定の表示を長くしたら、一番右の列がカードからはみ出して、選択肢が切れました。ラベルを短くして(「見えない」「条件つき」「全行」)、詳しい説明は、表の下の注意書きに移しました。

5. 実際の運用にするなら

どれも、私は試していません。

  • 本物のAI接続との突き合わせ:権限の設定から出した目安が、実際のエージェントの動きと合うかを確かめる
  • ポリシーの中身の解析:using の条件まで読んで、「何行見えるか」を出す
  • 列ごとの棚卸し:テーブル単位でなく、列単位(個人情報の列だけ隠す、など)
  • 判断の保存と共有:今は自分のブラウザだけ。チームで共有して、承認の記録を残す
  • 定期実行:テーブルやポリシーが増えたら、自動で「見せたくないのに読める」を見つける
  • ログイン:棚卸しの結果は、社外に出してはいけない情報です

まとめ

  • AIに鍵を渡す前の確認を、1枚の画面にした(棚卸し + 接続URLづくり)
  • このデモでは、読み取り専用のAI接続から、5つのテーブルすべてが全行読める結果になった
  • アプリのAPIからは誰にも見えない(RLSオン・ポリシーなし)テーブルも、AIの接続からは見える
  • 「見せたくない」と決めたテーブルが読めるときは、赤い警告で、判断と現実のズレを見せられる
  • 最初の版は、ログインなしの判定がまちがっていた。実際に読んで、突き合わせて直した
  • 実際のAIエージェントとの接続は、私は未検証

コードは、上のGitHubにあります。次は、Realtimeを使った「みんなで同時に見る」ライブ注文ボードです。


スクリーンショットは、記事用に作った無料プランのデモ環境で撮影したものです。画像のプロジェクトIDは、架空のものです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?