- fix(schema-compat): prefer 'string' in non-null scalar unions to fix SanitizeSchemaUnionItemsTypeTest
- fix(content): define featured_image output properties and return resilient empty object structure for posts without thumbnails
- feat(content): add get_allowed_protected_meta() with WooCommerce and paywall whitelist
- feat(user): add sanitized meta support to create-user, update-user, and get-user with privilege escalation guard
- feat(database): support 'query' argument alias in emcp-tools/query
- feat(woo): expand create/update-product with author/vendor ownership, virtual/downloadable, prices, categories, tags, and meta; add list-vendors
- test: add 14 new automated unit tests across User, Content, Woo, and Database suites
- EMCP_Tools_Migration_Engine: shared transfer engine — the four private
helpers that lived in the migrate abilities moved here verbatim
(push_packets/finalize/poll_job) plus push_archive orchestration.
- EMCP_Tools_Migrate_Targets: {prefix}emcp_migrate_targets store, encrypted
secrets (never returned), single-use pairing-code redemption. Connector is
now 1.2.0: adds /pair/issue, open /pair/exchange (HTTPS-gated), /verify, a
settings page that issues codes, and advertises scope-restore.
- EMCP_Tools_Sync_Engine + Packager scope opts: full/selective .emcp sync
archives (db all|none|tables[], file_roots all|none|roots[]) with additive
scope+kind manifest keys (absent scope = full; format_version stays 2).
- Scope-aware restore on both sides: search-replace gated on a real DB import
and restricted to archived tables; connector no longer errors on DB-less
(files-only) archives.
- MCP surface: migrate-site gains optional target_id (raw remote_url+secret
back-compat kept); sync-to-live is now the documented scoped push; new
url-search-replace tool carries the old local search-replace body
(disabled-by-default via DEFAULTS_VERSION 37, drift-guard + catalog wired).
- Admin: Backup & Migrate tab split into Archives | Push to Site | Sync with
paired-target add/delete/verify + blocking push/sync handlers; adds
assets/js/migrate.js and pro-manifest entry.
- Tests: migrate/packager assert suite extended (scope normalization, root
traversal guards, scoped archive manifests, files-only has no DB dump).
Co-Authored-By: Claude <[email protected]>
Code-review findings verified against the current code; only still-valid issues
fixed, minimal churn, no .emcp-format or push-protocol changes.
- migrate-site: schema now accepts direction "pull" so the explicit
unsupported error is reachable (matches the docs); a restore is reported as
success only when the destination job state is "done" — pending/unknown/
queued return a WP_Error carrying the job details.
- search-replace engine: walk_table() with before_cap=0 no longer sets
partial (no before-image capture is not truncation).
- connector: hashless archives (no database_sha256) now import instead of
failing integrity; import statement errors stay under db stats (non-fatal)
so a partial import still gets the URL rewrite, while stream/hash failures
remain fatal and drive the job state; destination URL is captured before the
import replaces wp_options; rewrite_rows/search_replace report partial when
the per-table cap is hit so a run can never silently claim done.
- restore engine: destination URL captured before import; hash-mismatch abort
no longer double-closes/cleans (the finally block owns cleanup); DB stream
extraction requires a successful stream_copy_to_stream and closes handles on
every failure path.
- packager: list_archives() no longer hashes every archive (full read per row
on a plain listing); create_archive validates addFile/addFromString/close and
removes a partial archive on failure; default filenames gain a random suffix
and the backups dir gets .htaccess/index.html web-denial guards.
- search-replace wrapper guards on the engine class existing.
- migrate module: drains output buffers before streaming binary downloads.
- tests: extended harness with a walk_table before_cap=0 case (9 suites green).
Co-Authored-By: Claude <[email protected]>
The fork's Pro merge left Backup/Restore/Migrate/Sync as placeholders (4 naive
files): no restore path, migrate-site was a canned-success stub, sync-to-live
touched only wp_options, and the connector exposed only a /status route. This
makes the story real at a bounded scope and fixes the wiring/doc drift found in
a feature-parity review against upstream Pro.
- search-replace engine: byte-accurate serialized fixer (length-delta, never
unserializes), JSON escaped+unescaped URL pair replace (_elementor_data), and
a site-wide table walker; legacy EMCP_Tools_Search_Replace now delegates.
- backups: streamed DB export (bounded chunks, explicit-column INSERTs), richer
.emcp manifest (format version, site_url, php/wp/db, DB sha256 + row stats),
opt-in include_files bundling of uploads/plugins/themes.
- restore: hardened statement-aware importer (skips transaction-control
directives, ';'-in-string safe); new Restore_Engine (hash-verified -> import
-> URL rewrite -> traversal-guarded file placement); admin Restore/Download/
Delete + restore-history panel (nonce + manage_options, admin-only).
- migrate-site: real push to a standalone HMAC connector (2 MB signed packets ->
assemble -> verify -> self-contained restore -> pollable job). confirm:true
required; direction "pull" returns unsupported.
- sync-to-live: real site-wide serialized-safe URL search-replace across
options/posts/postmeta/comments/term+usermeta + prefixed tables, ledger-
recorded for History rollback.
- remove 3 never-registered ghost tools (list-syncable-changes,
sync-content-item, discard-sync-change) from catalog/slugs/seeding.
- AI Chat: canonical module id 'ai-chat' (admin toggle now works, legacy
ai_chat read-back), /emcp/v1/chat route gated on module state, default on.
- docs: correct README/readme.txt over-claims (WooCommerce op list, addon slugs,
atomic wording, SEO/forms counts), tidy pro-manifest.txt; bump 3.16.0.
Tests: extended tests/test-migrate-packager.php (8 suites, all passing) covering
byte-accurate SR, export/import round trip, directive-skip, quote-safe split,
and packager create/read/list/delete.
Co-Authored-By: Claude <[email protected]>
LobeHub (and other) agents kept failing to call emcp-tools/comic-write
because its "scheme" was invisible: the static schema exposed only an
unconstrained operation string plus an opaque free-form arguments object,
and comic-write uniquely marked operation as REQUIRED, so the documented
"call with no operation to list the catalog" discovery call was blocked at
the schema level. The hand-maintained runtime catalog had also drifted from
the executors, omitting status (which defaults to publish), date, author,
taxonomies, content_warning, and comic_html_above/below — exactly the facts
agents were guessing at.
- comic-write no longer requires operation, so the empty-op discovery call
is legal; both tools' operation property now carries an enum of the real
operation names.
- Add op_schema() to the read and write operations classes as the single
source of truth (typed JSON Schema + a realistic example per op). The
discovery catalog is generated from it — each entry keeps the flat
arguments hints and gains typed schema + example — so the docs can never
drift from the executors again.
- Tool descriptions now state the discovery flow and the create-comic
contract (status defaults to "publish", date backdating, image flow,
author).
- Clarify in both emcp-comic-easel SKILL.md files that imported strips must
always pass "status": "draft" explicitly, since the tool's own default is
publish.
- Tests: new schema-exposure suite (no required operation, enum == spec
keys, catalog schema+example, status default/fields exposed, and a parity
guard pinning every executor field to the spec). All 8 suites pass.
Co-Authored-By: Claude <[email protected]>
The Sandbox admin page fatals on shad-base.com with
"Call to undefined method EMCP_Tools_Block_Store::user_has_access()"
(views/sandbox/overview.php:41). The store loads eagerly so class_exists()
passes, but the class shipped by the unlocked port (9176dd7) was never given
the API its own views call — pagination commit b7cfe45 updated blocks.php and
the widget/snippet stores but not the block store.
Add the missing consumer-facing methods, mirroring the widget store and the
EMCP_Tools_Sandbox_Store base:
- user_has_access() (overview.php:41, blocks.php:21, admin AJAX guards)
- summary() admin-table row: block_id/block_name, publish->active
- list_blocks() overview card counts
- list_blocks_page() blocks.php table via EMCP_Tools_Sandbox_List_Query
- get_asset() block.json/render.php code viewer (whitelisted)
- create_block() fixes latent fatal in apply_bundle() (bundle import)
- uninstall_cleanup() uninstaller.php:77
Harden create()/update()/compile() with a class_exists guard on the deferred
EMCP_Tools_Block_Generator (matches class-widget-store.php:372).
New tests/test-sandbox-blocks.php: 7 suites — method presence, user_has_access
gate, summary mapping, overview.php render smoke (the previously-fatal file),
get_asset whitelist round-trip, apply_bundle->create_block draft import, and
list_blocks_page shape. All pass; migrate/comic/skills suites still green.
Co-Authored-By: Claude <[email protected]>
Block_Tree::insert() no-ops when an index path does not resolve and returns the
tree unchanged, so every block pack saved the unchanged post and still reported
success — the block was lost and the agent believed it landed. Found live while
building a 29-block Kadence page: three nested infoboxes vanished with 'ok' for
each. New Block_Tree::position_is_valid() (append/prepend always valid;
inside/before/after require a path that resolves via at()), enforced in the
Kadence (add-block + insert-pattern), Spectra, GenerateBlocks and Blocksy add
paths with an invalid_position WP_Error. Core Gutenberg already guarded this.
Note for callers: position.path indexes the SEPARATOR-STRIPPED tree (what
get-post-blocks returns), not raw parse_blocks output, which counts whitespace
entries — that mismatch is exactly what produced the lost blocks.
Tests: BlockTreePositionTest (5) incl. the silent-noop behaviour the guard exists
to prevent. Suites: pro 2070, public 155.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On a WordPress site under a subpath (e.g. example.com/gpt-build/), the RFC 9728
protected-resource metadata request arrives as /gpt-build/.well-known/oauth-protected-resource,
but EMCP_Tools_OAuth_Metadata::request_path() matched the raw URI against the
site-root-relative /.well-known/... paths, so it never matched and the request
404'd — even though the WWW-Authenticate header advertises that exact subfolder URL.
OAuth discovery (ChatGPT, Claude, any RFC 9728 client) dead-ended and no tools
appeared. Strip the home-URL path prefix before matching (no-op on root installs).
Tests: OAuthMetadataSubdirTest (public suite 150).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
#111 (part 2): on installs with Elementor's Flexbox Container experiment OFF, the
'container' element type is unregistered, so add-container/build-page stored pages
that render empty (Elements_Manager skips the uninstantiable element). Add
EMCP_Tools_Atomic_Props::is_container_supported() (authoritative element-type check),
refuse add-container/build-page up front with an actionable container_unsupported
error, and surface supports_container + a warning + recommended_mode='unsupported'
from detect-elementor-version.
#112: Document::save() THROWS Elementor's atomic settings/styles validation on an
editor-authored {$$type:'dynamic'} paragraph whose tag the CLI/REST atomic
dynamic-tags registry can't resolve, and because save() validates the whole tree it
blocked EVERY edit to the document. Route a validation-rejection throw into the
existing raw meta-write fallback (the tree is already coerce_tree'd, so what remains
is data the editor itself persists) via EMCP_Tools_Data::is_atomic_validation_rejection();
genuine fatals still hard-fail.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
MCP-created pages rendered empty on Elementor 4.2.x: create-page writes empty
[] data, an intermediate render caches an empty _elementor_element_cache, and
the plugin's raw update_post_meta() write path never invalidated it (Elementor
only clears it in Document::save(), which the CLI/proxy fallback bypasses). On a
persistent object cache (WP Engine) the stale empty entry survives every content
write, so the page serves the cached empty render forever.
Add EMCP_Tools_Data::init() — an added/updated_post_meta hook that clears
_elementor_element_cache whenever _elementor_data changes, from ANY write path —
plus an explicit clear in the save fallback. Also fixes delayed visibility of
edits to existing pages. Root-caused + fix live-verified on Elementor 4.2.2.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
resource()/base_url() (token/register/revoke) now route through the shared
rest_endpoint() helper too, so an admin override (or a host where rest_url
differs) yields a fully consistent OAuth discovery document — issuer, authorize,
resource, and token all on one reachable host. HTTP-verified against the live
well-known endpoints. +2 tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On hosts that pin the Site Address to a not-yet-live domain (e.g. Hostinger
staging), home_url() is unreachable while rest_url() answers on the real host.
The .mcpb bundle WP_URL and the OAuth issuer/authorization_endpoint used
home_url(), so they shipped/advertised an unreachable URL — the bundle couldn't
connect, OAuth discovery 404'd, and mcp-remote dropped pursuing the dead issuer.
Add EMCP_Tools_Site_Context::public_base_url() (rest_url-derived default, admin
override option, emcp_tools_public_base_url filter) + mcp_endpoint(), and route
the bundle, OAuth issuer + authorize endpoint, the Connection-tab endpoint, and
the JS config vars through it. Add an editable Server URL field on the
Connection tab. Live-verified across a simulated rest/home split; +7 tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New EMCP_Tools_Redirect_Module (free, on by default) owns the store+handler boot
and gates — via static is_enabled() — the ability group in the registrar, the
Redirects admin tab, and the delete/rename suggest-only wiring, so the whole
feature can be switched off from the Modules tab. Same pattern as EMCP Themer.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Bug report Issue 1 (intermittent 'connector isn't responding' on shared
LiteSpeed): send Cache-Control: no-store + X-LiteSpeed-Cache-Control: no-cache
+ X-Accel-Buffering: no on the MCP route so LiteSpeed/QUIC never caches or
buffers MCP responses. Adds a readme FAQ (cache-exclusion + PHP limits + MCP
Log) and an opt-in EMCP_TOOLS_PROFILE_REGISTRATION timing seam. The registrar
was confirmed already lazy/cheap, so this targets the transport layer.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Bug report Issue 5: the client only sees generic error strings with no
server-side diagnostics. Records each MCP request (tool, status, duration,
request id) to a capped option via rest_pre/post_dispatch on the MCP route,
surfaced on a new 'MCP Log' admin tab. The underlying error is stored only
when WP_DEBUG is on. 3 unit tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Bug report Issue 2: a connector left pointed at an old domain still loaded
tools/list (GET follows the 301) but every tool execution failed opaquely.
A rest_pre_dispatch filter scoped to /mcp/emcp-tools-server now returns a
421 with the correct endpoint URL when the Host header no longer matches
home_url(), so the connector breaks visibly. Fail-open on missing Host.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sweeps shipped copy clean of em dashes: readme.txt, README.md,
CHANGELOG.md, the bundled sample prompts, and user-facing PHP strings
(tool descriptions, admin labels). PHP was swept on non-comment lines
only, so developer docblocks and inline comments are untouched.
Internal dev docs (CLAUDE.md, docs/) were deliberately left alone.
Replacements are contextual: colons for label/definition patterns,
commas for asides, en dashes for numeric ranges.
The .mcpb bundle display_name now reads "EMCP Tools - <host>" with a
plain hyphen; its test is updated to match. Fixing CHANGELOG.md also
clears the em dashes from the website's generated changelog on the
next site build, since that file is its source.
Verified: php -l clean on every touched file, pro suite 1559 pass,
public suite 102 pass. Bumps the pro submodule for the Pro-side sweep.
@Mrshahidali420's fix for the Claude Desktop bundle collision (each site's
.mcpb now installs side by side). Resolved CHANGELOG (folded into 3.3.0
Fixed); source + test applied clean. Public suite 70 green.
Co-Authored-By: Mrshahidali420 <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude Desktop keys installed extensions by the manifest `name`, but every
generated .mcpb bundle used a fixed `emcp-tools`, so installing a second
site's bundle silently replaced the first. Derive a per-site slug from the
host (emcp-tools-<host-slug>), with a self-contained PHPUnit test.
Fixes#86.
Adds WordPress nav-menu management as two dispatcher tools (menu-read /
menu-write, 13 operations) mirroring the ACF pattern, an [emcp_menu] shortcode
for live menus in custom HTML headers, and a self-contained PHPUnit test.
Wired always-on in the registrar + bootstrap; CHANGELOG under Unreleased.
Lands the ACF domain from #76 (by @DeibyRo), reworked to a consolidated
2-tool surface and a new admin home.
Surface: instead of 15 individual MCP abilities, the domain registers TWO
dispatcher tools — emcp-tools/acf-read and emcp-tools/acf-write — that expose
15 operations (8 read, 7 write). Each takes { operation, arguments }; calling
with no operation returns a discovery catalog listing that side's operations.
dispatch() gates each call by ACF-6.1+ availability (CPT/tax ops) then the
operation's own capability check — no privilege escalation.
Gating: read-vs-write is at the TOOL level — acf-read and acf-write are the
two toggles (acf-read on, acf-write off by default), enabled/disabled by the
standard filter_disabled_tools mechanism like any other tool. No per-operation
toggles. Seeded by the v14 defaults step (DEFAULTS_VERSION 13 -> 14), which
adds emcp-tools/acf-write to the disabled set and strips any pre-release
per-operation ACF slugs.
Admin: new "Plugins" sub-tab on the Tools screen; the ACF section shows the
two dispatcher toggles, each listing the operations it covers.
Everything else from #76 is preserved: CPT/taxonomy authoring as data via
acf_import_* (no executable PHP), full Pro field support
(repeater/flexible/gallery/group/clone), name->key write resolution, immutable
slugs, no delete operations.
Tests: tests/ harness reworked to the dispatcher model (49 tests — routing,
catalog, permission delegation + the 15 executors). pro/tests
ToolCatalogPlatformTest updated for the Plugins tab. Full suite 928 green;
live-verified end-to-end against ACF 6.8.5, including over the real MCP stdio
transport (acf-read exposed, acf-write hidden while off). Versioned 3.2.1.
Co-Authored-By: DeibyRo <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The root phpunit.xml points at the private pro/tests submodule that
outside contributors cannot fetch, so tests/ is a self-contained
harness (fixture-driven WordPress + ACF stubs) runnable with plain
PHPUnit: vendor/bin/phpunit -c tests/phpunit.xml. 29 tests covering
registration, permissions, target validation, name-to-key resolution,
flexible layout validation, the Pro gate, value normalization, and the
field-group authoring guardrails. Un-ignores /tests/ so the public
suite is committable.
Co-Authored-By: Claude Fable 5 <[email protected]>
Move all Pro-tier code + tests/ + tools/ out of this public repo into the
private msrbuilds/emcp-pro repo, mounted as a git submodule at pro/. No Pro
source ships in any free artifact.
- EMCP_Tools_Pro_Loader: dual-root conditional loader (plugin root in the
premium build where pro/* is overlaid; pro/ in a dev checkout). Free plugin
runs with pro/ absent — every Pro require/instantiation guarded.
- Bootstrap, widget-store, uninstaller guarded; admin AI-Chat/Skills tabs fall
back to an upsell CTA when pro/ is absent.
- pro-manifest.txt is the authoritative Pro-path list; verify-release-zip.sh
hard-fails if any Pro path leaks into the free zip.
- Free build (pro/ absent) = 115 abilities, no fatal; Pro present = 134
(identical to pre-split). PHPUnit 604 pass / 1 pre-existing skip.
Squashed from feature/ai-chat to avoid publishing in-place Pro history.
Docs: CLAUDE.md (Repository topology, Release, Freemius policy).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude Desktop does not cd into the extracted bundle directory before
running `node`, so the relative path `server/index.js` resolved against
the wrong CWD → "Cannot find module" → instant "Server disconnected".
Prefix the entry-point with the MCPB ${__dirname} substitution variable
(the bundle's install path), matching the canonical DXT/MCPB pattern.
Verified: generated server/index.js passes `node --check` and boots
cleanly (prints startup logs, exits 0) with credentials embedded and the
correct /mcp/emcp-tools-server route.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Convert bin/mcp-proxy.mjs ESM→CJS on the fly and embed it directly in
server/index.js with credentials prepended. Single file, no subprocess,
no PATH resolution needed — just the Node.js binary Claude Desktop already
has. Fixes build-in-Node.js mode where server/index.js exited immediately
and fixes the npm-package old-route issue by using the local proxy source.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
Replace npx-based launch with bundling bin/mcp-proxy.mjs directly inside
the .mcpb as server/proxy.mjs. server/index.js runs it via process.execPath
(the same Node binary Claude Desktop already uses), so the extension works
with Claude Desktop's built-in Node.js without needing npx or system PATH.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
Adds a single source-of-truth static method for the optional-Elementor
gate, a did_action() test stub, the Bootstrap autoload entry, the
Bootstrap test suite registration in phpunit.xml, and 2 passing tests.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>