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?

mcp__wordpress__update-post が固定ページで404を返す理由と、REST APIで直接解決する方法

0
Posted at

mcp__wordpress__update-post で固定ページを更新しようとすると 404 Not Found が返る。原因はエンドポイントの取り違えだ。

このMCPツールは内部で /wp-json/wp/v2/posts/{id} を叩く。固定ページのエンドポイントは /wp-json/wp/v2/pages/{id} であり、別リソースとして管理されている。固定ページのIDを /posts/{id} で指定しても該当リソースが存在しないため、404が返る。


投稿と固定ページはWordPress REST APIで別リソース

リソース エンドポイント
投稿(ブログ記事) /wp-json/wp/v2/posts/{id}
固定ページ /wp-json/wp/v2/pages/{id}

MCPツールが /posts だけに対応している以上、固定ページの更新には使えない。直接REST APIを叩く必要がある。


MCPサーバーと直接呼び出しで通信経路がどう変わるか

MCPツール経由の場合、通信経路はこうなっている。

Claude Code
  └─ mcp__wordpress__update-post
       └─ MCPサーバー(ミドルウェア)
            └─ /wp-json/wp/v2/posts/{id}   ← posts 固定

MCPサーバーがWordPressの認証情報を自身で保持しているため、Claude側はAPIキーを意識しなくてよい。その代わり、MCPサーバーの実装に縛られ、対応外のエンドポイントには手が届かない。

直接呼び出しはこうなる。

Claude Code(Pythonスクリプト)
  └─ HTTP POST(Basic認証ヘッダー付き)
       └─ /wp-json/wp/v2/pages/{id}        ← pages を直接指定

認証情報を自前で用意すれば、WordPressのREST APIが提供する任意のエンドポイントを自由に操作できる。


アプリケーションパスワードによるBasic認証

REST APIの認証方式はいくつかあるが、スクリプト用途にはアプリケーションパスワードが適している。WordPress 5.6から標準搭載されており、プラグイン不要で発行できる。JWT認証はプラグインが必要なうえトークンの有効期限管理が発生する。Cookie認証はブラウザセッション依存でスクリプトからは使いにくい。

発行は WordPress管理画面の ユーザー → プロフィール を開き、「アプリケーションパスワード」セクションで任意の名前を入力して「追加」をクリックするだけだ。表示されたパスワードは再表示されないのでその場でコピーする。

認証の仕組みはHTTP Basic認証そのものだ。ユーザー名:アプリケーションパスワード をBase64エンコードして Authorization ヘッダーに乗せる。

from base64 import b64encode

WP_USER     = "your_username"
WP_APP_PASS = "xxxx xxxx xxxx xxxx xxxx xxxx"  # スペース入りのままでOK

auth = b64encode(f"{WP_USER}:{WP_APP_PASS}".encode()).decode()

headers = {
    "Authorization": f"Basic {auth}",
    "Content-Type": "application/json",
}

アプリケーションパスワードはスペース区切りで発行されるが、そのままBase64エンコードして問題ない。WordPressはスペースを無視して検証する。


固定ページを更新するPythonスクリプト

実装は「認証セットアップ」「コンテンツ抽出」「送信」の3ステップに分けると見通しがよい。

認証セットアップ

認証情報は .env で管理し、コードにハードコードしない。python-dotenv を使わず標準ライブラリだけで完結させる場合、partition('=') で各行を分割して os.environ にセットするのがシンプルだ。

import os, re, json, ssl, urllib.request
from base64 import b64encode

# .env を読み込んで環境変数にセット
with open(".env") as f:
    for line in f:
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        k, _, v = line.partition('=')
        os.environ.setdefault(k.strip(), v.strip())

WP_USER = os.environ["WP_USER"]
WP_PASS = os.environ["WP_APP_PASS"]
AUTH    = b64encode(f"{WP_USER}:{WP_PASS}".encode()).decode()

# サーバー環境によってSSL証明書エラーが出る場合の対処
CTX = ssl.create_default_context()
CTX.check_hostname = False
CTX.verify_mode    = ssl.CERT_NONE

HTMLからコンテンツを抽出してラップ

HTMLファイルからラッパー要素を正規表現で抜き出し、<!-- wp:html --> ブロックでラップする。このラップが抜けると後述のwpautopが発動してCSSが壊れる。

with open("article.html", encoding="utf-8") as f:
    html = f.read()

# re.DOTALL で改行をまたいでマッチ
m = re.search(r'(<div class="your-wrapper">.*</div>)', html, re.DOTALL)
body = m.group(1).strip()

# Gutenbergのカスタムブロックとして扱わせるためラップ
wp_content = f"<!-- wp:html -->\n{body}\n<!-- /wp:html -->"

REST APIに送信

/wp-json/wp/v2/pages/{id} がターゲット。投稿との違いはここだけだ。

payload = json.dumps({
    "content": wp_content,
    "meta": {
        "_yoast_wpseo_title":    "ページのSEOタイトル",
        "_yoast_wpseo_metadesc": "ページのmeta description",
    },
}, ensure_ascii=False).encode("utf-8")

url = "https://your-site.com/wp-json/wp/v2/pages/2032"
req = urllib.request.Request(
    url,
    data    = payload,
    headers = {"Authorization": f"Basic {AUTH}", "Content-Type": "application/json"},
    method  = "POST",
)
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=CTX))
with opener.open(req, timeout=90) as resp:
    result = json.loads(resp.read().decode())

print("ID:    ", result.get("id"))
print("Link:  ", result.get("link"))
print("Status:", result.get("status"))

レスポンスの idlink が返ってきていれば更新成功だ。status: publish であればそのまま公開済みになっている。


wpautopによるCSS崩壊を防ぐ <!-- wp:html --> ラップ

WordPressはコンテンツ保存時に wpautop というフィルタを通す。連続する改行を <br><p> タグに自動変換する機能で、スタイルを作り込んだHTMLをそのまま送るとCSSの { } の前後に <p> タグが挿入されてスタイルが完全に崩壊する。

<!-- wp:html --> で囲むことで、Gutenbergの「カスタムHTMLブロック」として認識される。このブロック内ではwpautopが発動しない。

ラップなし — wpautopが発動してCSSが崩壊する

<div class="wrapper">
  <style>.hero { padding: 48px; }</style>
  <!-- <p>タグが挿入されてCSSが壊れる -->
</div>

ラップあり — HTMLがそのまま保存される

<!-- wp:html -->
<div class="wrapper">
  <style>.hero { padding: 48px; }</style>
</div>
<!-- /wp:html -->

更新前に既存コンテンツを取得するなら ?context=edit が必要

更新前に現在のコンテンツを確認したい場合、?context=edit クエリパラメータが必要になる。

このパラメータなしでGETすると content.rendered(WordPressがHTML変換済みのもの)しか返ってこない。content.raw(保存されている元のブロックマークアップ)を取得するには context=edit が必須だ。

url = "https://your-site.com/wp-json/wp/v2/pages/2032?context=edit"
req = urllib.request.Request(
    url,
    headers = {"Authorization": f"Basic {AUTH}"},
    method  = "GET",
)
with opener.open(req, timeout=30) as resp:
    page = json.loads(resp.read().decode())

raw_content = page["content"]["raw"]

取得したRAWコンテンツが <!-- wp:html --> でラップされているかも確認しておくとよい。Gutenberg以外のエディタで作成したページはラップされていないことがあり、その状態でPOSTするとwpautopが発動する。


よく使うREST APIエンドポイント

操作 メソッド エンドポイント
投稿 取得・更新 GET / POST /wp-json/wp/v2/posts/{id}
固定ページ 取得・更新 GET / POST /wp-json/wp/v2/pages/{id}
投稿・固定ページ 新規作成 POST /wp-json/wp/v2/posts /wp-json/wp/v2/pages
メディアアップロード POST /wp-json/wp/v2/media
カテゴリ取得 GET /wp-json/wp/v2/categories
タグ 取得・作成 GET / POST /wp-json/wp/v2/tags

サイトで利用可能なエンドポイントの全量は /wp-json/wp/v2 をGETすると確認できる。カスタム投稿タイプを使っている場合はここに独自のルートが追加されている。


MCPが届かない場所はREST APIを直接叩く

MCPツールは認証やエンドポイントの管理を隠蔽してくれる。その分、実装の範囲外の操作はできない。

アプリケーションパスワードとPython標準ライブラリの組み合わせであれば、外部依存ゼロでWordPressのあらゆるREST APIエンドポイントに対応できる。MCPでカバーできる操作はMCPに任せ、固定ページ・メディア・カスタム投稿タイプなど対応外の操作は直接呼び出しに切り替える。判断基準はシンプルで、エンドポイントが /posts かどうかだけだ。

WordPress REST APIのリファレンスは developer.wordpress.org/rest-api/reference/ にある。MCPで詰まったときの手がかりになる。

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?