Skip to content

WPShout Newsletter

Sign up for news, tips, and insights to help you build better websites.

Newsletter - Top banner

WordPress REST API vs MCP: Application Passwords, OAuth and When You Need Each

We tested the two ways to put an AI to work on a WordPress site: the REST API with an Application Password, and an MCP plugin signed in with OAuth. On a test WooCommerce store, we had an AI do the same three tasks through each route: fix a typo, add a product to a category, and list the last 7 days of comments.

Both routes completed all three. What actually differed was everything around the task: the credential each side held (its scope and expiry), the side effects of calling raw endpoints, and what each method left behind on the site. This walkthrough shows the tasks request by request, with excerpts of the real commands and tool calls (the test host replaced by example.com), what the chat apps did with a pasted password, what survives an uninstall, and when plain REST plus an Application Password is the better choice.

Wordpress REST API VS MCP

Let’s jump right into it.

The test setup 🔬

The three tasks below ran on 9 November 2026 against a sandbox store (Harbor Coffee Co.) running WordPress 7.1.4, WooCommerce 11.2.2 and Easy MCP AI 2.4.0. Easy MCP AI is built by Themeisle, the team behind WPShout. The AI doing the work was Claude Code running headless on Sonnet, and every task ran three times per route.

Route one: curl with an Application Password, passed from environment variables as $WP_USER:$WP_APP_PASSWORD. Route two: the same model connected to the site’s WordPress MCP tools through Easy MCP AI, authorized over OAuth.

Two request paths: AI client to curl with an Application Password to wp-json, and AI client to MCP tools over OAuth

The three tasks, side by side 🔧

Task 1: fix the typo

The prompt: Fix the typo in the post ‘How to Brew Better Pour-Over at Home’. The post (ID 15) had “recieve” in its second paragraph. The REST route searched with context=edit, then posted the corrected content back:

curl -s -u "$WP_USER:$WP_APP_PASSWORD" "https://example.com/wp-json/wp/v2/posts?search=Pour-Over&context=edit&status=any" | python3 -c "import sys,json;[print(p['id'],p['status'],p['title']['raw'],'\n',p['content']['raw']) for p in json.load(sys.stdin)]"

U=https://example.com/wp-json/wp/v2/posts/15
curl -s -u "$WP_USER:$WP_APP_PASSWORD" "$U?context=edit" | python3 -c "import sys,json;print(json.dumps({'content':json.load(sys.stdin)['content']['raw'].replace('recieve','receive')}))" > body.json
curl -s -u "$WP_USER:$WP_APP_PASSWORD" -X POST -H 'Content-Type: application/json' -d @body.json "$U?context=edit" | python3 -c "import sys,json;p=json.load(sys.stdin);print(p.get('status'),p.get('code'),'receive' in p['content']['raw'],'recieve' in p['content']['raw'])"

The MCP route loaded tool schemas on demand, then called named tools:

ToolSearch: {"query": "select:mcp__site__wp_search_posts,mcp__site__wp_get_post,mcp__site__wp_replace_in_post", "max_results": 5}
wp_search_posts: {"query": "How to Brew Better Pour-Over at Home", "subtype": "post"}
wp_get_post: {"post_id": 15}
wp_replace_in_post: {"id": 15, "field": "post_content", "search": "recieve", "replace": "receive", "expected_count": 1}Code language: JavaScript (javascript)

Both routes went 3/3, and only the typo changed in all six runs. REST took 2 shell calls per run at 13.3 to 19.3 seconds; MCP took 3 to 4 tool calls at 14.1 to 16.3 seconds. One detail worth noting: no REST run ever fetched the /wp-json/ route index. The model already knew the WordPress and WooCommerce routes and wrote its curl calls from memory.

Task 2: add the product to a category

The prompt: Add the product ‘Hand Grinder’ to the ‘Brewing Gear’ category. The product (ID 12) sat in Uncategorized (term 15); Brewing Gear is term 17. Two of the three REST runs dropped Uncategorized; this is run 2’s command:

B=https://example.com/wp-json; curl -s -X PUT -u "$WP_USER:$WP_APP_PASSWORD" -H "Content-Type: application/json" -d '{"categories":[{"id":17}]}' "$B/wc/v3/products/12" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("name"), d.get("categories"), d.get("code"))'

All three REST runs landed the product in Brewing Gear (2 shell calls each, 12.7 to 14.4 seconds), but 2 of 3 replaced the whole category list: the product ended up in term 17 only, without Uncategorized. PUT /wc/v3/products/<id> with a categories array replaces the list, so a model that sends only the new id drops the old one. The remaining run sent both ids, {"categories":[{"id":15},{"id":17}]}, and kept both.

The MCP side ended every run with the product in both categories, [15, 17], because its term tool appends:

wp_add_post_terms: {"post_id": 12, "taxonomy": "product_cat", "terms": [17]}Code language: JavaScript (javascript)

That route took more calls (6 to 9 per run, one run with an error) at 14.7 to 18.7 seconds, since the model first located the product, the taxonomy and the term. For more on store work over MCP, see WooCommerce MCP.

Task 3: list the comments from the last 7 days

The prompt: List the comments from the last 7 days with author, date and status. REST did it in a single command:

SINCE=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S); curl -s -u "$WP_USER:$WP_APP_PASSWORD" -G "https://example.com/wp-json/wp/v2/comments" --data-urlencode "after=$SINCE" --data-urlencode "status=all" --data-urlencode "per_page=100" --data-urlencode "orderby=date" --data-urlencode "order=desc" | jq -r 'if type=="array" then (.[] | [.author_name, .date, .status] | @tsv) else . end'

MCP took two tool calls:

ToolSearch: {"query": "select:mcp__site__wp_list_comments", "max_results": 1}
wp_list_comments: {"after": "2026-11-02T00:00:00", "status": "any", "per_page": 100}Code language: JavaScript (javascript)

Both routes went 3/3, and every run listed 7 comments, including the one held for moderation. The REST run computed a rolling seven-day cutoff, while the MCP call shown used midnight on 2 November; both routes reported seven comments, including the pending one. REST ran 8.7 to 10.7 seconds per attempt, MCP 9.5 to 10.8. Across the task runs the ranges largely overlap: REST 8.7 to 19.3 seconds, MCP 9.5 to 18.7 seconds. Speed is not the story here.

The chat apps can do this too 📱

Both setups above used Claude Code, which ran Bash commands for the REST route. A natural follow-up: can a plain chat app call the REST API itself? Given the site URL, a username and an Application Password pasted into the prompt, yes:

  • claude.ai reported “Ran 3 commands” and fixed the same typo in under 15 seconds. Its closing advice: “Since this Application Password has now been shared in a chat, it’s a good idea to revoke it”.
  • ChatGPT reported “Worked for 1m 17s” and “Ran 5 commands” (searching posts, testing the endpoint, then correcting and verifying the spelling).
claude.ai reports Ran 3 commands after fixing the post typo through the REST API; the pasted password is hidden
ChatGPT reports Worked for 1m 17s and Ran 5 commands fixing the typo through the REST API; the password is hidden

One observation each, not a benchmark. The point is the credential: an unscoped, non-expiring password now sits in a chat history, it works until someone revokes it, and the only record of what it did is the post revision. For a connection without a pasted password, see how to connect Claude to WordPress.

What each method leaves on your site 🧾

After the same tasks, the two routes leave different records behind.

REST + Application Password: post revisions in the password owner’s name (admin here) for the post edits, and an updated Last Used date and Last IP on the password’s row under Users > Profile > Application Passwords. In core WordPress that is the whole trace: there is no per-call log, no before and after, and nothing at all for the WooCommerce category change. If several apps share one password, they overwrite each other’s Last Used and Last IP.

MCP via Easy MCP AI: the same core revisions, plus an audit log with one row per call (tool, result, client, user, IP), plus a before/after change history for every write, attributed to “admin / AI client (OAuth)” and kept for 30 days by default. The category change is recorded there too.

Easy MCP AI change history listing AI edits to one post with date, tool, action, object and the account, admin / AI client (OAuth)

Undo: post content edits can be undone from the change history (verified on post 15, which was restored to its pre-edit content).

Scope and lifetime: what each credential can reach 🔑

An Application Password is generated per user on the profile screen. It authenticates REST API requests over HTTPS as Basic auth, and it works for XML-RPC in lieu of the account’s real password. It cannot log in to wp-admin. Its scope is the user: anything that account can do through the REST API, the password can do. Per-password scoping does not exist yet; as the core integration guide puts it, “In future versions, the expectation is to include the ability to scope a given application password to limit its access.”

There is no expiry either. The profile list shows Name, Created, Last Used, Last IP and Revoke, and a password works until you revoke it, one by one or all at once.

Application Passwords list under Users Profile showing Name, Created, Last Used, Last IP and Revoke columns; the IP is hidden

Easy MCP AI adds narrowing and expiry. Its OAuth consent screen says the connection will act on the site as a named account “and never beyond what that account can do”, and you pick an access level: Full access, Match a role (Editor, Author, Contributor, Subscriber, Customer or Shop manager), Read-only, or Custom per category. The MCP authorization spec (optional, and specific to HTTP transports) says authorization servers SHOULD issue short-lived access tokens. Easy MCP AI’s access tokens last 1 hour, its refresh tokens last 30 days and rotate, and a reused old refresh token is refused. Any connection can be revoked in the admin.

One more boundary, straight from the audit log. Asked to create a new Application Password through its REST passthrough tool, Easy MCP AI refused:

Error: Application Password routes cannot be called through these tools: a new password would outlive this key and bypass its tool list.

What survives uninstall and revoke 🧹

After the task runs, Easy MCP AI was uninstalled (wp plugin uninstall easy-mcp-ai --deactivate). Its tables and options were gone, and its endpoint went dead. The Application Password still answered:

MCP endpoint with OAuth token: 404 {"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}
REST users/me with App Password: 200 {"id":1,"roles":["administrator"]}Code language: JavaScript (javascript)

The password keeps working because it lives in WordPress core, not in any plugin. After revoking both Application Passwords on the site (wp user application-password delete admin <uuid> for each):

REST users/me with App Password: 401 {"code":"incorrect_password","message":"The provided password is an invalid application password.","data":{"status":401}}Code language: JavaScript (javascript)

Revoking ends either credential. The difference is what exists to revoke: a row in core that any plugin removal leaves untouched, or tokens that die with the plugin.

When the REST API is the better choice ✅

Reach for REST plus an Application Password when:

  • Scripts and cron jobs, where no AI is deciding anything and a tool layer adds nothing.
  • n8n, Zapier and similar platforms with fixed workflows.
  • Agents that can run code anyway. As the tests show, the model already knows the routes.
  • Sites where no plugin may be installed. Application Passwords are core.

It is also not either/or. Easy MCP AI exposes REST read and write tools of its own (wp_rest_read, wp_rest_write), so a connected AI client can still call REST routes through the plugin, with each call landing in the audit log. Application Password routes are refused, as shown above.

👉 Get Easy MCP AI from the WordPress plugin directory.

MCP vs API: the summary table 📊

REST API + Application PasswordMCP via Easy MCP AI
CredentialA per-user Application Password, sent as Basic authAn OAuth connection approved on the site’s consent screen (API key also possible)
ScopeEverything the user can do through the REST API, plus XML-RPC if enabled; no per-password scopingNever beyond the connected account; narrow it with access levels (role match, read-only, custom)
ExpiryNone; valid until revokedAccess token 1 hour; refresh token 30 days, rotating
Side effects of raw endpointsPUT with only the new category id dropped the existing one in 2 of 3 test runswp_add_post_terms appended the category in every run; the REST passthrough tools are logged
Record on the sitePost revisions plus the password’s Last Used date and Last IPRevisions plus a per-call audit log and a change history of every write (30 days by default)
After the plugin is removedUnaffected; the password lives in WordPress coreEndpoint answers 404 rest_no_route; tokens die with the plugin
RevokingOne by one, or all of a user’s at once; 401 incorrect_password afterwardsRevoke the connection in the admin
Best forScripts, n8n/Zapier, fixed workflows, code-running agents, no-plugin sitesAI clients doing varied work where a scoped, expiring credential and a written record matter

Whichever route you use, keep the credential small, watch what it leaves behind, and revoke it the day you stop needing it.

Yay! 🎉 You made it to the end of the article!
WPShout Editorial
Share:

0 Comments
Most Voted
Newest Oldest