Skip to content

Commit 1ea4bec

Browse files
ifahimrezaclaude
andauthored
fix(build): ship the MCP compat shim, and stop its absence being silent (#112)
class-saddle-mcp-compat.php was excluded from the shared build list, and the selfhosted channel only ever re-added the updater — so the ChatGPT fix from #80/#81 has been in no zip on any channel since it was written. It was reachable the whole time: adapter_available() tests for \WP\MCP\Core\McpAdapter, not for our bundled copy, so a site running the official MCP Adapter plugin takes the adapter path on any channel, finds class_exists( 'Saddle_MCP_Compat' ) false, and gets an app that connects and then reports no callable actions. The shim now ships on both channels. It is our own code with no library behind it and applies_to() already no-ops without SessionManager, so a .org build pays nothing for it; unlike the updater, its absence is not a guarantee we make to anyone. The selfhosted branch additionally re-adds includes/lib/** and the bundled-adapter loader, which the exclusion comment has asked for since the channel landed. And the guard no longer fails quietly. A false class_exists() branch with no outward signal is why this survived a release, so it now marks the health record degraded. Noted rather than recorded at the point of discovery: the health option is rewritten wholesale on mcp_adapter_init, later in the same request, so a direct write there would be erased on exactly the sites that have the adapter. Refs #111 Claude-Session: https://claude.ai/code/session_01AnLWpPkeyppYfTuKEPhTM2 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 620154a commit 1ea4bec

3 files changed

Lines changed: 81 additions & 11 deletions

File tree

Gruntfile.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,30 @@ module.exports = function ( grunt ) {
128128
// the self-hosted channel lands, re-include it there the
129129
// same way that branch re-includes the updater.
130130
'!includes/lib/**',
131-
// The two files that exist only to serve it: the
132-
// loader (which declares the library's own reserved
133-
// WP_MCP_* constants) and the shim for its session
134-
// strictness. Both are guarded with file_exists()/
135-
// class_exists() and degrade to no-ops.
131+
// The loader that declares the library's own reserved
132+
// WP_MCP_* constants. Useless without the library, and
133+
// guarded with class_exists() at the call site.
136134
'!includes/class-saddle-bundled-adapter.php',
137-
'!includes/class-saddle-mcp-compat.php',
135+
// class-saddle-mcp-compat.php is deliberately NOT
136+
// excluded, on either channel. It used to be, and that
137+
// is how the ChatGPT fix in #80/#81 shipped to nobody
138+
// for a month (#111).
139+
//
140+
// The trap: adapter_available() tests for
141+
// \WP\MCP\Core\McpAdapter, not for OUR bundled copy. A
142+
// site that installs the official MCP Adapter plugin
143+
// takes the adapter path on ANY channel, including
144+
// .org — and then class_exists( 'Saddle_MCP_Compat' )
145+
// is false, the shim never registers, and the owner
146+
// gets an app that connects and reports no callable
147+
// actions. Excluding it never made a build safer; it
148+
// only made that failure reachable.
149+
//
150+
// It is ~300 lines of our own code with no library
151+
// behind it, and applies_to() already no-ops when
152+
// SessionManager is absent, so it costs a .org build
153+
// nothing. Unlike the updater, its absence is not a
154+
// guarantee we are making to anyone.
138155
// WP.org listing assets — go to SVN assets/, never in the zip.
139156
'!.wordpress.org/**',
140157
// Lint config — dev-only.
@@ -257,11 +274,20 @@ module.exports = function ( grunt ) {
257274
}
258275

259276
if ( 'selfhosted' === channel ) {
260-
// Put the updater back by appending an un-negated pattern after
261-
// the exclusion — grunt-contrib-copy applies src patterns in
262-
// order, so the later include wins.
277+
// Put the self-hosted-only files back by appending un-negated
278+
// patterns after the exclusions — grunt-contrib-copy applies src
279+
// patterns in order, so the later include wins.
280+
//
281+
// The adapter belongs here and did not arrive with the updater:
282+
// the exclusion list has said "re-include it there the same way
283+
// that branch re-includes the updater" since the channel landed,
284+
// and this branch only ever pushed the updater (#111). Every
285+
// build shipped so far therefore runs the JSON-RPC transport
286+
// unless the site installs the official adapter plugin itself.
263287
const files = grunt.config.get( 'copy.dist.files' );
264288
files[ 0 ].src.push( 'includes/class-saddle-updater.php' );
289+
files[ 0 ].src.push( 'includes/lib/**' );
290+
files[ 0 ].src.push( 'includes/class-saddle-bundled-adapter.php' );
265291
grunt.config.set( 'copy.dist.files', files );
266292
}
267293

includes/class-saddle-mcp-diagnostics.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ class Saddle_MCP_Diagnostics {
4343
*/
4444
const TRACE_OPTION = 'saddle_mcp_trace';
4545

46+
/**
47+
* Whether the adapter path is running without Saddle_MCP_Compat.
48+
*
49+
* Set during transport setup and merged into the health record by
50+
* {@see self::record_health()}. It is request state, not stored state: the
51+
* next request re-derives it from whether the class loaded.
52+
*
53+
* @var bool
54+
*/
55+
private static $compat_missing = false;
56+
57+
/**
58+
* Note that the adapter is serving requests without its compatibility shim.
59+
*
60+
* This is a build fault, not a configuration one — the shim was excluded
61+
* from every zip for a month while the adapter path stayed reachable through
62+
* the official MCP Adapter plugin, and the only outward symptom was a client
63+
* connecting and then finding no callable tools (#111).
64+
*/
65+
public static function note_compat_missing() {
66+
self::$compat_missing = true;
67+
}
68+
4669
/**
4770
* Option holding the unix timestamp recording stops at.
4871
*/
@@ -153,6 +176,14 @@ public static function record_health( array $facts ) {
153176
$facts['recorded_at'] = time();
154177
$facts['init_fired'] = did_action( 'init' ) > 0;
155178

179+
// Merged into every write rather than recorded on its own, because this
180+
// option is replaced wholesale and the adapter writes it later in the
181+
// request than the point where the shim's absence is discovered.
182+
if ( self::$compat_missing ) {
183+
$facts['compat_missing'] = true;
184+
$facts['degraded'] = true;
185+
}
186+
156187
$previous = get_option( self::HEALTH_OPTION );
157188
if ( is_array( $previous ) ) {
158189
// Ignore the timestamp when deciding whether anything changed, or a

saddle.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,23 @@ public static function setup_mcp_transport() {
290290
// it. Reached only when that plugin is present.
291291
add_action( 'mcp_adapter_init', array( 'Saddle_MCP', 'register_adapter_server' ) );
292292

293-
// Shims the adapter's session and protocol-header strictness; it has
294-
// no purpose without the adapter, so it ships with it.
293+
// Shims the adapter's session and protocol-header strictness.
294+
//
295+
// The guard stays — it is the house rule — but it no longer fails
296+
// quietly. This exact class_exists() returned false in every build
297+
// for a month because the zip excluded the file, and the only
298+
// symptom reachable from outside was ChatGPT reporting that the
299+
// site has no callable actions (#111). A guard whose false branch
300+
// is invisible is how that lasted, so the false branch now says so
301+
// where the owner and a support conversation can both see it.
295302
if ( class_exists( 'Saddle_MCP_Compat' ) ) {
296303
Saddle_MCP_Compat::register();
304+
} else {
305+
// Noted, not recorded: the health record is rewritten wholesale
306+
// on mcp_adapter_init, a few hooks later, so anything written
307+
// here would be erased on precisely the sites where the adapter
308+
// is present — the only sites where this matters.
309+
Saddle_MCP_Diagnostics::note_compat_missing();
297310
}
298311
} else {
299312
add_action( 'rest_api_init', array( 'Saddle_MCP', 'register_routes' ) );

0 commit comments

Comments
 (0)