安全な DynamoDB MCP サーバーの構築に必要だったこと
DynoTable の MCP サーバーを使うと、Claude Code、Cursor、Codex があなたの DynamoDB テーブルを操作できます。エージェントがあなたの AWS キーを持つことはなく、書き込みをコミットすることもできません。
データベース向けの最も単純な MCP サーバーは、生の AWS 認証情報を保持し、エージェントの呼び出しをそのまま API に渡すプロセスです。そのプロセスに書き込みツールがあれば、モデルのミスとあなたのテーブルのあいだを隔てるものは何もありません。
ユーザー側から見た MCP サーバーを安全に選ぶ方法はすでに取り上げました。
この記事は構築側の話です。デスクトップの DynamoDB クライアントである DynoTable が、その AI ツールを外部のエージェントに提供するために何が必要だったのかを書きます。
設計を支えるプロダクトの要素は 3 つです。
- ステージング。 エージェントが行う変更はすべて、保留中の差分としてステージングエリアに入ります。それを DynamoDB にコミットできるのはあなただけです。
- スコープ。 各接続には 3 つのうち 1 つが与えられます。読み取り専用、読み取りとステージ、フルアクセス です。フルアクセスでは、ビューを開く、フィルターを設定する、データ変換、エクスポートが加わります。それでも DynamoDB への書き込みはステージングを経由します。
- ライセンスの段階。 有料ライセンス(トライアルまたはサブスクリプション)で書き込みと AI が使えるようになります。無料プランは読み取り専用で、サインアウトしたアプリはデータを一切提供しません。
これを作るには、デスクトップアプリの内部に OAuth 2.1 認可サーバーを置き、認証情報を取り違えられないアイデンティティ型を用意する必要がありました。そして、便利な既定値を何度も退ける必要もありました。
エージェントは敵対的だと仮定するので、キーも持たせずコミットもさせない
プロンプトインジェクションによって、AI エージェントの意図は信頼できないものになります。テーブルの行に、モデルが従ってしまう指示が含まれていることがあります。そのため、この設計はエージェントの行儀の良さに決して依存しません。
認証情報はアプリの中に留まります。エージェントはツール呼び出しを送り、アプリがプロファイル自身の認証情報で DynamoDB リクエストに署名します。エージェントがそれを目にすることはありません。
書き込みはステージングにのみ入ります。スコープは、モデルが何を求めても、呼び出しにできることを制限します。すべての MCP 呼び出しはアプリの監査ログに記録され、 MCP 由来であることが示されます。
アプリ内アシスタントには、もう 1 つ層があります。既定では、ツール呼び出しのたびにあなたの許可を求めます。そのプロンプトは MCP 経由では存在しません。それがない状態で被害を抑えるものについては、後のセクションで扱います。
127.0.0.1 にバインドしてもウェブページは止められない
サーバーは 127.0.0.1 でのみ待ち受けます。これで他のマシンは締め出せますが、あなたが開くどのウェブページも、ブラウザを通じて localhost にリクエストを送れます。
通常のクロスサイトリクエストには、そのページの Origin ヘッダーが付きます。サーバーは、
Origin がウェブページであるリクエストをすべて拒否します。
DNS リバインディングはもっと厄介なケースです。攻撃者のドメインが 127.0.0.1 に再解決されるため、ブラウザはそのリクエストを同一オリジンとして扱います。それでもリクエストの Host ヘッダーには攻撃者のホスト名が入っています。
サーバーは、自分のポート上のループバックの Host(localhost、127.0.0.1、[::1])だけを受け付けます。リバインドされたリクエストはこのチェックで失敗します。
Origin がまったくないリクエストは通ります。Claude Code のような CLI クライアントはこれを送らないからです。つまり、このガードはブラウザ攻撃の 1 つのクラスを防ぐだけで、誰も認証しません。認証はベアラートークンの役目です。
アプリ自身が OAuth サーバーになる必要があった
HTTP サーバー向けの MCP の認可仕様は OAuth 2.1 です。OAuth の用語では、MCP サーバーはリソースサーバー で、トークンを受け取ってツールを提供します。認可サーバー(AS)はユーザーに同意を求め、そのトークンを発行します。
ホスト型のサービスなら上流のアイデンティティプロバイダーに頼れます。ローカルのデスクトップサーバーにはそれがないため、DynoTable が自ら AS になります。
AS は S256 による PKCE、認可コード、ローテーションするリフレッシュトークン、そしてクライアントが AS を発見するための RFC 8414 と RFC 9728 のメタデータドキュメントを実装しています。
クライアントは動的クライアント登録(RFC 7591)で登録します。2025-11-25 版の MCP 仕様はこの方式を任意とし、後方互換性のために残しつつ、代わりに Client ID Metadata Documents を推奨しています。
現在サポートしているのは動的登録です。早い段階で定め、守り続けてきたルールは次のとおりです。
- プロトコルを自前で組まなかった。 公式の MCP SDK は AS のエンドポイントをすぐに使えるルーターとして提供しています。私たちが実装するのは、その下のストレージとポリシーだけです。
- AS は 2 つ目のループバックリスナーとして動く。 私たちの MCP フレームワークとそのルーターは別々の HTTP スタックを使います。リソースサーバーは、protected-resource メタデータを通じてクライアントを AS へ案内します。PKCE とトークンの文法を手書きするより、2 つ目のリスナーを受け入れることを選びました。
- リダイレクト先はループバックのみ(ネイティブアプリ向けの RFC 8252 に準拠)。無効なリダイレクトには直接 400 を返し、エラーリダイレクトは決して行わないため、攻撃者の URI が行き先になることはありません。
- 認可コードは 1 回しか使えない。 再送されたコードは無効なグラントで、未使用のコードは 60 秒で期限切れになります。
- 失効は設定の操作で、リクエストのたびに再チェックされる。 クライアントの「失効」をクリックすると、期限内のトークンがあっても、そのクライアントの次の呼び出しは失敗します。
すべてのトークンは同意時に 1 つのプロファイルと 1 つのリージョンを固定する
各エージェント接続が到達できるのは、ちょうど 1 つの DynoTable プロファイルです。エージェントに使わせてよいプロファイルごとに「MCP 経由で公開」をオンにし、同意時にそのうちの 1 つへ接続をバインドします。
アプリは承認された同意をそれぞれグラントとして保存します。中身はクライアント、スコープ、バインドされたプロファイルです。「MCP 経由で公開」をオフにすると、そのプロファイルにバインドされたすべてのグラントが失効します。
AWS プロファイルとリージョンごとに、専用の DynamoDB クライアントが用意されます。異なるプロファイル上の 2 つのエージェントが、互いのアカウントに手を伸ばすことはできません。
このバインディングは当初、ばらばらのフィールドとして始まり、同意からリクエストまでのあいだのすべてのレコードで再宣言されていました。現在は 2 つの形を持つ 1 つのアイデンティティ値です。リージョン付きの AWS プロファイルか、DynamoDB Local のポートかのどちらかです。
この型では、プロファイルとリージョンのない AWS アイデンティティも、そのどちらかを持つローカルのアイデンティティも表現できません。パースのたびに、2 つの形のどちらかが得られるか、失敗するかです。
この値は、同意からトークン、保存されたグラント、そしてすべてのリクエストまで、そのまま運ばれます。
ここから 2 つのルールが導かれます。
- リージョンは同意時に凍結される。 あなたがリージョン Y のプロファイル X を承認すると、トークンはその組を持ちます。リフレッシュは同じアイデンティティを再発行し、スコープを狭めることはできても広げることはできません。プロファイルのリージョンを変えるには、新しい同意が必要です。
- 不正な形式のトークンには 401 を返す。 トークン上のアイデンティティが 2 つの形のどちらにもパースできなければ、そのリクエストは認可されません。サーバーが既定のプロファイル、既定のリージョン、空文字列でその穴を埋めることは決してありません。
リクエストごとにリージョンを解決したり、既定値にフォールバックしたりすると、エージェントが
us-east-1 にクエリを投げる羽目になります。
同意は独自の信頼境界を持つアプリ内モーダルに移った
最初の同意プロンプトは、OS ネイティブのメッセージボックスでした。それが開いているあいだは、アプリのメインプロセスがブロックされました。私たちのデザインシステムに合わせることも、ドキュメント用にキャプチャすることもできませんでした。
現在の同意はアプリ内のモーダルです。接続してきたクライアントの名前、公開したプロファイルを対象とするプロファイルピッカー、そして 3 つのスコープが表示されます。既定では「読み取り専用」が選択され、「承認」を押すまでは何も付与されません。

同意リクエストは 5 分で期限切れになり、拒否として決着します。クライアントには、ハングしたリクエストではなく access_denied が返ります。すでに決着したリクエストへの 2 回目の応答は無視されます。
ピッカーには独自の信頼境界があります。クライアントは authorize URL でプロファイルをヒントとして示せますが、グラントはあなたが選択したものから作られ、あなたに表示されたリストと照合されます。偽造されたヒントで、画面に一度も表示されなかったプロファイルをバインドすることはできません。
同意は接続ごとに 1 回だけ与えられる
MCP 経由の認可はスコープのみで、クライアントの接続時に 1 回だけ承認されます。アプリ内アシスタントの呼び出しごとのプロンプトは、MCP のトラフィックには適用されません。
そのため、漏洩した「読み取りとステージ」のトークンは、人間が各呼び出しを見ることなく変更をステージできます。DynoTable のウィンドウが開いていなくても、フィルターまたはキーのリストで、1 回の呼び出しにつき最大 10,000 アイテムの一括削除もステージできます。
それをコミットすることはできません。ステージされた変更はそれぞれ、あなたがコミットするまでアプリの中で待機します。
被害を抑える仕組みはいくつかあります。アクセストークンは 1 時間で期限切れになり、リフレッシュトークンは使うたびにローテーションします。「設定」で個々のクライアントを失効でき、監査ログはすべての MCP 呼び出しに印を付け、モーダルは既定で「読み取り専用」を選択します。
残りは、受容したリスクとして私たち自身の設計ドキュメントに記録しています。
ライセンスは呼び出しのたびに再チェックされる
保存されたグラントは、あなたが承認したスコープを保持します。実際に適用されるスコープは、ツール呼び出しのたびに、その時点のアプリのライセンスから計算し直されます。
- 有料(トライアルまたはサブスクリプション): 付与されたスコープが適用されます。
- 無料の読み取り専用プラン: すべての接続が「読み取り専用」に制限され、有料ツールは非表示になります。結合と集計を伴う SQL と、マルチテーブルビューを開く操作です。
- サインアウト時: すべてのリクエストが 401 になります。
グラント自体は決して上書きされないため、セッションの途中で再アクティベーションすると、新たな同意なしにスコープが再び広がります。有料ツールは、クライアントが再接続した後にツール一覧へ再び現れます。
1 つの非対称性は意図的なものです。アプリ内では、ほとんどのエクスポートが無料プランでも利用できます。あなたのデータはあなたのものだからです。
MCP 経由では、エクスポートはフルアクセスのツールなので、読み取り専用への制限がそれをブロックします。自分のデータをエクスポートする人と、データを一括で持ち出すエージェントは、信頼の扱いが異なるケースです。
ヘッドレスなスキャンにはポップアップの代わりに予算を設ける
外部エージェントにはウィンドウもポップアップもなく、見ている人がいないこともよくあります。アプリ内の対話的なガードには、それぞれヘッドレスな代替が必要でした。
アプリ内では、1 GiB または 500 万アイテムを超えるテーブルの完全なスキャンは、まずあなたに確認します。MCP 経由では、確認する相手がいません。
そこで、アイテム数、バイト数、所要時間の予算が、テーブルに対する Workbench の SQL クエリとデータ変換を制限します。スキャンが予算に達すると、何を絞り込むべきかを示すエラーで停止します。
PartiQL の読み取りはこの予算の対象外です。各呼び出しは 1 ページを読み、その行数の上限でさらに低く抑えられます。
テーブルをまたぐ SQL は、アプリのローカルエンジンを通じてページ単位でストリーミングされるため、メモリ内の上限で答えが途中で切れることはありません。
クライアントがスキャンの途中で切断すると、サーバーはステートメントの途中ではなく次のページ境界で停止し、一時データを削除します。
MFA コードはエージェント自身のプロンプトを通じて受け取る
一部の AWS プロファイルには MFA コードが必要です。クライアントが MCP の elicitation をサポートしている場合、サーバーは同じ接続を通じて 6 桁のコードを求めます。
Claude Code はそのプロンプトをあなたに表示し、呼び出しはあなたのコードで再試行されます。コードは 1 回限りです。同じ認証情報を必要とする同時の呼び出しは、1 つのプロンプトを共有します。
クライアントが elicitation をサポートしていない場合は、その AWS プロファイルで開いている DynoTable のウィンドウが、アプリ自身の MFA プロンプトを表示します。どちらもなければ、呼び出しは失敗し、DynoTable で再認証するよう求めます。
SSO はこの経路に含めていません。SSO にはブラウザでのサインインフローが必要で、入力したコードでは完了できないため、SSO プロファイルはアプリ内で再認証します。
安全でない MCP サーバーを見抜く 7 つの質問
- 認証情報はどこにあるか。エージェントのプロセスの中か、それともサーバーの背後か?
- 実際のスコープを伴う接続ごとの同意があるか、それとも 1 つの設定ファイルがすべてを許可するか?
- 人間のレビューのステップなしに書き込みが実行されることはあるか?
- アイデンティティ(プロファイルとリージョン)は同意時に固定されるか、それとも呼び出し時に既定値から解決されるか?
- 不正な形式や失効したトークンは確実に失敗するか、それとも何かにフォールバックするか?
- どの操作がエージェント由来かを示す監査証跡はあるか?
- 暴走したスキャンを制限するものは何か。クライアントが操作の途中で切断したら何が起こるか?
MCP サーバーのドキュメントはセットアップと各スコープを、ステージングのドキュメントはレビューとコミットを扱います。接続側のガイドでは、クライアントごとの手順を説明しています。
制限のないスキャンにどれだけのコストがかかるかを見積もるには、料金計算ツールを使ってください。
サーバーを試すには、DynoTable をダウンロードし、「設定」で MCP サーバーをオンにして、プロファイルの「MCP 経由で公開」をオンにし、「読み取り専用」でクライアントを接続してください。


