PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.8
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-rest.php
ai-engine / labs Last commit date
mcp-core.php 8 hours ago mcp-oauth.php 1 month ago mcp-rest.php 1 week ago mcp.conf 1 year ago mcp.js 8 months ago mcp.md 8 months ago mcp.php 2 days ago wpai-connectors.php 1 month ago wpai-gateway-availability.php 2 months ago wpai-gateway-directory.php 2 months ago wpai-gateway-image-model.php 2 months ago wpai-gateway-model.php 2 months ago wpai-gateway-providers.php 2 months ago wpai-gateway.php 2 months ago
mcp-rest.php
330 lines
1 <?php
2
3 class Meow_MWAI_Labs_MCP_Rest {
4 // Bump the suffix when build_schema_from_args() changes so old cached schemas are ignored.
5 private $cache_key = 'mwai_mcp_tools_cache_v4';
6 private $allowed = [ 'posts', 'pages', 'media' ];
7
8 public function __construct() {
9 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
10 }
11
12 public function rest_api_init() {
13 add_filter( 'mwai_mcp_tools', [ $this, 'register_rest_tools' ] );
14 add_filter( 'mwai_mcp_callback', [ $this, 'handle_call' ], 10, 4 );
15 }
16
17 public function register_rest_tools( $prevTools ) {
18 $cached = get_transient( $this->cache_key );
19
20 if ( !$cached ) {
21 $tools = [];
22 $server = rest_get_server();
23 $routes = $server->get_routes();
24
25 foreach ( $this->allowed as $resource ) {
26 $base = "/wp/v2/{$resource}";
27 $item = "{$base}/(?P<id>[\d]+)";
28
29 if ( isset( $routes[ $base ] ) ) {
30 foreach ( $routes[ $base ] as $endpoint ) {
31 if ( !empty( $endpoint['methods']['GET'] ) ) {
32 $tools[ "list_{$resource}" ] = [
33 'name' => "list_{$resource}",
34 'description' => "List {$resource}",
35 'category' => 'Dynamic REST',
36 'inputSchema' => $this->build_schema_from_args( $endpoint['args'] ),
37 'outputSchema' => $this->build_output_schema(),
38 'accessLevel' => 'read',
39 ];
40 break;
41 }
42 }
43 }
44
45 if ( isset( $routes[ $item ] ) ) {
46 foreach ( $routes[ $item ] as $endpoint ) {
47 if ( !empty( $endpoint['methods']['GET'] ) ) {
48 $tools[ "get_{$resource}" ] = [
49 'name' => "get_{$resource}",
50 'description' => "Get single {$resource} by ID",
51 'category' => 'Dynamic REST',
52 'inputSchema' => $this->build_schema_from_args( $endpoint['args'] ),
53 'outputSchema' => $this->build_output_schema(),
54 'accessLevel' => 'read',
55 ];
56 break;
57 }
58 }
59 }
60
61 if ( isset( $routes[ $base ] ) ) {
62 foreach ( $routes[ $base ] as $endpoint ) {
63 if ( !empty( $endpoint['methods']['POST'] ) ) {
64 $tools[ "create_{$resource}" ] = [
65 'name' => "create_{$resource}",
66 'description' => "Create {$resource}",
67 'category' => 'Dynamic REST',
68 'inputSchema' => $this->build_schema_from_args( $endpoint['args'] ),
69 'outputSchema' => $this->build_output_schema(),
70 'accessLevel' => 'write',
71 ];
72 break;
73 }
74 }
75 }
76
77 if ( isset( $routes[ $item ] ) ) {
78 foreach ( $routes[ $item ] as $endpoint ) {
79 $methods = array_keys( $endpoint['methods'] );
80 if ( array_intersect( [ 'POST', 'PUT', 'PATCH' ], $methods ) ) {
81 $tools[ "update_{$resource}" ] = [
82 'name' => "update_{$resource}",
83 'description' => "Update {$resource}",
84 'category' => 'Dynamic REST',
85 'inputSchema' => $this->build_schema_from_args( $endpoint['args'] ),
86 'outputSchema' => $this->build_output_schema(),
87 'accessLevel' => 'write',
88 ];
89 break;
90 }
91 }
92 }
93
94 if ( isset( $routes[ $item ] ) ) {
95 foreach ( $routes[ $item ] as $endpoint ) {
96 if ( !empty( $endpoint['methods']['DELETE'] ) ) {
97 $tools[ "delete_{$resource}" ] = [
98 'name' => "delete_{$resource}",
99 'description' => "Delete {$resource}",
100 'category' => 'Dynamic REST',
101 'inputSchema' => $this->build_schema_from_args( $endpoint['args'] ),
102 'outputSchema' => $this->build_output_schema(),
103 'accessLevel' => 'admin',
104 ];
105 break;
106 }
107 }
108 }
109 }
110
111 set_transient( $this->cache_key, $tools, DAY_IN_SECONDS );
112 $cached = $tools;
113 }
114
115 return array_merge( array_values( $cached ), $prevTools );
116 }
117
118 private function build_schema_from_args( $args ) {
119 $schema = [
120 'type' => 'object',
121 'properties' => [],
122 'required' => [],
123 ];
124
125 // JSON Schema keys worth forwarding from WordPress REST arg definitions.
126 // PHP callbacks (sanitize_callback/validate_callback) and WP-only keys (arg_options,
127 // required) are intentionally excluded - clients would choke on them.
128 $allowed_keys = [
129 'type', 'description', 'enum', 'default', 'format',
130 'items', 'properties', 'additionalProperties',
131 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
132 'minLength', 'maxLength', 'pattern',
133 'minItems', 'maxItems', 'uniqueItems',
134 'oneOf', 'anyOf', 'allOf',
135 ];
136
137 foreach ( $args as $name => $def ) {
138 $property = [];
139 foreach ( $allowed_keys as $key ) {
140 if ( array_key_exists( $key, $def ) ) {
141 $property[ $key ] = $def[ $key ];
142 }
143 }
144 if ( !isset( $property['type'] ) ) {
145 $property['type'] = 'string';
146 }
147 if ( !isset( $property['description'] ) ) {
148 $property['description'] = '';
149 }
150
151 $schema['properties'][ $name ] = $this->normalize_schema_node( $property );
152
153 if ( !empty( $def['required'] ) ) {
154 $schema['required'][] = $name;
155 }
156 }
157
158 return $this->normalize_schema_node( $schema );
159 }
160
161 /**
162 * JSON Schema requires "properties" to be an object. WP REST arg definitions
163 * commonly set it to an empty PHP array (e.g. the "meta" arg on media/posts),
164 * which json_encode would serialize as [] and Claude's MCP validator rejects.
165 * Walk the schema and cast any empty "properties" / object-typed
166 * "additionalProperties" to (object)[] so they serialize as {}.
167 */
168 private function normalize_schema_node( $node ) {
169 if ( !is_array( $node ) ) {
170 return $node;
171 }
172
173 if ( array_key_exists( 'properties', $node ) ) {
174 if ( is_array( $node['properties'] ) ) {
175 if ( empty( $node['properties'] ) ) {
176 $node['properties'] = (object) [];
177 }
178 else {
179 foreach ( $node['properties'] as $key => $child ) {
180 $node['properties'][ $key ] = $this->normalize_schema_node( $child );
181 }
182 }
183 }
184 }
185
186 if ( isset( $node['items'] ) ) {
187 $node['items'] = $this->normalize_schema_node( $node['items'] );
188 }
189
190 if ( isset( $node['additionalProperties'] ) && is_array( $node['additionalProperties'] ) ) {
191 $node['additionalProperties'] = empty( $node['additionalProperties'] )
192 ? (object) []
193 : $this->normalize_schema_node( $node['additionalProperties'] );
194 }
195
196 foreach ( [ 'oneOf', 'anyOf', 'allOf' ] as $combinator ) {
197 if ( isset( $node[ $combinator ] ) && is_array( $node[ $combinator ] ) ) {
198 foreach ( $node[ $combinator ] as $i => $child ) {
199 $node[ $combinator ][ $i ] = $this->normalize_schema_node( $child );
200 }
201 }
202 }
203
204 // Some clients (notably Google Gemini) only allow "enum" on string-typed
205 // properties and reject the request with a 400 otherwise. WordPress REST
206 // sometimes defines integer enums, e.g. Jetpack's publicize "status" => [0, 1].
207 // Drop the enum when the type is not a string; the value still works, it just
208 // loses the schema-level enumeration. Coercing to string instead would risk
209 // breaking the endpoint's own integer validation when the tool is called.
210 if ( isset( $node['enum'] ) ) {
211 $type = $node['type'] ?? null;
212 $isStringType = $type === 'string'
213 || ( is_array( $type ) && in_array( 'string', $type, true ) )
214 || ( $type === null && count( array_filter( (array) $node['enum'], function ( $v ) {
215 return !is_string( $v );
216 } ) ) === 0 );
217 if ( !$isStringType ) {
218 unset( $node['enum'] );
219 }
220 }
221
222 return $node;
223 }
224
225 private function build_output_schema() {
226 return [
227 'type' => 'object',
228 'properties' => [
229 'content' => [
230 'type' => 'array',
231 'items' => [
232 'type' => 'object',
233 'properties' => [
234 'type' => [
235 'type' => 'string',
236 'description' => 'Block type, e.g. text or image',
237 ],
238 'text' => [
239 'type' => 'string',
240 'description' => 'Human-readable content',
241 ],
242 ],
243 'required' => [ 'type', 'text' ],
244 ],
245 ],
246 ],
247 'required' => [ 'content' ],
248 ];
249 }
250
251 public function handle_call( $existing, $tool, $args, $id ) {
252 if ( !empty( $existing ) ) {
253 return $existing;
254 }
255
256 $tools = get_transient( $this->cache_key );
257 if ( !isset( $tools[ $tool ] ) ) {
258 return $existing;
259 }
260
261 // Security check is already done in the MCP auth layer
262 // If we reach here, the user is authorized to use MCP
263
264 list( $action, $resource ) = explode( '_', $tool, 2 );
265 $path = "/wp/v2/{$resource}";
266 $method = 'GET';
267
268 if ( in_array( $action, [ 'get', 'update', 'delete' ], true ) ) {
269 if ( empty( $args['id'] ) ) {
270 return [
271 'jsonrpc' => '2.0',
272 'id' => $id,
273 'error' => [
274 'code' => -32602,
275 'message' => 'Missing parameter: id',
276 ],
277 ];
278 }
279 $path .= '/' . intval( $args['id'] );
280 }
281
282 switch ( $action ) {
283 case 'create':
284 case 'update':
285 $method = 'POST';
286 break;
287 case 'delete':
288 $method = 'DELETE';
289 break;
290 default:
291 $method = 'GET';
292 break;
293 }
294
295 $request = new WP_REST_Request( $method, $path );
296
297 if ( $method === 'GET' ) {
298 foreach ( $args as $key => $value ) {
299 $request->set_param( $key, $value );
300 }
301 }
302 else {
303 $request->set_body_params( $args );
304 }
305
306 $response = rest_do_request( $request );
307
308 if ( is_wp_error( $response ) || $response->get_status() >= 400 ) {
309 $error_obj = is_wp_error( $response ) ? $response : $response->as_error();
310
311 // Return error in old format for backward compatibility
312 // The execute_tool method will detect this and not re-wrap it
313 return [
314 'jsonrpc' => '2.0',
315 'id' => $id,
316 'error' => [
317 'code' => (int) ( $error_obj->get_error_code() ?: $response->get_status() ),
318 'message' => $error_obj->get_error_message(),
319 'data' => $error_obj->get_error_data() ?: null,
320 ],
321 ];
322 }
323
324 $data = $response->get_data();
325
326 // Return just the data - execute_tool will wrap it properly
327 return $data;
328 }
329 }
330