Codex CLIからWordPressへMCP経由で「下書きだけ」投稿する安全な仕組みを作ってみた
生成AIにWordPressの記事を書かせたい。しかし、AIに管理者権限や公開権限まで渡すのは怖い——。そこで今回は、AIは新規下書きを作るだけ、内容の確認と公開は人間だけが行う仕組みを実際に構築・検証しました。
この記事は完成済み製品の紹介ではなく、WordPress 7.1、PHP CLI 8.3.6、公式 MCP Adapter 0.6.1、Codex CLI 0.151.0/0.153.0で行った構築記録です。最終的にCodexからMCPへ接続し、WordPress管理画面で新規投稿が「下書き」になっていることまで確認しました。
最重要の前提:この構成でAIに許可するのは新規下書きの作成だけです。公開、既存投稿の編集・削除、ユーザー・プラグイン・テーマ・設定・DBの変更は許可しません。本番サイトへ導入する前に、バックアップのあるステージング環境で検証してください。
- 1. 今回やりたいこと
- 2. なぜAIへ管理者権限を渡さないのか
- 3. MCP、REST API、wp-jsonを短く理解する
- 4. 全体のセキュリティ設計
- 5. MCP Adapterを導入する
- 6. デフォルトMCPサーバーを止める
- 7. MCP専用ユーザーを作る
- 8. 下書き専用AbilityとカスタムMCPサーバーを作る
- 9. Application Passwordを発行する
- 10. Codex認証とWordPress認証は別物
- 11. Nginxで wp-json がトップページへ飛ぶ問題
- 12. curlでMCPプロトコルを直接確認する
- 13. Codex CLIへMCPサーバーを登録する
- 14. 認証情報を .env で保持する
- 15. 実際にCodexから下書きを作る
- 16. トラブルシューティング
- 17. 今回のセキュリティ設計を振り返る
- 18. 今後やりたいこと
- 19. まとめ
- 用語集
- 技術補足:CodexからWordPressへ下書きが作られるまで
- TIPS記事候補
- 参考資料・一次情報
1. 今回やりたいこと
作りたい流れは単純です。
- 人間がCodex CLIへ記事作成を依頼する
- CodexがMCP経由でWordPressへタイトルと本文だけを送る
- WordPress側が必ず
draftとして新規投稿を作る - 人間が管理画面で内容を確認する
- 問題がなければ、人間が公開ボタンを押す
flowchart TD
U[User] --> C[Codex CLI]
C -->|HTTPS / TCP 443| N[Nginx]
N --> R[WordPress REST API]
R --> M[MCP Adapter]
M --> A[Create Draft Ability]
A --> D[WordPress Draft]
D --> H[Human Review]
H --> P[Publish by human only]
MCP用に3000番や8000番などの新しいTCPポートは開けません。既存のHTTPS(TCP 443)をNginxで受け、WordPress REST APIへ渡します。
2. なぜAIへ管理者権限を渡さないのか
生成AIは便利ですが、入力の誤解、操作対象の取り違え、プロンプトインジェクションなどが起こり得ます。「公開しないで」と文章で頼むだけでは、技術的な権限制御にはなりません。
そこで、事故が起きても影響を小さくする最小権限を採用します。今回は次の三層で制限しました。
- WordPress専用ユーザーをContributor(寄稿者)にし、
publish_postsを持たせない - MCPサーバーには下書き作成Abilityを1個だけ登録する
- Codex側でも
enabled_toolsで同じツール1個だけを許可する
さらにAbilityの入力に status を用意せず、PHP内部で post_status => draft に固定します。AIの指示文、MCPの引数、WordPressの権限という複数地点で防御する設計です。
3. MCP、REST API、wp-jsonを短く理解する
MCP(Model Context Protocol)は、AIクライアントが外部の道具を発見し、決められた形式で呼び出すための共通プロトコルです。USBのような「接続の共通ルール」と考えると分かりやすいでしょう。今回の道具は「WordPressに下書きを1件作る」です。
REST APIは、WebのHTTP通信を使ってアプリ同士がデータや処理をやり取りする入口です。WordPressでは投稿、ユーザー、独自機能などをJSONで扱えます。
wp-jsonはサーバー上の実フォルダーではありません。WordPress REST APIへ入るためのURL上の入口です。したがって、Nginxは /wp-json/... を静的ファイルとして探すのではなく、WordPressの index.php へ正しく渡す必要があります。
WordPress公式のMCP Adapterは、WordPressのAbilities APIとMCPを橋渡しします。公式READMEでは、AbilityをMCPのtool、resource、promptへ変換し、HTTPとSTDIOのtransportを提供すると説明されています。今回使うのはHTTP transportと、独自のtoolだけです。
参考:WordPress MCP Adapter公式GitHub、WordPress Abilities API、OpenAI Docs: CodexのMCP設定
4. 全体のセキュリティ設計
WordPressには papanda-mcp という専用ユーザーを作り、Contributorを割り当てました。検証環境で確認した主なCapabilityは次のとおりです。
| Capability | 結果 | 意味 |
|---|---|---|
read |
YES | ログインして基本情報を読める |
edit_posts |
YES | 自分の投稿を新規作成・編集できる |
publish_posts |
NO | 投稿を公開できない |
edit_others_posts |
NO | 他人の投稿を編集できない |
manage_options |
NO | WordPress設定を変更できない |
activate_plugins |
NO | プラグインを有効化できない |
delete_published_posts |
NO | 公開済み投稿を削除できない |
edit_published_posts |
NO | 公開済み投稿を編集できない |
upload_files |
NO | メディアをアップロードできない |
flowchart TD
AI[AI] --> USER[papanda-mcp 専用ユーザー]
USER --> ROLE[Contributor]
ROLE --> EDIT[edit_posts: YES]
ROLE --> PUB[publish_posts: NO]
Contributorには標準で delete_posts があり、自分の未公開投稿を削除できます。つまり、これは理論上の「完全な最小権限」ではありません。v0.1では、標準ロールを使う運用の単純さとリスクのバランスからContributorを採用し、MCPには削除Abilityを一切公開しませんでした。より厳密にするなら、将来は read と edit_posts だけを持ち delete_posts を持たない専用ロールを設計・検証する余地があります。本記事では既存ロールやCapabilityを変更しません。
WordPress公式も、ロールはCapabilityの集合であり、current_user_can() で現在のユーザーのCapabilityを確認できると説明しています。
参考:Roles and Capabilities、投稿Capabilityの定義
5. MCP Adapterを導入する
検証時に使ったのはWordPress公式 WordPress/mcp-adapter の0.6.1です。記事執筆時点のリリース情報では、0.6.1は0.6.0の配布ZIPを修復した版で、API・hook・protocolの挙動変更はありません。導入時には必ず最新リリースノートと要件を再確認してください。
公式のWP-CLI例は次の形です。
wp plugin install \
https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip \
--activate
実環境ではWordPressファイル所有者との権限差があったため、Webサーバーの実行ユーザーで操作しました。
sudo -u www-data -- \
wp --path=/var/www/example.com plugin install \
https://github.com/WordPress/mcp-adapter/releases/latest/download/mcp-adapter.zip \
--activate
--path は対象WordPressを明示し、sudo -u www-data はWordPressファイルの所有者権限でWP-CLIを実行します。環境によって実行ユーザーやパスは異なります。事前に所有者とバックアップを確認してください。権限エラーを力ずくで回避する chmod 777 や、対象を確認しない再帰的 chown は行いません。
参考:公式Installation Guide、MCP Adapter 0.6.1リリース
6. デフォルトMCPサーバーを止める
MCP Adapterのデフォルトサーバーは、公開設定されたAbilityを探して実行する3つのメタツールを提供します。汎用用途には便利ですが、今回は「自作した下書き作成だけ」を露出させたいので無効にしました。
wp-content/mu-plugins/papanda-mcp-safety.php に次のMU Pluginを置きます。MU Pluginは通常のプラグインより自動的・早期に読み込まれる仕組みです。
<?php
/**
* Plugin Name: Papanda MCP Safety
* Description: MCP Adapter のデフォルトMCPサーバーを無効化する安全設定。
*/
defined( 'ABSPATH' ) || exit();
add_filter(
'mcp_adapter_create_default_server',
'__return_false'
);
これにより、デフォルトサーバーの3ツールとendpointは作られません。後述のカスタムサーバーに明示したAbilityだけを公開します。
7. MCP専用ユーザーを作る
通常の管理者アカウントを流用せず、用途を名前から判別できる専用ユーザーを作ります。次は概念例です。パスワードをコマンドラインへ直接書くとshell history等へ残るため、対話入力や安全なsecret管理を利用してください。
wp --path=/var/www/example.com user create papanda-mcp \
mcp-user@example.com \
--role=contributor \
--prompt=user_pass
上のメールアドレスは架空です。実際の個人メールアドレスは記事やログへ載せません。作成後は、環境のCapabilityを読み取り専用コマンドで確認します。
wp --path=/var/www/example.com user list \
--role=contributor \
--fields=user_login,roles
wp --path=/var/www/example.com cap list contributor
Contributorを選んだ理由は、下書きを作る edit_posts はある一方、公開する publish_posts がないためです。ただしサイトやプラグインがロールを変更している可能性があるため、ロール名だけを信頼せず、実Capabilityを確認します。
8. 下書き専用AbilityとカスタムMCPサーバーを作る
独自プラグインで papanda-mcp/create-draft だけを登録します。MCPではスラッシュがハイフンへ正規化され、papanda-mcp-create-draft と見えます。
次は今回の設計を再現する重要部分です。WordPress 7.1とMCP Adapter 0.6.1向けです。導入前にステージングでPHP構文、依存バージョン、実際のレスポンスを確認してください。
<?php
/**
* Plugin Name: Papanda Draft MCP
* Description: papanda-mcp ユーザーへ新規下書き作成だけを提供する。
* Version: 0.1.0
* Requires Plugins: mcp-adapter
*/
defined( 'ABSPATH' ) || exit();
/**
* 認証済みユーザーが想定どおりの最小権限か確認する。
* publish_posts が将来追加された場合も拒否する(fail closed)。
*/
function papanda_mcp_is_draft_only_user(): bool {
$user = wp_get_current_user();
return is_user_logged_in()
&& 'papanda-mcp' === $user->user_login
&& current_user_can( 'edit_posts' )
&& ! current_user_can( 'publish_posts' );
}
add_action( 'wp_abilities_api_init', function (): void {
wp_register_ability(
'papanda-mcp/create-draft',
array(
'label' => 'Create WordPress Draft',
'description' => 'Creates one new WordPress post with draft status.',
'category' => 'site',
'input_schema' => array(
'type' => 'object',
'additionalProperties' => false,
'properties' => array(
'title' => array(
'type' => 'string',
'minLength' => 1,
'maxLength' => 200,
),
'content' => array(
'type' => 'string',
'minLength' => 1,
),
),
'required' => array( 'title', 'content' ),
),
'output_schema' => array(
'type' => 'object',
'additionalProperties' => false,
'properties' => array(
'post_id' => array( 'type' => 'integer' ),
'status' => array( 'type' => 'string', 'enum' => array( 'draft' ) ),
),
'required' => array( 'post_id', 'status' ),
),
'execute_callback' => function ( array $input ) {
// 防御的に実行直前にも権限を再確認する。
if ( ! papanda_mcp_is_draft_only_user() ) {
return new WP_Error(
'papanda_mcp_forbidden',
'Draft-only MCP access denied.'
);
}
$post_id = wp_insert_post(
array(
'post_title' => sanitize_text_field( $input['title'] ),
'post_content' => wp_kses_post( $input['content'] ),
'post_status' => 'draft',
'post_type' => 'post',
'post_author' => get_current_user_id(),
),
true
);
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
return array(
'post_id' => $post_id,
'status' => get_post_status( $post_id ),
);
},
'permission_callback' => 'papanda_mcp_is_draft_only_user',
'meta' => array(
'public' => false,
'mcp' => array( 'public' => false ),
'annotations' => array(
'readonly' => false,
'destructive' => false,
'idempotent' => false,
),
),
)
);
} );
add_action( 'mcp_adapter_init', function ( $adapter ): void {
$adapter->create_server(
'papanda-draft-server',
'mcp',
'papanda-draft',
'Papanda Draft MCP Server',
'Creates new WordPress drafts only.',
'1.0.0',
array( \WP\MCP\Transport\HttpTransport::class ),
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
array( 'papanda-mcp/create-draft' ), // tools
array(), // resources
array(), // prompts
'papanda_mcp_is_draft_only_user' // server-wide permission
);
} );
なぜ status を引数にしないのか
入力は title と content だけです。additionalProperties => false により、定義していない status なども受け付けません。そして、実行コード内で次を固定しています。
'post_status' => 'draft',
status に既定値 draft を置きつつ publish も選べる設計ではありません。そもそもAIが公開状態を指定する経路を作らないのが重要です。
タイトルは sanitize_text_field()、本文は投稿で許可されるHTMLへ絞る wp_kses_post() を通します。投稿者は固定IDではなく、認証済みWordPressユーザーの get_current_user_id() を使います。
二重のpermission checkとfail closed
同じ条件をAbilityの permission_callback とMCPサーバー全体のpermission callbackに入れました。
- ログイン名が
papanda-mcp edit_postsを持つpublish_postsを持たない
特に ! current_user_can( 'publish_posts' ) が大切です。将来、誰かが誤って専用ユーザーをAuthorなどへ昇格させても、機能が広がるのではなくMCPアクセス自体が拒否されます。安全側に停止する、いわゆるfail closedです。
なお、MCP annotationはクライアント向けのヒントであり、権限境界の代わりではありません。実際の強制はWordPress Capability、permission callback、入力schema、固定した draft で行います。
参考:wp_register_ability()、Creating Abilities、Transport Permission Callbacks
9. Application Passwordを発行する
Application Passwordは、外部アプリごとに発行・失効できるWordPressの認証情報です。WordPress管理画面へ入る通常パスワードとは別物です。WordPress 5.6以降に標準搭載され、HTTPS上のREST APIではBasic認証として利用できます。
wp --path=/var/www/example.com user application-password create \
papanda-mcp \
"Papanda Draft MCP" \
--porcelain
このコマンドは秘密値を標準出力へ表示します。画面共有、terminalログ、shell履歴、CIログへ残さない環境で実行し、表示された値はpassword manager等へ直ちに保存します。この記事には実値を掲載しません。
Application Passwordは用途単位で無効化できますが、専用ユーザーのCapabilityを超えることはできません。漏えいが疑われたら当該Application Passwordを失効・再発行します。
参考:WordPress REST API Authentication、Application Passwords Integration Guide
10. Codex認証とWordPress認証は別物
ここは混同しやすい点です。CodexがOpenAI/ChatGPTへ接続する認証と、MCPがWordPressへ接続する認証は別々です。
flowchart LR
H[Human] --> C[Codex CLI]
C -->|Codex / ChatGPT authentication| O[OpenAI service]
C -->|WordPress Application Password over HTTPS| W[WordPress MCP endpoint]
O -.-> W
Codexへログインできていても、WordPressのApplication PasswordがなければWordPress MCPは401になります。反対に、WordPressの認証情報はOpenAIアカウントの認証には使えません。
11. Nginxで wp-json がトップページへ飛ぶ問題
構築途中、次のendpointへアクセスするとJSONではなくWordPressトップページのHTMLへ流れる問題がありました。
https://example.com/wp-json/mcp/papanda-draft
一方、query形式の ?rest_route=/mcp/papanda-draft ではREST APIとして応答しました。これにより、MCPやAbilityより手前のNginx routingを疑えます。
元の設定イメージは次でした。
location ^~ /wp-json/ {
limit_req zone=llm burst=20 nodelay;
try_files $uri /index.php?$args;
}
wp-json は実フォルダーではないため、pretty URLから rest_route へ明示的に変換しました。
location ^~ /wp-json/ {
limit_req zone=llm burst=20 nodelay;
rewrite ^/wp-json/?(.*)$ /index.php?rest_route=/$1 last;
}
変更前に設定ファイルの対象server blockと既存rewriteを確認し、必ず構文検査します。
sudo nginx -t
sudo systemctl reload nginx
nginx -t が成功した場合だけreloadします。設定は環境により異なるため、この断片を無条件に貼り付けないでください。WordPressをサブディレクトリへ置く構成、reverse proxy、WAF、キャッシュ、multisiteでは別の調整が必要です。
変更後は /wp-json/ が application/json を返し、MCP endpointは未認証なら401のJSONを返すようになりました。401はこの段階では正常です。「URLがREST APIへ届き、認証で止まった」と切り分けられます。
12. curlでMCPプロトコルを直接確認する
Codexをつなぐ前に、MCPのHTTP endpointを直接検証しました。流れは次のとおりです。
initializeを送る- 応答ヘッダーからsessionをクライアント内部で保持する
notifications/initializedを送るtools/listで公開ツールを確認するtools/callを1回だけ行う
認証情報やsession値を記事へ載せないため、ここではJSON-RPC本文だけを示します。実際の curl では、認証headerとinitialize応答で得たsession headerを安全な環境変数等から渡しました。curl -v、set -x、history、ログ収集に秘密値を出さないよう注意してください。
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-check","version":"1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
tools/list では papanda-mcp-create-draft 1個だけが見えることを確認しました。デフォルトサーバー由来のメタツールや、編集・削除・公開ツールが見えたら先へ進まず、設定を見直します。
動作確認用の tools/call は、引数がタイトルと本文だけであることを確認してから1回実行しました。
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "papanda-mcp-create-draft",
"arguments": {
"title": "MCP下書き動作確認",
"content": "MCP経由で作成した確認用の下書きです。"
}
}
}
応答の isError が false、結果の status が draft であることを確認し、さらにWordPress管理画面でも下書き表示を人間が確認しました。実際のPost IDやsession値は記録・公開しません。
13. Codex CLIへMCPサーバーを登録する
Codexの ~/.codex/config.toml へ、HTTP MCPサーバーを登録します。
[mcp_servers.papanda-wordpress]
url = "https://example.com/wp-json/mcp/papanda-draft"
env_http_headers = { Authorization = "PAPANDA_WP_AUTH" }
enabled_tools = ["papanda-mcp-create-draft"]
default_tools_approval_mode = "prompt"
各設定の意味は次のとおりです。
url: HTTPSのMCP endpoint。新しい公開portは使わないenv_http_headers:Authorizationの値を環境変数PAPANDA_WP_AUTHから取得するenabled_tools: Codexが使えるtoolを下書き作成1個へ絞るdefault_tools_approval_mode = "prompt": MCP tool実行前に人間へ確認を求める
Codexの現行公式ドキュメントでは、env_http_headers は「header名から環境変数名へのmap」、enabled_tools はtool allow listです。承認modeには auto、prompt、writes、approve があり、今回は明示的な人間承認を優先して prompt を使います。利用中のCodex版がこの項目をサポートするか、codex mcp --help と公式ドキュメントで確認してください。
設定後は次で一覧を見られます。
codex mcp list
Codex TUI内では /mcp を使います。
参考:OpenAI Docs: Model Context Protocol、OpenAI Docs: Configuration Reference、openai/codex公式GitHub
14. 認証情報を .env で保持する
最初は現在のshellだけで PAPANDA_WP_AUTH を export していました。そのため新しいSSH loginでは変数が消え、papanda-wordpress がHTTP 401/rest_forbidden になりました。MCPの故障ではなく、Codexを起動したprocessへ環境変数が引き継がれていなかったことが原因でした。
最終的には専用directoryへ保存しました。
mkdir -p "$HOME/.config/papanda-mcp"
chmod 700 "$HOME/.config/papanda-mcp"
chmod 600 "$HOME/.config/papanda-mcp/.env"
.env の中身は、環境変数 PAPANDA_WP_AUTH をexportする1行です。ただしBasic認証に使われる値は暗号化ではありません。本稿では書式を含め実値を掲載しません。ファイルはeditorで安全に作成し、作成直後にpermissionを600へ制限します。
.bashrc から読み込みます。
if [ -f "$HOME/.config/papanda-mcp/.env" ]; then
. "$HOME/.config/papanda-mcp/.env"
fi
注意点は次のとおりです。
.envをGitへcommitしない。repositoryの.gitignoreだけに頼らずgit statusも確認する- backup、shell履歴、process dump、画面共有、support bundleにも注意する
- directoryは700、fileは600にする
- 漏えいが疑われたらApplication Passwordを失効・再発行する
- 可能ならOSのsecret storeや短命credentialへの移行も検討する
新しいSSH sessionでCodexを起動し、/mcp で次の状態を確認しました。
codex_apps: connected
papanda-wordpress: connected (1 tool)
15. 実際にCodexから下書きを作る
最終実証では、Codexへ次の趣旨で依頼しました。
papanda-wordpress MCPを使って、WordPressに確認用の下書きを1件作成してください。必ず下書きとして保存。公開しない。既存投稿を変更・削除しない。
承認画面ではtool名が papanda-mcp-create-draft で、引数が title と content だけであることを人間が確認してから許可します。未知のtool、status、既存投稿ID、削除や更新を示す引数が見えたら拒否します。
作成された内容は次のとおりです。
- タイトル:
[MCP接続テスト] CodexからWordPress下書き作成 - 本文:
Codexからpapanda-wordpress MCPを経由して作成した確認用の下書きです。 - WordPress上の状態:
下書き
MCP応答だけで終わりにせず、人間がWordPress管理画面を開き、「下書き」であることを確認しました。公開ボタンは押していません。
16. トラブルシューティング
/wp-json/... がJSONではなくトップページになる
まず ?rest_route=/... 形式と比較します。query形式だけが動くなら、NginxなどWebサーバーのpretty URL routingを疑います。MCPプラグインを変更する前に、Content-Type、HTTP status、Nginx access/error logを秘密値なしで確認します。
401または rest_forbidden
次を順番に確認します。
- 新しいshellに
PAPANDA_WP_AUTHが存在するか(値そのものは表示しない) - Codexを環境変数読込後に起動したか
- Application Passwordが失効していないか
papanda-mcpのCapabilityが期待どおりか- endpoint URLとHTTPS証明書が正しいか
tools/list に余計なtoolがある
その状態では接続しません。デフォルトサーバー無効化、カスタムサーバーのtools配列、Codexの enabled_tools を確認します。WordPress側とCodex側の両方が1個に絞られていることが重要です。
専用ユーザーを強くしたらMCPが拒否された
意図したfail closedです。publish_posts が追加されると ! current_user_can( 'publish_posts' ) がfalseになり、サーバーとAbilityの両方が拒否します。権限を強くして回避せず、なぜrole/Capabilityが変わったかを調査します。
Codexのbackend endpointで404になった
検証中、Codex 0.153.0で https://chatgpt.com/backend-api/codex/responses がHTTP 404になる事象を経験しました。切り分けのため0.151.0へ下げても同じ404が再現したため、WordPress MCPや0.153.0固有の問題ではない可能性が高いと判断しました。
記事作成時点でもopenai/codexのGitHub Issuesには同じendpointの404報告があります。ただしIssuesはユーザー報告であり、OpenAIの公式見解や恒久障害の証明ではありません。今回も「検証時に一時的なbackend側とみられる404を経験した」という範囲に留めます。後にCodexを再起動するとMCPがconnectedになり、下書き作成まで成功しました。
参考:openai/codex Issue #28756(ユーザー報告)
17. 今回のセキュリティ設計を振り返る
今回の防御を整理すると次のとおりです。
| 層 | 制御 | 失敗時の狙い |
|---|---|---|
| 人間の運用 | MCP実行を承認し、公開は管理画面だけ | 意図しない書込みを止める |
| Codex | enabled_tools を1個、approvalをprompt |
余計なtoolを選べない |
| 通信 | HTTPS 443、追加portなし | 平文送信と攻撃面を減らす |
| WordPress認証 | 専用Application Password | 通常login passwordを渡さず個別失効可能 |
| WordPressユーザー | Contributor、publish_postsなし |
AIが公開できない |
| MCP server | Abilityを1個だけ登録、server-wide check | 別Abilityを露出しない |
| Ability | title/contentのみ、draft固定 |
引数で公開へ変えられない |
| 実行時 | user名、edit_posts、! publish_postsを二重確認 |
誤った権限追加時もfail closed |
| 入力処理 | schema、sanitize_text_field()、wp_kses_post() |
想定外入力や危険なHTMLを抑える |
大事なのは「AIが指示を守るから安全」ではなく、AIが誤動作してもWordPress側で公開できないことです。ただしリスクはゼロではありません。Contributorは自分の未公開投稿を削除でき、Application Passwordは保存先から盗まれる可能性があり、下書き本文に不適切な内容が入る可能性もあります。監査ログ、rate limit、credential rotation、stage環境での試験、人間reviewを組み合わせます。
18. 今後やりたいこと
v0.1の次に検討したい項目は次です。
delete_postsも持たない専用custom roleを、既存roleへ影響させず設計する- 同一内容の重複下書きを防ぐidempotency keyを導入する
- 投稿本文の文字数上限、rate limit、監査ログ、alertを追加する
- Application Passwordの定期rotation手順を決める
- WordPress/MCP Adapter/Codex更新時の回帰testを自動化する
- AIが外部文書を読む場合のprompt injection対策を追加する
これらも「できる機能を増やす」より先に、「事故時の影響を小さくする」観点で進めます。
19. まとめ
Codex CLIからWordPressへ記事を送る仕組みは、管理者権限を渡さなくても構築できます。今回の要点は次の5つです。
- WordPressは専用Contributorユーザーにし、
publish_postsがないことを確認する - デフォルトMCPサーバーを止め、下書き作成Abilityだけを登録する
- Abilityの入力から
statusを消し、PHP内部でdraftに固定する - Codexでもtool allow listと人間のapprovalを使う
- 最後は必ず人間が管理画面で下書きを確認し、人間だけが公開する
実証では、Codexから papanda-mcp-create-draft を1回呼び、新規投稿がWordPress管理画面で「下書き」になっていることを確認できました。AIに公開権限を持たせない——この単純な境界が、実運用では最も大切です。
検証環境と情報源(2026年9月4日確認)
- WordPress 7.1
- PHP CLI 8.3.6
- WordPress公式 MCP Adapter 0.6.1
- OpenAI Codex CLI 0.151.0/0.153.0
- Nginx、HTTPS(TCP 443)
- WordPress/mcp-adapter
- MCP Adapter Releases
- WordPress REST API Handbook
- WordPress Application Passwords
- WordPress Roles and Capabilities
- OpenAI Docs: Codex MCP
- OpenAI Docs: Codex Configuration Reference
- openai/codex
バージョン、option名、APIは将来変わり得ます。導入時点の公式documentとrelease notesを確認してください。
用語集
MCP(Model Context Protocol)
AIと外部システムを共通形式で接続するプロトコルです。今回はCodexがWordPressの下書き作成toolを呼ぶために使います。詳細はMCP公式仕様を参照してください。
MCP Host / Client / Server / Tool
Hostは利用環境、ClientはServerとの接続担当、Serverは機能の提供側です。ToolはAIへ公開する操作で、今回は papanda-mcp-create-draft だけです。詳細はMCP Architectureを参照してください。
Ability / Abilities API
WordPress内の機能を入出力schema・実行処理・権限確認とともに登録する仕組みです。今回は下書き作成処理を登録し、MCP Adapterがtoolへ橋渡しします。詳細はWordPress Abilities APIを参照してください。
Application Password
外部アプリ専用に発行し個別に失効できるWordPress認証情報です。今回はHTTPS認証に使います。詳細は公式Integration Guideを参照してください。
REST API / wp-json
HTTPでWordPressの機能へアクセスする入口です。wp-json は実フォルダーではなくURL prefixです。詳細はREST API Handbookを参照してください。
Role / Capability / Contributor
Roleは権限のまとまり、Capabilityは edit_posts のような個別権限です。今回は edit_posts: YES と publish_posts: NO を確認しました。詳細はRoles and Capabilitiesを参照してください。
Nginx
HTTPS requestを受けWordPressへ渡すWeb serverです。今回は /wp-json/ を正しくrouteするために設定しました。詳細はNginx documentationを参照してください。
JSON-RPC / Streamable HTTP / curl
JSON-RPCはMCP messageの表現形式、Streamable HTTPはHTTPを使うtransport、curl はendpointを直接確認するcommandです。
.env / config.toml / enabled_tools / allowlist
.env は秘密値を環境変数として読み込むファイル、config.toml はCodex設定です。enabled_tools はtoolのallowlistです。詳細はOpenAI Codex MCPとConfiguration Referenceを参照してください。
permission check / fail closed / staging
permission checkは権限確認、fail closedは想定外なら許可せず停止する設計、stagingは本番に影響させず検証する環境です。
技術補足:CodexからWordPressへ下書きが作られるまで
sequenceDiagram
actor Human
participant Host as Codex Host
participant Client as MCP Client
participant Nginx
participant Server as MCP Adapter Server
participant Ability as Create Draft Ability
participant WP as WordPress
participant DB as WordPress DB
Human->>Host: 下書き作成を依頼・tool実行を承認
Host->>Client: MCP接続を開始
Client->>Nginx: initialize (HTTPS / JSON-RPC)
Nginx->>Server: Streamable HTTP
Client->>Server: notifications/initialized
Client->>Server: tools/list
Server-->>Client: papanda-mcp-create-draft のみ
Client->>Server: tools/call(title, content)
Server->>Server: permission check
Server->>Ability: papanda-mcp/create-draft
Ability->>Ability: permission_callback / schema validation
Ability->>WP: wp_insert_post(post_status = draft)
WP->>DB: 下書きを保存
WP-->>Client: status = draft
Client-->>Host: 実行結果
図は検証したMCP 2025-06-18 の挙動です。2026年9月4日時点の最新仕様 2026-07-28 はstateless coreへ移行し、必須のhandshakeやsessionを前提としません。実装記録と最新仕様を混同せず、実際に検証したsequenceを残しています。
TIPS記事候補
現時点では未作成のため、存在しない内部リンクは追加していません。
- WordPress Application Passwordの設定と安全な保管方法 —
wordpress-application-password-setup - Contributorで「下書きだけ」に制限する方法 —
wordpress-contributor-draft-only - Codex CLIにHTTP MCP Serverを設定する方法 —
codex-cli-http-mcp-config - curlでMCPのtools/listとtools/callを確認する —
curl-mcp-tools-list-call - wp-jsonがNginxで404になるときの確認方法 —
wordpress-wp-json-nginx-404

コメント