PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.4
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-rest.php
ai-engine / labs Last commit date
mcp-core.php 3 weeks ago mcp-oauth.php 1 month ago mcp-rest.php 1 month ago mcp.conf 1 year ago mcp.js 8 months ago mcp.md 8 months ago mcp.php 1 month 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
312 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_v3';
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 return $node;
205 }
206
207 private function build_output_schema() {
208 return [
209 'type' => 'object',
210 'properties' => [
211 'content' => [
212 'type' => 'array',
213 'items' => [
214 'type' => 'object',
215 'properties' => [
216 'type' => [
217 'type' => 'string',
218 'description' => 'Block type, e.g. text or image',
219 ],
220 'text' => [
221 'type' => 'string',
222 'description' => 'Human-readable content',
223 ],
224 ],
225 'required' => [ 'type', 'text' ],
226 ],
227 ],
228 ],
229 'required' => [ 'content' ],
230 ];
231 }
232
233 public function handle_call( $existing, $tool, $args, $id ) {
234 if ( !empty( $existing ) ) {
235 return $existing;
236 }
237
238 $tools = get_transient( $this->cache_key );
239 if ( !isset( $tools[ $tool ] ) ) {
240 return $existing;
241 }
242
243 // Security check is already done in the MCP auth layer
244 // If we reach here, the user is authorized to use MCP
245
246 list( $action, $resource ) = explode( '_', $tool, 2 );
247 $path = "/wp/v2/{$resource}";
248 $method = 'GET';
249
250 if ( in_array( $action, [ 'get', 'update', 'delete' ], true ) ) {
251 if ( empty( $args['id'] ) ) {
252 return [
253 'jsonrpc' => '2.0',
254 'id' => $id,
255 'error' => [
256 'code' => -32602,
257 'message' => 'Missing parameter: id',
258 ],
259 ];
260 }
261 $path .= '/' . intval( $args['id'] );
262 }
263
264 switch ( $action ) {
265 case 'create':
266 case 'update':
267 $method = 'POST';
268 break;
269 case 'delete':
270 $method = 'DELETE';
271 break;
272 default:
273 $method = 'GET';
274 break;
275 }
276
277 $request = new WP_REST_Request( $method, $path );
278
279 if ( $method === 'GET' ) {
280 foreach ( $args as $key => $value ) {
281 $request->set_param( $key, $value );
282 }
283 }
284 else {
285 $request->set_body_params( $args );
286 }
287
288 $response = rest_do_request( $request );
289
290 if ( is_wp_error( $response ) || $response->get_status() >= 400 ) {
291 $error_obj = is_wp_error( $response ) ? $response : $response->as_error();
292
293 // Return error in old format for backward compatibility
294 // The execute_tool method will detect this and not re-wrap it
295 return [
296 'jsonrpc' => '2.0',
297 'id' => $id,
298 'error' => [
299 'code' => (int) ( $error_obj->get_error_code() ?: $response->get_status() ),
300 'message' => $error_obj->get_error_message(),
301 'data' => $error_obj->get_error_data() ?: null,
302 ],
303 ];
304 }
305
306 $data = $response->get_data();
307
308 // Return just the data - execute_tool will wrap it properly
309 return $data;
310 }
311 }
312