PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.3
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / mcp.php
ai-engine / labs Last commit date
mcp.conf 1 year ago mcp.js 1 year ago mcp.md 1 year ago mcp.php 1 year ago mcp_core.php 1 year ago mcp_rest.php 1 year ago realtime.php 1 year ago
mcp.php
556 lines
1 <?php
2
3 class Meow_MWAI_Labs_MCP {
4 private $core = null;
5 private $namespace = 'mcp/v1';
6 private $server_version = '0.0.1';
7 private $protocol_version = '2025-03-26';
8 private $queue_key = 'mwai_mcp_msg';
9 private $session_id = null;
10 private $logging = false;
11 private $last_action_time = 0;
12 private $bearer_token = null;
13
14 #region Initialize
15 public function __construct( $core ) {
16 $this->core = $core;
17 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
18 }
19
20 public function rest_api_init() {
21 $this->bearer_token = $this->core->get_option( 'mcp_bearer_token' );
22 if ( !empty( $this->bearer_token ) ) {
23 add_filter( 'mwai_allow_mcp', [ $this, 'auth_via_bearer_token' ], 10, 2 );
24 }
25 register_rest_route( $this->namespace, '/sse', [
26 'methods' => 'GET',
27 'callback' => [ $this, 'handle_sse' ],
28 'permission_callback' => function( $request ) {
29 return $this->can_access_mcp( $request );
30 },
31 ] );
32
33 register_rest_route( $this->namespace, '/sse', [
34 'methods' => 'POST',
35 'callback' => [ $this, 'handle_sse' ],
36 'permission_callback' => function( $request ) {
37 return $this->can_access_mcp( $request );
38 },
39 ] );
40
41 register_rest_route( $this->namespace, '/messages', [
42 'methods' => 'POST',
43 'callback' => [ $this, 'handle_message' ],
44 'permission_callback' => function( $request ) {
45 return $this->can_access_mcp( $request );
46 },
47 ] );
48 }
49 #endregion
50
51 #region Auth (Bearer token)
52 function can_access_mcp( $request ) {
53 $logged_in = is_user_logged_in();
54 return apply_filters( 'mwai_allow_mcp', $logged_in, $request );
55 }
56
57 public function auth_via_bearer_token( $allow, $request ) {
58 if ( empty( $this->bearer_token ) ) {
59 return false;
60 }
61 $hdr = $request->get_header( 'authorization' );
62 if ( $hdr && preg_match( '/Bearer\s+(.+)/i', $hdr, $m ) &&
63 hash_equals( $this->bearer_token, trim( $m[1] ) ) ) {
64 if ( $admin = $this->core->get_admin_user() ) {
65 wp_set_current_user( $admin->ID, $admin->user_login );
66 }
67 return true;
68 }
69 // ?token=xyz fallback (optional)
70 $q = sanitize_text_field( $request->get_param( 'token' ) );
71 if ( $q && hash_equals( $this->bearer_token, $q ) ) return true;
72 return $allow;
73 }
74 #endregion
75
76 #region Helpers (log / JSON-RPC utils)
77 private function log( $msg ) {
78 if ( $this->logging ) {
79 Meow_MWAI_Logging::log( "[MCP] ({$this->session_id}): {$msg}" );
80 }
81 }
82
83 /** Wrap a JSON-RPC error object */
84 private function rpc_error( $id, int $code, string $msg, $extra = null ): array {
85 $err = [ 'code' => $code, 'message' => $msg ];
86 if ( $extra !== null ) $err['data'] = $extra;
87 return [ 'jsonrpc' => '2.0', 'id' => $id, 'error' => $err ];
88 }
89
90 /** Queue an error for SSE delivery */
91 private function queue_error( $sess, $id, int $code, string $msg, $extra = null ): void {
92 $this->store_message( $sess, $this->rpc_error( $id, $code, $msg, $extra ) );
93 }
94
95 /** Format tool result for MCP protocol */
96 private function format_tool_result( $result ) : array {
97 // If result is a string, wrap it in the MCP content format
98 if ( is_string( $result ) ) {
99 return [
100 'content' => [
101 [
102 'type' => 'text',
103 'text' => $result,
104 ],
105 ],
106 ];
107 }
108
109 // If result has 'content' key, assume it's already properly formatted
110 if ( is_array( $result ) && isset( $result['content'] ) ) {
111 return $result;
112 }
113
114 // If result is an array without 'content' key, wrap it as JSON
115 if ( is_array( $result ) ) {
116 return [
117 'content' => [
118 [
119 'type' => 'text',
120 'text' => wp_json_encode( $result, JSON_PRETTY_PRINT ),
121 ],
122 ],
123 'data' => $result,
124 ];
125 }
126
127 // For any other type, convert to string and wrap
128 return [
129 'content' => [
130 [
131 'type' => 'text',
132 'text' => (string) $result,
133 ],
134 ],
135 ];
136 }
137 #endregion
138
139 #region Handle direct JSON-RPC (for Claude's MCP client)
140 /**
141 * Claude's MCP client (via Anthropic API) sends JSON-RPC requests directly to the SSE endpoint
142 * as POST requests, rather than following the typical SSE flow:
143 * - Normal flow: GET /sse → establish SSE stream → POST /messages for JSON-RPC
144 * - Claude's flow: POST /sse with JSON-RPC body → expect immediate JSON response
145 *
146 * This method handles the direct JSON-RPC requests to maintain compatibility with Claude.
147 */
148 private function handle_direct_jsonrpc( WP_REST_Request $request, $data ) {
149 $id = $data['id'] ?? null;
150 $method = $data['method'] ?? null;
151
152 if ( json_last_error() !== JSON_ERROR_NONE ) {
153 return new WP_REST_Response( [
154 'jsonrpc' => '2.0',
155 'id' => null,
156 'error' => [ 'code' => -32700, 'message' => 'Parse error: invalid JSON' ]
157 ], 200 );
158 }
159
160 if ( ! is_array( $data ) || !$method ) {
161 return new WP_REST_Response( [
162 'jsonrpc' => '2.0',
163 'id' => $id,
164 'error' => [ 'code' => -32600, 'message' => 'Invalid Request' ]
165 ], 200 );
166 }
167
168 try {
169 $reply = null;
170
171 switch ( $method ) {
172 case 'initialize':
173 // Check if client requests a specific protocol version
174 $params = $data['params'] ?? [];
175 $requested_version = $params['protocolVersion'] ?? null;
176
177 if ( $requested_version && $requested_version !== $this->protocol_version ) {
178 if ( $this->logging ) {
179 Meow_MWAI_Logging::warn( "[MCP] Client requested protocol version {$requested_version}, but we only support {$this->protocol_version}" );
180 }
181 }
182
183 $reply = [
184 'jsonrpc' => '2.0',
185 'id' => $id,
186 'result' => [
187 'protocolVersion' => $this->protocol_version,
188 'serverInfo' => (object)[
189 'name' => get_bloginfo( 'name' ) . ' MCP',
190 'version' => $this->server_version,
191 ],
192 'capabilities' => [
193 'tools' => [ 'listChanged' => true ],
194 'prompts' => [ 'subscribe' => false, 'listChanged' => false ],
195 'resources' => [ 'listChanged' => false ],
196 ],
197 ],
198 ];
199 break;
200
201 case 'tools/list':
202 $reply = [
203 'jsonrpc' => '2.0',
204 'id' => $id,
205 'result' => [ 'tools' => $this->get_tools_list() ],
206 ];
207 break;
208
209 case 'tools/call':
210 $params = $data['params'] ?? [];
211 $tool = $params['name'] ?? '';
212 $arguments = $params['arguments']?? [];
213 $reply = $this->execute_tool( $tool, $arguments, $id );
214 break;
215
216 default:
217 $reply = [
218 'jsonrpc' => '2.0',
219 'id' => $id,
220 'error' => [ 'code' => -32601, 'message' => "Method not found: {$method}" ]
221 ];
222 }
223
224 return new WP_REST_Response( $reply, 200 );
225
226 } catch ( Exception $e ) {
227 return new WP_REST_Response( [
228 'jsonrpc' => '2.0',
229 'id' => $id,
230 'error' => [ 'code' => -32603, 'message' => 'Internal error', 'data' => $e->getMessage() ]
231 ], 200 );
232 }
233 }
234 #endregion
235
236 #region Handle SSE (stream loop)
237 private function reply( string $event, $data = null, string $enc = 'json' ) {
238 if ( $enc === 'json' && $data === null ) {
239 $this->log( "no data for {$event}");
240 return;
241 }
242 echo "event: {$event}\n";
243 if ( $enc === 'json' )
244 $data = $data === null ? '{}' : wp_json_encode( $data, JSON_UNESCAPED_UNICODE );
245 echo 'data: ' . $data . "\n\n";
246
247 if ( ob_get_level() ) ob_end_flush();
248 flush();
249
250 $this->last_action_time = time();
251 $this->log( "{$event}" );
252 }
253
254 private function generate_sse_id( $req ) {
255 $last = $req ? $req->get_header( 'last-event-id' ) : '';
256 return $last ?: str_replace( '-', '', wp_generate_uuid4() );
257 }
258
259 public function handle_sse( WP_REST_Request $request ) {
260
261 $raw_body = $request->get_body();
262 if ( $this->logging ) {
263 Meow_MWAI_Logging::log( '[MCP] Raw Body: ' . $raw_body );
264 }
265
266 // Handle POST request with JSON-RPC body (Claude's MCP client behavior)
267 // Claude's API sends JSON-RPC requests directly to the SSE endpoint instead of
268 // establishing an SSE connection first. This is non-standard but we need to support it.
269 // Expected: GET /sse (establish stream) → POST /messages (send JSON-RPC)
270 // Claude: POST /sse with JSON-RPC body → expects immediate JSON response
271 if ( $request->get_method() === 'POST' && !empty( $raw_body ) ) {
272 $data = json_decode( $raw_body, true );
273 if ( $data && isset( $data['method'] ) ) {
274 if ( $this->logging ) {
275 Meow_MWAI_Logging::log( '[MCP] Handling direct JSON-RPC for method: ' . $data['method'] );
276 }
277 // Process as a direct JSON-RPC request instead of starting SSE stream
278 return $this->handle_direct_jsonrpc( $request, $data );
279 }
280 }
281
282 @ini_set( 'zlib.output_compression', '0' );
283 @ini_set( 'output_buffering', '0' );
284 @ini_set( 'implicit_flush', '1' );
285 if ( function_exists( 'ob_implicit_flush' ) ) ob_implicit_flush( true );
286
287 header( 'Content-Type: text/event-stream' );
288 header( 'Cache-Control: no-cache' );
289 header( 'X-Accel-Buffering: no' );
290 header( 'Connection: keep-alive' );
291 while ( ob_get_level() ) ob_end_flush();
292
293 /* — greet client —*/
294 $this->session_id = $this->generate_sse_id( $request );
295 $this->last_action_time = time();
296 echo "id: {$this->session_id}\n\n";
297 flush();
298
299 $msg_uri = sprintf(
300 '%s/messages?session_id=%s',
301 rest_url( $this->namespace ),
302 $this->session_id
303 );
304 $this->reply( 'endpoint', $msg_uri, 'text' );
305 $this->log( 'SSE started' );
306
307 /* — main loop —*/
308 while ( true ) {
309 $max_time = 60 * 5; // 5 minutes
310 $idle = ( time() - $this->last_action_time ) >= $max_time;
311 if ( connection_aborted() || $idle ) {
312 $this->reply( 'bye' );
313 $this->log( $idle ? 'idle-timeout' : 'client abort' );
314 break;
315 }
316
317 foreach ( $this->fetch_messages( $this->session_id ) as $p )
318 $this->reply( 'message', $p );
319
320 usleep( 200000 ); // 200 ms
321 }
322 exit;
323 }
324 #endregion
325
326 #region Handle /messages (JSON-RPC ingress)
327 public function handle_message( WP_REST_Request $request ) {
328 $sess = sanitize_text_field( $request->get_param( 'session_id' ) );
329 $raw = $request->get_body();
330 $dat = json_decode( $raw, true );
331
332 if ( json_last_error() !== JSON_ERROR_NONE ) {
333 $this->queue_error( $sess, null, -32700, 'Parse error: invalid JSON' );
334 return new WP_REST_Response( null, 204 );
335 }
336 if ( ! is_array( $dat ) ) {
337 $this->queue_error( $sess, null, -32600, 'Invalid Request' );
338 return new WP_REST_Response( null, 204 );
339 }
340
341 $id = $dat['id'] ?? null;
342 $method = $dat['method'] ?? null;
343
344 /* — notifications —*/
345 if ( $method === 'initialized' ) return new WP_REST_Response( null, 204 );
346 if ( $method === 'mwai/kill' ) exit(0);
347
348 // It's a notification, no ID = no reply
349 if ( $id === null && $method !== null ) {
350 return new WP_REST_Response( null, 204 );
351 }
352
353 if ( !$method ) {
354 $this->queue_error( $sess, $id, -32600, 'Invalid Request: method missing' );
355 return new WP_REST_Response( null, 204 );
356 }
357
358 try {
359
360 $reply = null;
361
362 #region Methods switch
363 switch ( $method ) {
364
365 case 'initialize':
366 // Check if client requests a specific protocol version
367 $params = $data['params'] ?? [];
368 $requested_version = $params['protocolVersion'] ?? null;
369
370 if ( $requested_version && $requested_version !== $this->protocol_version ) {
371 if ( $this->logging ) {
372 Meow_MWAI_Logging::warn( "[MCP] Client requested protocol version {$requested_version}, but we only support {$this->protocol_version}" );
373 }
374 }
375
376 $reply = [
377 'jsonrpc' => '2.0',
378 'id' => $id,
379 'result' => [
380 'protocolVersion' => $this->protocol_version,
381 'serverInfo' => (object)[
382 'name' => get_bloginfo( 'name' ) . ' MCP',
383 'version' => $this->server_version,
384 ],
385 'capabilities' => [
386 'tools' => [ 'listChanged' => true ],
387 'prompts' => [ 'subscribe' => false, 'listChanged' => false ],
388 'resources' => [ 'listChanged' => false ],
389 ],
390 ],
391 ];
392 break;
393
394 case 'tools/list':
395 $reply = [
396 'jsonrpc' => '2.0',
397 'id' => $id,
398 'result' => [ 'tools' => $this->get_tools_list() ],
399 ];
400 break;
401
402 case 'resources/list':
403 $reply = [
404 'jsonrpc' => '2.0',
405 'id' => $id,
406 'result' => [ 'resources' => $this->get_resources_list() ],
407 ];
408 break;
409
410 case 'prompts/list':
411 $reply = [
412 'jsonrpc' => '2.0',
413 'id' => $id,
414 'result' => [ 'prompts' => $this->get_prompts_list() ],
415 ];
416 break;
417
418 case 'tools/call':
419 $params = $dat['params'] ?? [];
420 $tool = $params['name'] ?? '';
421 $arguments = $params['arguments']?? [];
422 $reply = $this->execute_tool( $tool, $arguments, $id );
423 break;
424
425 default:
426 $reply = $this->rpc_error( $id, -32601, "Method not found: {$method}" );
427 }
428 #endregion
429
430 if ( $reply ) $this->store_message( $sess, $reply );
431
432 } catch ( Exception $e ) {
433 $this->queue_error( $sess, $id, -32603, 'Internal error', $e->getMessage() );
434 }
435
436 return new WP_REST_Response( null, 204 );
437 }
438 #endregion
439
440 #region Tools Definitions
441 private function get_tools_list() {
442 $base_tools = [
443 [
444 'name' => 'mcp_ping',
445 'description' => 'Simple connectivity check. Returns the current GMT time and the WordPress site name. Whenever a tool call fails (error or timeout), immediately invoke mcp_ping to verify the server; if mcp_ping itself does not respond, assume the server is temporarily unreachable and pause additional tool calls.',
446 'inputSchema' => [ 'type' => 'object', 'properties' => (object) [] ],
447 ],
448 ];
449 return apply_filters( 'mwai_mcp_tools', $base_tools );
450 }
451 #endregion
452
453 #region Resources Definitions
454 private function get_resources_list() {
455 return [];
456 }
457 #endregion
458
459 #region Prompts Definitions
460 private function get_prompts_list() {
461 return [];
462 }
463 #endregion
464
465 #region Tools Call (execute_tool)
466 private function execute_tool( $tool, $args, $id ) {
467 try {
468 // Handle built-in tools first
469 if ( $tool === 'mcp_ping' ) {
470 $ping_data = [
471 'time' => gmdate( 'Y-m-d H:i:s' ),
472 'name' => get_bloginfo( 'name' ),
473 ];
474 return [
475 'jsonrpc' => '2.0',
476 'id' => $id,
477 'result' => [
478 'content' => [
479 [
480 'type' => 'text',
481 'text' => 'Ping successful: ' . wp_json_encode( $ping_data, JSON_PRETTY_PRINT ),
482 ],
483 ],
484 'data' => $ping_data,
485 ],
486 ];
487 }
488
489 // Let other modules handle their tools
490 $filtered = apply_filters( 'mwai_mcp_callback', null, $tool, $args, $id, $this );
491
492 if ( $filtered !== null ) {
493 // Check if it's already a full JSON-RPC response (backward compatibility)
494 if ( is_array( $filtered ) && isset( $filtered['jsonrpc'] ) && isset( $filtered['id'] ) ) {
495 return $filtered;
496 }
497
498 // Otherwise, wrap the result in proper JSON-RPC format
499 return [
500 'jsonrpc' => '2.0',
501 'id' => $id,
502 'result' => $this->format_tool_result( $filtered ),
503 ];
504 }
505
506 throw new Exception( "Unknown tool: {$tool}" );
507 } catch ( Exception $e ) {
508 return $this->rpc_error( $id, -32603, $e->getMessage() );
509 }
510 }
511 #endregion
512
513 #region Message Queue (per-message transient)
514 private function transient_key( $sess, $id ) {
515 return "{$this->queue_key}_{$sess}_{$id}";
516 }
517
518 private function store_message( $sess, $payload ) {
519 if ( !$sess ) return;
520 $idKey = array_key_exists( 'id', $payload ) ? ( $payload['id'] ?? 'NULL' ) : 'N/A';
521 set_transient( $this->transient_key( $sess, $idKey ), $payload, 30 );
522 $this->log( "queued #{$idKey}" );
523 }
524
525 private function fetch_messages( $sess ) {
526 global $wpdb;
527 $like = $wpdb->esc_like( '_transient_' . "{$this->queue_key}_{$sess}_" ) . '%';
528
529 $rows = $wpdb->get_results(
530 $wpdb->prepare(
531 "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
532 $like
533 ),
534 ARRAY_A
535 );
536
537 $msgs = [];
538 foreach ( $rows as $r ) {
539 $msgs[] = maybe_unserialize( $r['option_value'] );
540 delete_option( $r['option_name'] );
541 }
542 usort( $msgs, fn( $a,$b ) => ( $a['id'] ?? 0 ) <=> ( $b['id'] ?? 0 ) );
543 if ( $msgs ) $this->log( 'flush '.count($msgs).' msg(s)' );
544 return $msgs;
545 }
546 #endregion
547
548 #region Resources (note)
549 /*--------------------------------------------------*/
550 /**
551 * MCP also supports “resources” – static or dynamic data a client can
552 * retrieve by URL (e.g. `mcp://resource/posts/123`).
553 */
554 #endregion
555 }
556