Skip to content

Commit 65043cf

Browse files
authored
fix: clear the WordPress.org rejection (#82) (#87)
* fix(build): stop shipping the vendored MCP adapter to WordPress.org WordPress.org rejected the submission over generic and reserved prefixes. Our own 126 elements were already clean — every one of the flagged wp_mcp and mcp_adapter names came from includes/lib/wp-mcp/, the vendored WordPress MCP Adapter, which is 347 of 464 shipped files and about half the zip. It also carried the only error_log and fwrite calls in the tree and stood up a second, undeclared endpoint at /wp-json/mcp/mcp-adapter-default-server. So the .org build no longer ships it. Nothing is lost: Saddle_MCP's own JSON-RPC transport already served the same /saddle/v1/mcp URL with the same abilities behind the same tier and approval gate, and a site that installs the official MCP Adapter plugin gets the adapter path back automatically. Absence is the switch, the way it already is for the updater. The bundle loader moved into its own excluded file rather than staying a guarded branch: it declares the library's WP_MCP_DIR/WP_MCP_VERSION, and a scanner reading source cannot tell those two defines are unreachable — shipping the file only where the library ships means the .org build contains no reserved-prefix declaration at all. The session shim moved with it, having no purpose without the adapter. Hardening the built-in transport first, since it is now the only one .org installs get: - Tool names lose the namespace slash. saddle/list-posts is illegal in MCP and rejected outright by OpenAI's clients, which drop the whole list over one bad name — the exact "no callable actions" symptom, waiting to happen again. call_tool() accepts both forms so a cached name still resolves. - Tools carry title and behaviour hints. Every ability already declared readonly/destructive/idempotent via saddle_ability_meta(); the transport was throwing it away. An agent can now weigh a call before making it. - A refusal is a tool result with isError, not a JSON-RPC error. MCP reserves protocol errors for protocol faults, and several clients never show the model a JSON-RPC error — which meant our carefully written "the site is at the read access level, do not retry" text was reaching nobody. Verified on the built .org zip, not the dev tree: 114 files, zero includes/lib, zero wp_mcp, zero fwrite, zero error_log calls. Then run in WP Playground over real HTTP — initialize returns tools-only capabilities, a stateless tools/list returns 61 tools with zero illegal names, a read tool executes, and a write tool at the default read tier is refused as isError with the reason intact. Refs #82 * fix(context): keep installed inventory at the admin tier, where it belongs WordPress.org flagged get-instructions for handing out plugin inventory at the read tier. The literal claim was wrong — the ability has a permission_callback — but the point underneath was right, and worse than they could see. site.php already states the policy: "Reads that expose configuration (option values, installed inventory) sit at admin too, not read — the inventory itself is sensitive." saddle/list-plugins and saddle/list-themes are gated on exactly that. Meanwhile the system context handed every read-tier session a prose copy of the same active-plugin list, with versions, plus the theme by name. So the plugin list and the theme name now appear only at the admin tier. What survives at every tier is the part an agent actually needs to avoid mangling a page: whether the theme is block-based, and the builder/multilingual detection, which is behavioural guidance rather than an inventory. The worse half was the handshake. initialize runs before any ability's permission_callback — its only check is that someone is logged in — and it served MORE than get-instructions would (the entire context plus the owner's own written instructions) with FEWER checks. A paused site still answered it, and a Subscriber-level key or a saddle:read bearer got the same payload as an admin. It now honours the pause switch, and its context is tier-aware, so the handshake can no longer exceed what the equivalent ability would return. Tests drive the real path rather than restating it: get_plugins() has no filter, so the plugin case primes the cache group it reads from, and asserts the same plugin is named at admin tier and absent at read. * docs: say what transport a site has, and stop describing a bundle we dropped readme.txt claimed Saddle "bundles the WordPress MCP Adapter library... license included in includes/lib/wp-mcp/". That stopped being true one commit ago, and a false statement in the readme is exactly the kind of thing a reviewer checks. Rewritten as what is now the case, and as the answer to the question the change raises: Saddle speaks MCP itself, there is nothing else to install, and if the separate MCP Adapter plugin is active Saddle uses it — same address, same tools, same access levels either way. Same wording in the Connections HelpTip, which called the built-in path a fallback, and in docs/connections.md. Also closes the troubleshooting gap that started this whole thread: the docs covered "connected but can't do something" and had nothing for "connected but shows no tools at all" — the exact sentence the customer sent twice. It now points at Client traffic and explains how to read it, including the case only that panel can show, where no row appears because the request never reached WordPress at all. WPORG-SUBMISSION.md: §3 no longer describes a bundled library, and the argument in §11 that the flagged notices were "unreachable in the shipped zip, not patched deliberately" is struck — they are simply not shipped. Records that the two remaining mcp_adapter_* strings are add_action/add_filter calls against that plugin's own hooks, which cannot carry our prefix. POT regenerated (1087 to 1090 msgids), bundle rebuilt.
1 parent 32275f5 commit 65043cf

16 files changed

Lines changed: 710 additions & 143 deletions

Gruntfile.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,37 @@ module.exports = function ( grunt ) {
9393
'!admin/DESIGN-ALIGNMENT.md',
9494
// User docs live on the website, not in the zip.
9595
'!docs/**',
96+
// The vendored WordPress MCP Adapter is NOT shipped.
97+
//
98+
// It is ~347 of 464 files and half the zip, and every
99+
// symbol in it is namespaced WP\MCP / hooked
100+
// mcp_adapter_* — `wp_` is reserved for core, which is
101+
// what WordPress.org rejected the submission over. It
102+
// also carries the only error_log/fwrite calls in the
103+
// tree and stands up a second, undeclared endpoint at
104+
// /wp-json/mcp/mcp-adapter-default-server.
105+
//
106+
// Nothing is lost by its absence: Saddle_MCP's own
107+
// JSON-RPC transport serves the same /saddle/v1/mcp URL
108+
// with the same abilities and the same tier + approval
109+
// gate, and Saddle::load_bundled_mcp_adapter() already
110+
// early-returns when the directory isn't readable —
111+
// before it defines WP_MCP_DIR/WP_MCP_VERSION, so those
112+
// two reserved constants disappear with it. A site that
113+
// installs the official MCP Adapter plugin gets the
114+
// adapter path back automatically.
115+
//
116+
// Keep this exclusion in the WordPress.org build. When
117+
// the self-hosted channel lands, re-include it there the
118+
// same way that branch re-includes the updater.
119+
'!includes/lib/**',
120+
// The two files that exist only to serve it: the
121+
// loader (which declares the library's own reserved
122+
// WP_MCP_* constants) and the shim for its session
123+
// strictness. Both are guarded with file_exists()/
124+
// class_exists() and degrade to no-ops.
125+
'!includes/class-saddle-bundled-adapter.php',
126+
'!includes/class-saddle-mcp-compat.php',
96127
// WP.org listing assets — go to SVN assets/, never in the zip.
97128
'!.wordpress.org/**',
98129
// Lint config — dev-only.

WPORG-SUBMISSION.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,31 @@ Four outbound `wp_remote_get` target classes, all disclosed in readme.txt
8484

8585
No telemetry, no phoning home, no update checker, no external CDN assets.
8686

87-
### 3. Bundled library
88-
89-
`includes/lib/wp-mcp` — the official WordPress **MCP Adapter** (`WP\MCP`),
90-
GPLv2, license included. If the standalone MCP Adapter plugin is active,
91-
Saddle defers to that copy (`saddle_load_bundled_mcp_adapter` filter to opt
92-
out of the bundled one).
87+
### 3. Bundled libraries — none
88+
89+
**The zip contains no third-party library.** Every function, class, constant,
90+
option, hook, CPT and asset handle it declares is prefixed `saddle` / `Saddle_`
91+
/ `SADDLE_`.
92+
93+
This changed in response to the first review. Saddle previously bundled the
94+
official WordPress **MCP Adapter** (`WP\MCP`) under `includes/lib/wp-mcp/` — 347
95+
of 464 files, roughly half the zip, and the source of every `wp_mcp` and
96+
`mcp_adapter` name in the prefix report. It is no longer shipped
97+
(`Gruntfile.js`), together with the two files that existed only to serve it:
98+
`includes/class-saddle-bundled-adapter.php` (which declared that library's own
99+
`WP_MCP_*` constants) and `includes/class-saddle-mcp-compat.php`.
100+
101+
Nothing was lost. `Saddle_MCP` has always carried its own JSON-RPC transport on
102+
the same `/wp-json/saddle/v1/mcp` route, exposing the same abilities behind the
103+
same access tiers and approval gate; it is now the only transport in this build.
104+
If a site separately installs the MCP Adapter plugin, Saddle detects the class
105+
and uses it — that path is optional and guarded with `class_exists()`.
106+
107+
Two `mcp_adapter_*` strings remain in the source, at `saddle.php` and
108+
`includes/class-saddle-mcp.php`. Both are `add_action`/`add_filter` calls
109+
against **that plugin's own hooks** — the documented way to integrate with it.
110+
They are names it owns, they cannot carry a Saddle prefix, and they only fire
111+
when that plugin is present.
93112

94113
### 4. Third-party brand names/logos
95114

@@ -228,13 +247,13 @@ Two things a scanner plausibly matched on:
228247
closes the buffer. Saddle's own notices register at priority 0 so they print
229248
above it rather than exempting themselves from view. This prevents other
230249
plugins hijacking Saddle's screen; it is not Saddle hijacking anything.
231-
2. **Two notices in the vendored `WP\MCP` library** (`includes/lib/wp-mcp/
232-
includes/Autoloader.php:67`, `Plugin.php:73`) are non-dismissible and not
233-
capability-checked. Both are unreachable in the shipped zip: the composer
234-
autoloader **is** bundled (262 `wp-mcp/vendor/` entries incl.
235-
`vendor/autoload.php`), and `Saddle::setup_mcp_transport()` only loads the
236-
library when `wp_register_ability` already exists (`saddle.php:132-153`).
237-
Deliberately **not** patched — don't fork third-party code for a dead branch.
250+
2. ~~Two notices in the vendored `WP\MCP` library.~~ **No longer applicable**
251+
the library is not shipped (see §3). The same removal takes with it the
252+
`error_log()` in its `Autoloader.php`, the `fwrite(STDOUT)` calls in its
253+
stdio CLI bridge, and the second REST endpoint it registered at
254+
`/wp-json/mcp/mcp-adapter-default-server`. **The shipped zip contains no
255+
`error_log()` call, no `fwrite()`, and no `var_dump()`** — verified by
256+
grepping the built artifact, not the dev tree.
238257

239258
### 12. Public OAuth endpoints (new in this submission)
240259

admin/build/index.asset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => 'eccc9798aca5ca970c4e');
1+
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '93d3d4761821b065a965');

0 commit comments

Comments
 (0)