PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.2
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
328 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 $queue_key = 'mwai_mcp_msg';
8 private $session_id = null;
9 private $logging = false;
10 private $last_action_time = 0;
11 private $bearer_token = null;
12
13 #region Initialize
14 public function __construct( $core ) {
15 $this->core = $core;
16 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
17 }
18
19 public function rest_api_init() {
20 $this->bearer_token = $this->core->get_option( 'mcp_bearer_token' );
21 if ( !empty( $this->bearer_token ) ) {
22 add_filter( 'mwai_allow_mcp', [ $this, 'auth_via_bearer_token' ], 10, 2 );
23 }
24 register_rest_route( $this->namespace, '/sse', [
25 'methods' => 'GET',
26 'callback' => [ $this, 'handle_sse' ],
27 'permission_callback' => function( $request ) {
28 return $this->can_access_mcp( $request );
29 },
30 ] );
31
32 register_rest_route( $this->namespace, '/messages', [
33 'methods' => 'POST',
34 'callback' => [ $this, 'handle_message' ],
35 'permission_callback' => function( $request ) {
36 return $this->can_access_mcp( $request );
37 },
38 ] );
39 }
40 #endregion
41
42 #region Auth (Bearer token)
43 function can_access_mcp( $request ) {
44 $logged_in = is_user_logged_in();
45 return apply_filters( 'mwai_allow_mcp', $logged_in, $request );
46 }
47
48 public function auth_via_bearer_token( $allow, $request ) {
49 $hdr = $request->get_header( 'authorization' );
50 if ( $hdr && preg_match( '/Bearer\s+(.+)/i', $hdr, $m ) &&
51 hash_equals( $this->bearer_token, trim( $m[1] ) ) ) {
52 if ( $admin = $this->core->get_admin_user() ) {
53 wp_set_current_user( $admin->ID, $admin->user_login );
54 }
55 return true;
56 }
57 // ?token=xyz fallback (optional)
58 $q = sanitize_text_field( $request->get_param( 'token' ) );
59 if ( $q && hash_equals( $this->bearer_token, $q ) ) return true;
60 return $allow;
61 }
62 #endregion
63
64 #region Helpers (log / JSON-RPC utils)
65 private function log( $msg ) {
66 if ( $this->logging ) error_log( "MCP ({$this->session_id}): {$msg}" );
67 }
68
69 /** Wrap a JSON-RPC error object */
70 private function rpc_error( $id, int $code, string $msg, $extra = null ): array {
71 $err = [ 'code' => $code, 'message' => $msg ];
72 if ( $extra !== null ) $err['data'] = $extra;
73 return [ 'jsonrpc' => '2.0', 'id' => $id, 'error' => $err ];
74 }
75
76 /** Queue an error for SSE delivery */
77 private function queue_error( $sess, $id, int $code, string $msg, $extra = null ): void {
78 $this->store_message( $sess, $this->rpc_error( $id, $code, $msg, $extra ) );
79 }
80 #endregion
81
82 #region Handle SSE (stream loop)
83 private function reply( string $event, $data = null, string $enc = 'json' ) {
84 if ( $enc === 'json' && $data === null ) {
85 $this->log( "no data for {$event}");
86 return;
87 }
88 echo "event: {$event}\n";
89 if ( $enc === 'json' )
90 $data = $data === null ? '{}' : wp_json_encode( $data, JSON_UNESCAPED_UNICODE );
91 echo 'data: ' . $data . "\n\n";
92
93 if ( ob_get_level() ) ob_end_flush();
94 flush();
95
96 $this->last_action_time = time();
97 $this->log( "{$event}" );
98 }
99
100 private function generate_sse_id( $req ) {
101 $last = $req ? $req->get_header( 'last-event-id' ) : '';
102 return $last ?: str_replace( '-', '', wp_generate_uuid4() );
103 }
104
105 public function handle_sse( WP_REST_Request $request ) {
106 @ini_set( 'zlib.output_compression', '0' );
107 @ini_set( 'output_buffering', '0' );
108 @ini_set( 'implicit_flush', '1' );
109 if ( function_exists( 'ob_implicit_flush' ) ) ob_implicit_flush( true );
110
111 header( 'Content-Type: text/event-stream' );
112 header( 'Cache-Control: no-cache' );
113 header( 'X-Accel-Buffering: no' );
114 header( 'Connection: keep-alive' );
115 while ( ob_get_level() ) ob_end_flush();
116
117 /* — greet client —*/
118 $this->session_id = $this->generate_sse_id( $request );
119 $this->last_action_time = time();
120 echo "id: {$this->session_id}\n\n";
121 flush();
122
123 $msg_uri = sprintf(
124 '%s/messages?session_id=%s',
125 rest_url( $this->namespace ),
126 $this->session_id
127 );
128 $this->reply( 'endpoint', $msg_uri, 'text' );
129 $this->log( 'SSE started' );
130
131 /* — main loop —*/
132 while ( true ) {
133 $max_time = 60 * 5; // 5 minutes
134 $idle = ( time() - $this->last_action_time ) >= $max_time;
135 if ( connection_aborted() || $idle ) {
136 $this->reply( 'bye' );
137 $this->log( $idle ? 'idle-timeout' : 'client abort' );
138 break;
139 }
140
141 foreach ( $this->fetch_messages( $this->session_id ) as $p )
142 $this->reply( 'message', $p );
143
144 usleep( 200000 ); // 200 ms
145 }
146 exit;
147 }
148 #endregion
149
150 #region Handle /messages (JSON-RPC ingress)
151 public function handle_message( WP_REST_Request $request ) {
152
153 $sess = sanitize_text_field( $request->get_param( 'session_id' ) );
154 $raw = $request->get_body();
155 $dat = json_decode( $raw, true );
156
157 if ( json_last_error() !== JSON_ERROR_NONE ) {
158 $this->queue_error( $sess, null, -32700, 'Parse error: invalid JSON' );
159 return new WP_REST_Response( null, 204 );
160 }
161 if ( ! is_array( $dat ) ) {
162 $this->queue_error( $sess, null, -32600, 'Invalid Request' );
163 return new WP_REST_Response( null, 204 );
164 }
165
166 $id = $dat['id'] ?? null;
167 $method = $dat['method'] ?? null;
168
169 /* — notifications —*/
170 if ( $method === 'initialized' ) return new WP_REST_Response( null, 204 );
171 if ( $method === 'mwai/kill' ) exit(0);
172
173 // It's a notification, no ID = no reply
174 if ( $id === null && $method !== null ) {
175 return new WP_REST_Response( null, 204 );
176 }
177
178 if ( !$method ) {
179 $this->queue_error( $sess, $id, -32600, 'Invalid Request: method missing' );
180 return new WP_REST_Response( null, 204 );
181 }
182
183 try {
184
185 $reply = null;
186
187 #region Methods switch
188 switch ( $method ) {
189
190 case 'initialize':
191 $reply = [
192 'jsonrpc' => '2.0',
193 'id' => $id,
194 'result' => [
195 'protocolVersion' => '2025-03-26',
196 'serverInfo' => (object)[
197 'name' => get_bloginfo( 'name' ) . ' MCP',
198 'version' => $this->server_version,
199 ],
200 'capabilities' => [
201 'tools' => [ 'listChanged' => true ],
202 'prompts' => [ 'subscribe' => false, 'listChanged' => false ],
203 'resources' => [ 'listChanged' => false ],
204 ],
205 ],
206 ];
207 break;
208
209 case 'tools/list':
210 $reply = [
211 'jsonrpc' => '2.0',
212 'id' => $id,
213 'result' => [ 'tools' => $this->get_tools_list() ],
214 ];
215 break;
216
217 case 'resources/list':
218 $reply = [
219 'jsonrpc' => '2.0',
220 'id' => $id,
221 'result' => [ 'resources' => $this->get_resources_list() ],
222 ];
223 break;
224
225 case 'prompts/list':
226 $reply = [
227 'jsonrpc' => '2.0',
228 'id' => $id,
229 'result' => [ 'prompts' => $this->get_prompts_list() ],
230 ];
231 break;
232
233 case 'tools/call':
234 $params = $dat['params'] ?? [];
235 $tool = $params['name'] ?? '';
236 $arguments = $params['arguments']?? [];
237 $reply = $this->execute_tool( $tool, $arguments, $id );
238 break;
239
240 default:
241 $reply = $this->rpc_error( $id, -32601, "Method not found: {$method}" );
242 }
243 #endregion
244
245 if ( $reply ) $this->store_message( $sess, $reply );
246
247 } catch ( Exception $e ) {
248 $this->queue_error( $sess, $id, -32603, 'Internal error', $e->getMessage() );
249 }
250
251 return new WP_REST_Response( null, 204 );
252 }
253 #endregion
254
255 #region Tools Definitions
256 private function get_tools_list() {
257 return apply_filters( 'mwai_mcp_tools', [] );
258 }
259 #endregion
260
261 #region Resources Definitions
262 private function get_resources_list() {
263 return [];
264 }
265 #endregion
266
267 #region Prompts Definitions
268 private function get_prompts_list() {
269 return [];
270 }
271 #endregion
272
273 #region Tools Call (execute_tool)
274 private function execute_tool( $tool, $args, $id ) {
275 try {
276 $filtered = apply_filters( 'mwai_mcp_callback', null, $tool, $args, $id, $this );
277 if ( is_array( $filtered ) && isset( $filtered['id'] ) ) return $filtered;
278 throw new Exception( "Unknown tool: {$tool}" );
279 } catch ( Exception $e ) {
280 return $this->rpc_error( $id, -32603, $e->getMessage() );
281 }
282 }
283 #endregion
284
285 #region Message Queue (per-message transient)
286 private function transient_key( $sess, $id ) {
287 return "{$this->queue_key}_{$sess}_{$id}";
288 }
289
290 private function store_message( $sess, $payload ) {
291 if ( !$sess ) return;
292 $idKey = array_key_exists( 'id', $payload ) ? ( $payload['id'] ?? 'NULL' ) : 'N/A';
293 set_transient( $this->transient_key( $sess, $idKey ), $payload, 30 );
294 $this->log( "queued #{$idKey}" );
295 }
296
297 private function fetch_messages( $sess ) {
298 global $wpdb;
299 $like = $wpdb->esc_like( '_transient_' . "{$this->queue_key}_{$sess}_" ) . '%';
300
301 $rows = $wpdb->get_results(
302 $wpdb->prepare(
303 "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
304 $like
305 ),
306 ARRAY_A
307 );
308
309 $msgs = [];
310 foreach ( $rows as $r ) {
311 $msgs[] = maybe_unserialize( $r['option_value'] );
312 delete_option( $r['option_name'] );
313 }
314 usort( $msgs, fn( $a,$b ) => ( $a['id'] ?? 0 ) <=> ( $b['id'] ?? 0 ) );
315 if ( $msgs ) $this->log( 'flush '.count($msgs).' msg(s)' );
316 return $msgs;
317 }
318 #endregion
319
320 #region Resources (note)
321 /*--------------------------------------------------*/
322 /**
323 * MCP also supports “resources” – static or dynamic data a client can
324 * retrieve by URL (e.g. `mcp://resource/posts/123`).
325 */
326 #endregion
327 }
328