ブログ一覧

AI エージェントに X の鍵を渡さず投稿させる権限設計

X 公式の MCPMCP は AI エージェントへ外部サービスの操作を見せる規格)には、読み取りのツールは揃っていますが投稿のツールがありません。認可サーバーの scopes_supported に tweet.write が無いことを、2026年9月8日に確認しました。

で、色々と選択肢があって迷ったんですが、最終的に、Slack を起点に X の API 経由で投稿している自作のサービス (以下、neta-bot) に 外部からのリクエストを受け入れる経路を1つ足すことで、やりたいことが実現できました。

話が前後しましたが、きっかけは、ブレーンである LLM(以下、秘書)自身に X への投稿を任せたかったことです。(詳細はサンドボックスと git だけで記憶を持たせる設計を参照)

鍵の置き場所を先に決めてから経路を選ぶ

当初、X への投稿の経路として考えられる案は6つありました。

  1. コピペ
  2. neta-bot の定常パイプラインに乗せる
  3. neta-bot に単発の入口を足す
  4. ホストの CLI を使う
  5. 秘書が X API を直接叩く

そして、評価軸は3つに絞りました。公開の引き金が人間に残るか、鍵の保持者が増えないか、投稿が台帳に載るか、です。

秘書が API を直接叩く案は、X の投稿トークンをサンドボックスに置くことになり鍵の保持者が増えるので落としました。手打ちとホスト CLI は、投稿が台帳を通らないので落としました。残ったのが、neta-bot に単発の入口を足す案です。

経路を1つに決めても、鍵は2つ残ります。X の投稿トークンは neta-bot が動く Lambda だけが持ち、サンドボックスには最初から置きません。もう 1 つは、その neta-bot の投稿口を叩くための Bearer トークンで、これをどこに置くかが次の論点になります。

値をサンドボックスに置かない

最初に検討したのは、Bearer トークンを秘書の MCP 設定にヘッダとして書き、値をサンドボックスの環境変数に注入する案でした。動作はします。しかし、この案は採用しませんでした。サンドボックスに値が実在すると、秘書だけでなく、そこで動く全プロセスがその値を読めてしまいます。動くかどうかと、置いてよい場所かどうかは別の問題です。値を置く場所を増やすたびに、漏れる経路も同じ数だけ増えます。

採用したのは、ホスト側で動く mcp-remote が Bearer を付けて neta-bot の口へ中継する形です。中継はホストのプロセスとして起動し、サンドボックスの秘書からは投稿ツールしか見えません。トークンの値そのものは、秘書のプロセスにも、サンドボックスの環境変数にも、一度も現れません。

承認は会話に一本化する

neta-bot には、候補文面を Slack のボタンで承認する UI がすでにあります。秘書の投稿もそこに流す案も検討しましたが、会話で承認した上に Slack でもう一度ボタンを押すのは二重承認で、コピペより手間が増えます。Slack のボタンは MCP のツールからは押せないという制約もありました。

そこで承認は会話に置くと決めました。運用の規約は次の通りです。

  • 秘書が投稿ツールを使えるのは、会話で承認された文言に限る。一字でも変えたら再承認が必要で、まとめての承認はしない
  • 投稿した直後に URL をその場で報告する。承認から公開までのあいだに、確認できない空白の時間を作らない
  • 公式 MCP に投稿の手段が将来増えたらその時に戦略を立て直す

会話に承認を固定できた理由は、可逆な操作と不可逆な操作を切り分けたことにあります。下書きのような戻せる操作は、鍵とスクリプトを渡して秘書に直接やらせて構いません。公開して戻せない操作だけ、承認は会話に置き、実行は口の狭い道具に限ります。

投稿口を 1 つに閉じ、台帳に載せる

neta-bot はすでに X の投稿権限を持ち、投稿を DynamoDB の台帳に載せ、週次でメトリクスを集めています。ここに口を1つ足せば、鍵の保持者を増やさずに、秘書の投稿も定常投稿も同じ台帳に載ります。

口の形はできるだけ狭くしました。既存の Lambda Function URL に新しいパスを足し、Bearer で認証する形です。

  • ツールは create_post の1つだけで、受け取るのは本文と任意の返信先 ID だけ
  • プロンプトも文体の規則も通さず、受け取った文面をそのまま投稿。台帳には source: "mcp" として記録し、定常投稿と見分けられるようにする

運用を始めてすぐ、応答が失われた場合の挙動を見直す必要が出ました。応答が届かず秘書が文面を少し変えて再送すると、二重投稿になったためです。同一文面なら X 側が重複として弾きますが、文面を変えると通ってしまう。対策として、request_id を渡す冪等キーを足しました。台帳の行を条件付き PUT で先に確保し、2回目以降の呼び出しには1回目の結果をそのまま返します。二重投稿を X 側の重複判定に頼らない形にした前例は、API の重複投稿を入口と出口の両方で防いだ話にも書きました。

実績と、担保できる範囲

現在、このフローは期待通りに動いています。

読み取りは公式 MCP、書き込みは neta-bot の口という役割分担のまま、投稿から確認までが一連の会話の中で完結していて、台帳にも定常投稿と同じ形で source: "mcp" の記録が残りました。

この設計で決定的に担保できているのは2点。秘書が単独では投稿できないこと、そして投稿は必ず台帳に載ることです。この2点は Bearer トークンの置き場所と新しく作った投稿口の形そのものから導かれるので、秘書の振る舞いに依存しません。

現在は、1人で運用しているため問題ないですが、組織的に運用する場合は、承認された文言と実際に投稿された文言が一致しているかどうかの判定を加えるなど、工夫が必要かもしれません。

関連

この記事をシェア