SECTOR-QA · TEST RANGELIVE

SYSTEM TEST RESULTS

Live MCP tool battery against demo · plugin v3.9.5 · MCP v3.9.5
Every result on this page came from a real MCP call. Nothing is mocked.

This page documents what happens when you point an MCP-compatible AI agent at a WordPress site running AgenticWP Pro. Every block below is a real tool invocation from an external Python MCP server (running on a developer machine in Slovenia) talking to this WordPress install over the REST API. Each test run is its own dated session — use the sticky nav on the left to jump between sessions and individual tests.

3
Test sessions
37
Tool invocations
36/37
Pass
1
Found + fixed
2026-04-27 · 21:23 CET

Session 1 — Initial test battery

17 tools16 passed1 known issue~286ms median latency
▶ TEST 01

check_mcp_reachability

First call any agent should make. Probes plugin /status + site root, returns latency + a hint if anything’s wrong.

responsejson
{
  "plugin": { "reachable": true, "plugin_version": "3.9.5", "latency_ms": 286.4 },
  "root":   { "reachable": true, "http_status": 200, "latency_ms": 438.3 },
  "hint": null
}
Plugin and site root both responsive. Median ~286ms.
▶ TEST 02

get_wp_status

responsejson
{
  "wordpress_version": "6.9.4",
  "php_version": "8.4.19",
  "theme": "Astra 4.13.0",
  "agenticwp_version": "3.9.5",
  "edition": "pro",
  "licensed": true,
  "mcp_version": "3.9.5",
  "version_parity": true
}
Edition: pro · Licensed: true · Version parity: true — all paid endpoints unlocked.
▶ TEST 03

Options round-trip

Three calls: write, read, delete. Verifies protected-options blocklist doesn’t false-positive.

3 callsmcp
update_option("agenticwp_test_marker", { ... })   → { success: true }
get_option("agenticwp_test_marker")               → { exists: true, value: {...} }
delete_option("agenticwp_test_marker")            → { success: true }
Object → wp_options serialized → JSON deserialized → identical structure.
▶ TEST 04

Post meta round-trip

3 calls on page id 43mcp
update_postmeta(43, "_agenticwp_test_marker", "test-write-then-delete")  → ✓
get_postmeta(43, "_agenticwp_test_marker")                              → ✓
delete_postmeta(43, "_agenticwp_test_marker")                           → ✓
Single-key post meta CRUD works. Same shape exists for term meta.
⌁ TEST 05 · PAID

update_postmeta_multi

3 meta keys written across 2 posts in a single round-trip.

request → responsemcp
update_postmeta_multi([
  { post_id: 43, meta: { _agenticwp_test_a: "value-a", _agenticwp_test_b: "value-b" } },
  { post_id: 8,  meta: { _agenticwp_test_a: "home-a" } }
])
→ { success: true, results: [
    { post_id: 43, updated_count: 2 },
    { post_id: 8,  updated_count: 1 }
] }
Cleaned up after the test.
⌁ TEST 06 · PAID

list_elementor_pages

responsejson
{
  "pages": [
    { "id": 41, "title": "Build Log", "element_count": 0 },
    { "id": 8,  "title": "Home",      "element_count": 9 }
  ],
  "total": 2
}
⌁ TEST 07 · PAID

get_elementor_outline (max_depth=1)

Lightweight tree of Home page — IDs and types only. Saves ~100x context vs get_elementor_page.

response (truncated)json
{
  "page_id": 8,
  "element_count": 9,
  "outline": [
    { "id": "hero0001", "elType": "container", "children": [...] },
    { "id": "prob0001", "elType": "container", "children": [...] },
    { "id": "feat0001", "elType": "container", "children": [...] },
    { "id": "flow0001", "elType": "container", "children": [...] },
    { "id": "tool0001", "elType": "container", "children": [...] },
    { "id": "pric0001", "elType": "container", "children": [...] },
    { "id": "faq00001", "elType": "container", "children": [...] },
    { "id": "cta0001",  "elType": "container", "children": [
      { "id": "cta0002", "widgetType": "heading", "label": "...Correctly." },
      { "id": "cta0003", "widgetType": "button",  "label": "Download AgenticWP" }
    ]}
  ],
  "truncated_count": 24
}
⌁ TEST 08 · PAID

get_elementor_element

request → responsemcp
get_elementor_element(page_id=8, element_id="cta0002")
→ {
  "widgetType": "heading",
  "settings": {
    "title": "Let your AI agent manage WordPress. Correctly.",
    "align": "center",
    "typography_font_family": "Inter",
    "typography_font_size": { "unit": "rem", "size": 2.4 }
  }
}
⌁ TEST 09 · PAID

get_elementor_components

11 component templates the plugin can build, with parameters each accepts.

component namesjson
["hero", "heading", "text", "image", "button", "video_bg",
 "products_grid", "icon_list", "category_cards", "cta_banner", "spacer"]
⌁ TEST 10 · PAID

list_elementor_revisions

response (head)json
{
  "page_id": 8,
  "revision_count": 31,
  "revisions": [
    { "revision_id": 71, "date": "2026-04-27 10:53:45", "element_count": 9 },
    { "revision_id": 9,  "date": "2026-04-22 23:44:52", "element_count": 1 }
    /* … 29 more */
  ]
}
▶ TEST 11

patch_css + css_has_selector

3-step round-tripmcp
patch_css(block: "testing_page_marker", rules: ".awp-testing-marker { display: none; }")
→ { before_bytes: 81220, after_bytes: 81418, delta: +198 }

css_has_selector(".awp-testing-marker")
→ { exists: true, count: 1 }

patch_css(block: "testing_page_marker", mode: "remove")
→ { before_bytes: 81418, after_bytes: 81220, delta: -198 }
CSS file size returned to exactly the pre-test value (81220 bytes). Idempotent + reversible.
▶ TEST 12

seo_audit

aggregatejson
{
  "total_posts": 11,
  "summary": {
    "missing_title": 0, "missing_description": 0, "missing_focus_keyword": 0,
    "missing_og_image": 0, "thin_content": 0, "noindex": 0
  }
}
11 posts scanned, zero issues.
▶ TEST 13

list_menu_locations

responsejson
{
  "locations": [
    { "location": "primary",       "menu_name": "Primary", "item_count": 7 },
    { "location": "mobile_menu",   "menu_name": "Primary", "item_count": 7 },
    { "location": "footer_menu",   "menu_name": null,      "item_count": 0 }
  ],
  "all_menus": [
    { "id": 2, "name": "Primary",      "count": 7 },
    { "id": 3, "name": "Footer Legal", "count": 5 }
  ]
}
▶ TEST 14

flush_cache

responsejson
{ "success": true,
  "flushed": ["object_cache", "elementor_css", "astra_css", "rewrite_rules"] }
▶ TEST 15

get_debug_log

responsejson
{ "exists": false,
  "message": "debug.log does not exist. Enable WP_DEBUG_LOG to start capturing errors." }
Demo runs with WP_DEBUG_LOG = false. Endpoint correctly reports the file’s absence.
⚠ TEST 16 · KNOWN ISSUE

list_elementor_widgets — 500

The endpoint failed during this run. Investigation → root cause → fix applied. See Session 2 below for verification.

responsejson
GET /elementor/widgets failed (500):
{ "code": "internal_server_error",
  "message": "There has been a critical error on this website." }
Root cause: $widget->init_controls() is a protected method on Elementor\Widget_Base in current Elementor versions. Calling it from outside the class throws a fatal Error. Fixed by switching to the public get_controls() API. See Session 2 for verification.
2026-04-27 · 22:00 CET

Session 2 — list_elementor_widgets fix verification

3 mode tests3/3 passed0 regressions152 widgets enumerated

What changed: the endpoint was rewritten in two phases. (1) Default mode flipped from heavy-introspection to names only — same data path that 500’d is now opt-in via ?mode=full. (2) The protected-method bug was fixed by switching from init_controls() + get_stack() to the public get_controls() API. ?mode=meta is the new middle ground returning {name, title, icon, categories}.

⌁ SESSION 2 · TEST 01

mode=names (new default)

response (head)json
{
  "widgets": [
    "accordion", "alert", "animated-headline", "audio", "author-box",
    "button", "call-to-action", "countdown", "counter", "divider",
    /* … 142 more */
  ],
  "total": 152,
  "mode": "names"
}
152 widget slugs, ~50ms response. No init_controls() walk — just array_keys() on the already-loaded registry. Ridiculously fast and never fatals.
⌁ SESSION 2 · TEST 02

mode=meta

response samplejson
{
  "widgets": [
    { "name": "button",  "title": "Button",  "icon": "eicon-button",  "categories": ["basic"] },
    { "name": "heading", "title": "Heading", "icon": "eicon-t-letter", "categories": ["basic"] },
    { "name": "form",    "title": "Form",    "icon": "eicon-apps",   "categories": ["general", "pro-elements"] }
    /* … 149 more */
  ],
  "total": 152,
  "mode": "meta"
}
152 widgets categorized. Categories observed on this install: basic, general, pro-elements, link-in-bio, v4-elements, wordpress.
⌁ SESSION 2 · TEST 03

mode=full — the original failing path, after fix

response samplejson
{
  "widgets": [
    {
      "name": "button",
      "title": "Button",
      "icon": "eicon-button",
      "categories": ["basic"],
      "controls": {
        "button_section":  { "type": "section",   "label": "Button",   "default": null },
        "text":            { "type": "text",      "label": "Text",     "default": "Click here" },
        "link":            { "type": "url",       "label": "Link",     "default": {...} },
        "size":            { "type": "select",    "label": "Size",     "default": "sm" }
      }
    }
    /* … 151 more widgets */
  ],
  "total": 152,
  "mode": "full",
  "skipped": [],
  "skipped_count": 0
}
0 skipped, 152 returned with controls. Total payload: 101,826 chars / 4,376 lines / 479 controls across all widgets. Use mode=full only when you need controls; otherwise stick to meta or names.
2026-04-27 · 22:30 CET

Session 3 — Free-tier battery (license deliberately disabled)

17 tools17/17 passed2 paid endpoints correctly gated~161ms latency

What this session proves: every free-tier endpoint works without a license. Every paid endpoint returns a structured 402 instead of leaking. License was disabled in WP Admin between Sessions 2 and 3, then re-enabled at the end so the page could be saved. Side-effect bonus: the SEO audit caught that this Testing page had no Rank Math meta, and update_postmeta_bulk set it in the same session.

▶ SESSION 3 · TEST 01

check_mcp_reachability

responsejson
{
  "plugin": { "reachable": true, "plugin_version": "3.9.5", "latency_ms": 161.5 },
  "root":   { "reachable": true, "http_status": 200, "latency_ms": 672.1 },
  "hint": null
}
Plugin endpoint responsive at 161ms — ~120ms faster than Session 1’s 286ms (Cloudflare edge cache warm).
▶ SESSION 3 · TEST 02

get_wp_status — unlicensed

responsejson
{
  "agenticwp_version": "3.9.5",
  "edition": "pro",
  "licensed": false,
  "version_parity": true,
  "readonly_mode": false
}
Same Pro plugin install, but license was deliberately turned off in Settings → AgenticWP for this session. licensed: false tells the MCP to expect 402s on paid endpoints.
▶ SESSION 3 · TEST 03

Paid endpoints — gating verified (402)

Two representative paid endpoints called with no license. Both return a structured 402, not a 5xx — the front-door auth layer rejects cleanly before any business logic runs.

2 paid calls → 2 × 402mcp
list_elementor_pages()
→ GET /elementor/pages failed (402):
   { "code": "agenticwp_unlicensed",
     "message": "This endpoint requires an AgenticWP license. Add one under Settings → AgenticWP." }

media_upload({ url: "https://example.com/test.png" })
→ POST /media/upload failed (402):
   { "code": "agenticwp_unlicensed",
     "message": "This endpoint requires an AgenticWP license. Add one under Settings → AgenticWP." }
Gating works as designed. Both endpoints return the same structured response shape so an agent can detect license-failure programmatically and react (re-prompt the user, fall back to free-tier alternatives).
▶ SESSION 3 · TEST 04

Options round-trip

3 callsmcp
update_option("agenticwp_test_session_3", { timestamp, purpose, license_state: "disabled" })
→ { success: true, updated: true }

get_option("agenticwp_test_session_3")
→ { exists: true, value: { ... } }

delete_option("agenticwp_test_session_3")
→ { success: true }
JSON object round-tripped through PHP serialize → wp_options → unserialize → JSON without drift.
▶ SESSION 3 · TEST 05

Post meta CRUD

on this Testing page (id 97)mcp
update_postmeta(97, "_agenticwp_session3_marker", "free-tier-test-2026-04-27")  → ✓
delete_postmeta(97, "_agenticwp_session3_marker")                              → ✓
▶ SESSION 3 · TEST 06

Term meta CRUD

Same shape as post meta but for taxonomy terms (categories, tags, custom taxonomies). Useful for setting Rank Math SEO on category archive pages.

on category id 1 (Uncategorized)mcp
update_termmeta(1, "_agenticwp_session3_term_marker", "term-meta-test")  → ✓
get_termmeta(1, "_agenticwp_session3_term_marker")                       → { exists: true, value: "term-meta-test" }
delete_termmeta(1, "_agenticwp_session3_term_marker")                    → ✓
▶ SESSION 3 · TEST 07

update_postmeta_bulk — free-tier (single post, multi key)

Different from update_postmeta_multi (paid, many posts). Free tier writes N keys to ONE post atomically.

on page id 97mcp
update_postmeta_bulk(97, {
  _agenticwp_s3_a: "alpha",
  _agenticwp_s3_b: "bravo",
  _agenticwp_s3_c: "charlie"
})
→ { success: true, updated_count: 3 }
3 keys in one round-trip vs 3 separate update_postmeta calls. Cleaned up after.
▶ SESSION 3 · TEST 08

SEO meta bulk-write — closed-loop fix

The SEO audit (test 10 below) found this page had no Rank Math meta. Same session used update_postmeta_bulk to fix it.

on this Testing pagemcp
update_postmeta_bulk(97, {
  rank_math_title: "Live Test Range — MCP Tool Battery Results | AgenticWP",
  rank_math_description: "Live test results from running the AgenticWP MCP tool battery against this WordPress demo. Every block on the page is a real tool call. Multiple test sessions documented.",
  rank_math_focus_keyword: "agenticwp test results"
})
→ { success: true, updated_count: 3 }
Audit → fix → re-audit cycle in one MCP session, no human in the loop.
▶ SESSION 3 · TEST 09

patch_css + css_has_selector

3-step round-tripmcp
patch_css(block: "session3_marker", rules: ".awp-s3-marker { display: none; }")
→ { before_bytes: 85839, after_bytes: 86010, delta: +171 }

css_has_selector(".awp-s3-marker")
→ { exists: true, count: 1 }

patch_css(block: "session3_marker", mode: "remove")
→ { before_bytes: 86010, after_bytes: 85839, delta: -171 }
Byte-perfect reversal (85839 → 85839). Atomic + idempotent.
▶ SESSION 3 · TEST 10

seo_audit — caught one real issue

aggregate (3 of 12 posts scanned)json
{
  "total_posts": 12,
  "summary": {
    "missing_title": 1,
    "missing_description": 1,
    "missing_focus_keyword": 1,
    "missing_og_image": 1
  },
  "items[0]": {
    "id": 97,
    "title": "Testing",
    "issues": ["missing_title", "missing_description", "missing_focus_keyword", "missing_og_image"]
  }
}
⚠→✓Audit caught the Testing page (id 97) missing all Rank Math fields — then test 08 fixed it via update_postmeta_bulk in the same session.
▶ SESSION 3 · TEST 11

list_menu_locations

responsejson
{
  "locations": [
    { "location": "primary",     "menu_name": "Primary", "item_count": 8 },
    { "location": "mobile_menu", "menu_name": "Primary", "item_count": 8 },
    { "location": "footer_menu", "menu_name": null,      "item_count": 0 }
  ],
  "all_menus": [
    { "id": 2, "name": "Primary", "count": 8 }
  ]
}
Primary menu now 8 items (vs 7 in Session 1) — the Testing page itself was added to the menu since the original test, and the count reflects that automatically.
▶ SESSION 3 · TEST 12

flush_cache

responsejson
{ "success": true,
  "flushed": ["object_cache", "elementor_css", "astra_css", "rewrite_rules"] }
▶ SESSION 3 · TEST 13

get_debug_log

responsejson
{ "exists": false,
  "message": "debug.log does not exist. Enable WP_DEBUG_LOG to start capturing errors." }
Same gentle no-op as Session 1. Endpoint never crashes when debug log is disabled — it tells the agent why there’s nothing to read.
▶ SESSION 3 · TEST 14

find_pages by title

request → responsemcp
find_pages(title="Testing")
→ { count: 1, pages: [
    { id: 97, slug: "testing", link: ".../testing/", is_elementor: false }
] }
Lightweight finder — returns just the metadata an agent needs to follow up with update_postmeta_bulk, update_elementor_page, or get_elementor_outline.
▶ SESSION 3 · TEST 15

wp_rest_call — generic REST passthrough

For any WordPress REST endpoint that doesn’t have a dedicated AgenticWP wrapper. Free-tier escape hatch.

request → responsemcp
wp_rest_call(method="GET", path="/users/me", query={ _fields: "id,name" })
→ { status: 200, data: { id: 1, name: "glava" } }
Authenticated as the WP user the MCP was configured with. Useful for any /wp/v2/* endpoint not wrapped by a dedicated tool.
▶ SESSION 3 · TEST 16

render_page — unauthenticated public fetch

Python-side fetch that returns the visitor’s-eye view of a page. No auth, no plugin endpoint — useful to verify what visitors actually see after a save.

request → responsemcp
render_page(url="https://demo-agenticwp.racunalnicar.eu/manual/")
→ {
  "title": "Operations Manual — Install, Configure, Deploy | AgenticWP",
  "size_bytes": 190551,
  "stylesheets_count": 3,
  "inline_style_blocks": 5,
  "inline_css_total_bytes": 150033
}
Returns enough structural detail (title, size, stylesheet counts) to verify a page change rendered correctly without a headless browser.
⌁ STATUS

Range clear · 3 sessions

seba@agenticwp:~$ agenticwp-mcp test –battery=full –sessions=3
3 test sessions · 37 tool invocations total
Session 1 (paid · 21:23): 17 tests · 16 passed · 1 found-and-fixed
Session 2 (fix verify · 22:00): 3 tests · 3 passed
Session 3 (free-tier · 22:30): 17 tests · 17 passed · 2 paid endpoints correctly returned 402
test data: all written + cleaned up across all 3 sessions
css patches: 3 round-trips, all reverted byte-perfect
real fixes: Testing page Rank Math SEO meta set via update_postmeta_bulk

# every block above came from a real network call. nothing on this page is mocked.

seba@agenticwp:~$
Scroll to Top