PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.5.2
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.5.2
0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 0.3.8 All 32 releases
code-engine / classes / mcp.php

mcp.php in Code Engine – PHP Snippets, AI Functions & Automation for WordPress 0.5.2, at classes/mcp.php

730 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class Meow_MWCODE_MCP {
4 private $core;
5 private $api;
6
7 public function __construct( $core ) {
8 $this->core = $core;
9
10 // Initialize everything on 'init' to ensure options are loaded
11 add_action( 'init', array( $this, 'init' ), 20 );
12 }
13
14 public function init() {
15 global $mwcode, $mwai;
16 $this->api = $mwcode;
17
18 // Nothing to do without AI Engine's MCP server.
19 if ( !isset( $mwai ) ) {
20 return;
21 }
22
23 // Two independent surfaces share AI Engine's MCP server, each behind its own
24 // global master switch (and AI Engine's own MCP auth gate upstream):
25 // - the management/internal API (the 'mcp_support' option)
26 // - individual Callable functions, opted-in per snippet via 'functionMcp' and
27 // only exposed when the 'mcp_functions' option is enabled
28 // Either one alone is enough to justify hooking the filters.
29 add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) );
30 add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 );
31 }
32
33 public function register_tools( $tools ) {
34 // Individual Callable functions that opted in to MCP, exposed as first-class tools.
35 // Gated behind a global master switch (Settings > For Developers > MCP Functions)
36 // in addition to each snippet's per-function opt-in.
37 if ( $this->core->get_option( 'mcp_functions', false ) ) {
38 $tools = $this->register_function_tools( $tools );
39 }
40
41 // Code Engine's management/internal API (Settings > For Developers > MCP Support).
42 if ( $this->core->get_option( 'mcp_support', false ) ) {
43 $tools = $this->register_management_tools( $tools );
44 }
45
46 return $tools;
47 }
48
49 /**
50 * Map a Code Engine argument type to a JSON Schema type.
51 * Returns null for 'mixed'/unknown so the schema leaves the type open.
52 */
53 private function mcp_type( $type ) {
54 switch ( $type ) {
55 case 'number': return 'number';
56 case 'boolean': return 'boolean';
57 case 'array': return 'array';
58 case 'object': return 'object';
59 case 'string': return 'string';
60 default: return null;
61 }
62 }
63
64 /**
65 * Return the active Callable (function) snippets that opted in to MCP exposure.
66 */
67 private function get_mcp_functions() {
68 global $mwcode;
69 if ( !isset( $mwcode ) || !method_exists( $mwcode, 'getSnippets' ) ) {
70 return [];
71 }
72 $functions = $mwcode->getSnippets( true, 'function' );
73 if ( empty( $functions ) ) {
74 return [];
75 }
76 return array_values( array_filter( $functions, function ( $fn ) {
77 return !empty( $fn['functionMcp'] ) && !empty( $fn['functionName'] );
78 } ) );
79 }
80
81 /**
82 * Register each opted-in Callable function as its own MCP tool, named after the
83 * function, with an input schema derived from its declared arguments.
84 */
85 public function register_function_tools( $tools ) {
86 foreach ( $this->get_mcp_functions() as $fn ) {
87 $properties = [];
88 $required = [];
89
90 $argsDict = isset( $fn['functionArgsDict'] ) && is_array( $fn['functionArgsDict'] ) ? $fn['functionArgsDict'] : [];
91 foreach ( $argsDict as $argName => $arg ) {
92 $name = ltrim( $argName, '$' );
93 if ( $name === '' ) {
94 continue;
95 }
96 $prop = [];
97 $type = $this->mcp_type( $arg['type'] ?? 'string' );
98 if ( $type !== null ) {
99 $prop['type'] = $type;
100 }
101 if ( !empty( $arg['desc'] ) ) {
102 $prop['description'] = $arg['desc'];
103 }
104 $properties[ $name ] = $prop;
105 if ( !empty( $arg['required'] ) ) {
106 $required[] = $name;
107 }
108 }
109
110 $schema = [
111 'type' => 'object',
112 // Cast so an argument-less function still serializes as {} and not [].
113 'properties' => (object) $properties,
114 ];
115 if ( !empty( $required ) ) {
116 $schema['required'] = $required;
117 }
118
119 $tools[] = [
120 'name' => $fn['functionName'],
121 'description' => !empty( $fn['description'] ) ? $fn['description'] : ( 'Code Engine function: ' . $fn['functionName'] ),
122 'category' => 'Code Engine (Functions)',
123 'inputSchema' => $schema,
124 'annotations' => [ 'openWorldHint' => true ],
125 ];
126 }
127 return $tools;
128 }
129
130 public function register_management_tools( $tools ) {
131 // Get Snippet
132 $tools[] = [
133 'name' => 'mwcode_get_snippet',
134 'description' => 'Get a Code Engine snippet by its ID',
135 'category' => 'Code Engine',
136 'inputSchema' => [
137 'type' => 'object',
138 'properties' => [
139 'id' => [
140 'type' => 'integer',
141 'description' => 'The snippet ID'
142 ],
143 'options' => [
144 'type' => 'object',
145 'description' => 'Optional filtering options',
146 'properties' => [
147 'php_ready_args' => [
148 'type' => 'boolean',
149 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
150 ]
151 ]
152 ]
153 ],
154 'required' => ['id']
155 ]
156 ];
157
158 // Get Snippet by Name
159 $tools[] = [
160 'name' => 'mwcode_get_snippet_by_name',
161 'description' => 'Get a Code Engine snippet by its name',
162 'category' => 'Code Engine',
163 'inputSchema' => [
164 'type' => 'object',
165 'properties' => [
166 'name' => [
167 'type' => 'string',
168 'description' => 'The snippet name'
169 ],
170 'options' => [
171 'type' => 'object',
172 'description' => 'Optional filtering options',
173 'properties' => [
174 'php_ready_args' => [
175 'type' => 'boolean',
176 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
177 ]
178 ]
179 ]
180 ],
181 'required' => ['name']
182 ]
183 ];
184
185 // Get Snippets
186 $tools[] = [
187 'name' => 'mwcode_get_snippets',
188 'description' => 'Get all Code Engine snippets, optionally filtered by scope',
189 'category' => 'Code Engine',
190 'inputSchema' => [
191 'type' => 'object',
192 'properties' => [
193 'safe' => [
194 'type' => 'boolean',
195 'description' => 'Whether to filter out snippets with invalid names',
196 'default' => true
197 ],
198 'scope' => [
199 'type' => 'string',
200 'description' => 'Optional scope filter',
201 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
202 ]
203 ]
204 ]
205 ];
206
207 // Execute Snippet
208 $tools[] = [
209 'name' => 'mwcode_execute_snippet',
210 'description' => 'Execute a Code Engine snippet by its ID',
211 'category' => 'Code Engine',
212 'inputSchema' => [
213 'type' => 'object',
214 'properties' => [
215 'id' => [
216 'type' => 'integer',
217 'description' => 'The snippet ID'
218 ],
219 'args' => [
220 'type' => 'object',
221 'description' => 'Arguments to pass to the snippet (key-value pairs)',
222 'additionalProperties' => true
223 ]
224 ],
225 'required' => ['id']
226 ]
227 ];
228
229 // Execute Snippet by Name
230 $tools[] = [
231 'name' => 'mwcode_execute_snippet_by_name',
232 'description' => 'Execute a Code Engine snippet by its name',
233 'category' => 'Code Engine',
234 'inputSchema' => [
235 'type' => 'object',
236 'properties' => [
237 'name' => [
238 'type' => 'string',
239 'description' => 'The snippet name'
240 ],
241 'args' => [
242 'type' => 'object',
243 'description' => 'Arguments to pass to the snippet (key-value pairs)',
244 'additionalProperties' => true
245 ]
246 ],
247 'required' => ['name']
248 ]
249 ];
250
251 // Create Snippet
252 $tools[] = [
253 'name' => 'mwcode_create_snippet',
254 'description' => 'Create a new Code Engine snippet',
255 'category' => 'Code Engine',
256 'inputSchema' => [
257 'type' => 'object',
258 'properties' => [
259 'name' => [
260 'type' => 'string',
261 'description' => 'Name of the snippet'
262 ],
263 'code' => [
264 'type' => 'string',
265 'description' => 'Code of the snippet'
266 ],
267 'scope' => [
268 'type' => 'string',
269 'description' => 'Scope of the snippet',
270 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'],
271 'default' => 'function'
272 ],
273 'options' => [
274 'type' => 'object',
275 'description' => 'Additional options for the snippet',
276 'properties' => [
277 'target' => [
278 'type' => 'string',
279 'enum' => ['php', 'js'],
280 'description' => 'Target language (for function snippets)'
281 ],
282 'description' => [
283 'type' => 'string',
284 'description' => 'Description of the snippet'
285 ],
286 'args' => [
287 'type' => 'array',
288 'description' => 'Arguments for function snippets',
289 'items' => [ 'type' => 'string' ]
290 ],
291 'argsData' => [
292 'type' => 'object',
293 'description' => 'Argument data for function snippets',
294 'additionalProperties' => [
295 'type' => 'object',
296 'properties' => [
297 'type' => [ 'type' => 'string' ],
298 'description' => [ 'type' => 'string' ],
299 'default' => [ 'type' => 'string' ]
300 ]
301 ]
302 ],
303 'behavior' => [
304 'type' => 'string',
305 'enum' => ['dynamic', 'static'],
306 'description' => 'Behavior for function snippets'
307 ],
308 'mcp' => [
309 'type' => 'boolean',
310 'description' => 'For function snippets: expose this function as its own MCP tool in AI Engine, named after the function and callable directly by external agents. Defaults to false.'
311 ],
312 'tags' => [
313 'type' => 'array',
314 'description' => 'Array of tags',
315 'items' => [ 'type' => 'string' ]
316 ],
317 'priority' => [
318 'type' => 'integer',
319 'description' => 'Execution priority'
320 ],
321 'active' => [
322 'type' => 'boolean',
323 'description' => 'Active status'
324 ],
325 'endpoint' => [
326 'type' => 'string',
327 'description' => 'REST endpoint'
328 ],
329 'method' => [
330 'type' => 'string',
331 'enum' => ['GET', 'POST'],
332 'description' => 'HTTP method'
333 ],
334 'intervalHours' => [
335 'type' => 'integer',
336 'description' => 'Hours for scheduled snippets'
337 ],
338 'intervalMinutes' => [
339 'type' => 'integer',
340 'description' => 'Minutes for scheduled snippets'
341 ]
342 ]
343 ]
344 ],
345 'required' => ['name', 'code']
346 ]
347 ];
348
349 // Update Snippet
350 $tools[] = [
351 'name' => 'mwcode_update_snippet',
352 'description' => 'Update an existing Code Engine snippet',
353 'category' => 'Code Engine',
354 'inputSchema' => [
355 'type' => 'object',
356 'properties' => [
357 'id' => [
358 'type' => 'integer',
359 'description' => 'ID of the snippet to update'
360 ],
361 'params' => [
362 'type' => 'object',
363 'description' => 'Parameters to update',
364 'properties' => [
365 'name' => [ 'type' => 'string' ],
366 'code' => [ 'type' => 'string' ],
367 'scope' => [
368 'type' => 'string',
369 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
370 ],
371 'description' => [ 'type' => 'string' ],
372 'active' => [ 'type' => 'boolean' ],
373 'priority' => [ 'type' => 'integer' ],
374 'tags' => [
375 'type' => 'array',
376 'items' => [ 'type' => 'string' ]
377 ],
378 'endpoint' => [ 'type' => 'string' ],
379 'method' => [
380 'type' => 'string',
381 'enum' => ['GET', 'POST']
382 ],
383 'target' => [
384 'type' => 'string',
385 'enum' => ['php', 'js']
386 ],
387 'args' => [
388 'type' => 'array',
389 'items' => [ 'type' => 'string' ]
390 ],
391 'argsData' => [
392 'type' => 'object',
393 'additionalProperties' => true
394 ],
395 'behavior' => [
396 'type' => 'string',
397 'enum' => ['dynamic', 'static']
398 ],
399 'mcp' => [
400 'type' => 'boolean',
401 'description' => 'For function snippets: expose this function as its own MCP tool in AI Engine, named after the function and callable directly by external agents.'
402 ],
403 'intervalHours' => [ 'type' => 'integer' ],
404 'intervalMinutes' => [ 'type' => 'integer' ]
405 ]
406 ]
407 ],
408 'required' => ['id']
409 ]
410 ];
411
412 // Delete Snippet
413 $tools[] = [
414 'name' => 'mwcode_delete_snippet',
415 'description' => 'Delete a Code Engine snippet by its ID',
416 'category' => 'Code Engine',
417 'inputSchema' => [
418 'type' => 'object',
419 'properties' => [
420 'id' => [
421 'type' => 'integer',
422 'description' => 'The snippet ID'
423 ]
424 ],
425 'required' => ['id']
426 ]
427 ];
428
429 // Delete Snippet by Name
430 $tools[] = [
431 'name' => 'mwcode_delete_snippet_by_name',
432 'description' => 'Delete a Code Engine snippet by its name',
433 'category' => 'Code Engine',
434 'inputSchema' => [
435 'type' => 'object',
436 'properties' => [
437 'name' => [
438 'type' => 'string',
439 'description' => 'The snippet name'
440 ]
441 ],
442 'required' => ['name']
443 ]
444 ];
445
446 // Activate Snippet
447 $tools[] = [
448 'name' => 'mwcode_activate_snippet',
449 'description' => 'Activate a Code Engine snippet',
450 'category' => 'Code Engine',
451 'inputSchema' => [
452 'type' => 'object',
453 'properties' => [
454 'id' => [
455 'type' => 'integer',
456 'description' => 'The snippet ID'
457 ]
458 ],
459 'required' => ['id']
460 ]
461 ];
462
463 // Deactivate Snippet
464 $tools[] = [
465 'name' => 'mwcode_deactivate_snippet',
466 'description' => 'Deactivate a Code Engine snippet',
467 'category' => 'Code Engine',
468 'inputSchema' => [
469 'type' => 'object',
470 'properties' => [
471 'id' => [
472 'type' => 'integer',
473 'description' => 'The snippet ID'
474 ]
475 ],
476 'required' => ['id']
477 ]
478 ];
479
480 // Get Snippets by Scope
481 $tools[] = [
482 'name' => 'mwcode_get_snippets_by_scope',
483 'description' => 'Get Code Engine snippets filtered by scope',
484 'category' => 'Code Engine',
485 'inputSchema' => [
486 'type' => 'object',
487 'properties' => [
488 'scope' => [
489 'type' => 'string',
490 'description' => 'The scope to filter by',
491 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
492 ],
493 'filters' => [
494 'type' => 'object',
495 'description' => 'Additional filters',
496 'properties' => [
497 'active' => [
498 'type' => 'boolean',
499 'description' => 'Filter by active status'
500 ],
501 'tags' => [
502 'type' => 'array',
503 'description' => 'Filter by tags',
504 'items' => [ 'type' => 'string' ]
505 ]
506 ]
507 ]
508 ],
509 'required' => ['scope']
510 ]
511 ];
512
513 // Get Active Snippets
514 $tools[] = [
515 'name' => 'mwcode_get_active_snippets',
516 'description' => 'Get all active Code Engine snippets',
517 'category' => 'Code Engine',
518 'inputSchema' => [
519 'type' => 'object',
520 'properties' => [
521 'scope' => [
522 'type' => 'string',
523 'description' => 'Optional scope filter',
524 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
525 ]
526 ]
527 ]
528 ];
529
530 // Snippet Exists
531 $tools[] = [
532 'name' => 'mwcode_snippet_exists',
533 'description' => 'Check if a Code Engine snippet exists by ID',
534 'category' => 'Code Engine',
535 'inputSchema' => [
536 'type' => 'object',
537 'properties' => [
538 'id' => [
539 'type' => 'integer',
540 'description' => 'The snippet ID'
541 ]
542 ],
543 'required' => ['id']
544 ]
545 ];
546
547 // Snippet Exists by Name
548 $tools[] = [
549 'name' => 'mwcode_snippet_exists_by_name',
550 'description' => 'Check if a Code Engine snippet exists by name',
551 'category' => 'Code Engine',
552 'inputSchema' => [
553 'type' => 'object',
554 'properties' => [
555 'name' => [
556 'type' => 'string',
557 'description' => 'The snippet name'
558 ]
559 ],
560 'required' => ['name']
561 ]
562 ];
563
564 // Validate Snippet Code
565 $tools[] = [
566 'name' => 'mwcode_validate_snippet_code',
567 'description' => 'Validate snippet code syntax',
568 'category' => 'Code Engine',
569 'inputSchema' => [
570 'type' => 'object',
571 'properties' => [
572 'code' => [
573 'type' => 'string',
574 'description' => 'The snippet code to validate'
575 ],
576 'target' => [
577 'type' => 'string',
578 'description' => 'The target language',
579 'enum' => ['php', 'js'],
580 'default' => 'php'
581 ]
582 ],
583 'required' => ['code']
584 ]
585 ];
586
587 return $tools;
588 }
589
590 /**
591 * Execute a Callable function exposed via MCP. Returns $result unchanged when the
592 * tool name does not match one of our opted-in functions, so the filter chain
593 * continues to the management tools (and other plugins).
594 */
595 private function handle_function_execution( $result, $tool, $args ) {
596 // Master switch: even an opted-in Callable is unreachable via MCP unless the
597 // site has explicitly enabled function exposure. Returning $result unchanged
598 // lets the filter chain fall through to the management tools and other plugins.
599 if ( !$this->core->get_option( 'mcp_functions', false ) ) {
600 return $result;
601 }
602
603 $match = null;
604 foreach ( $this->get_mcp_functions() as $fn ) {
605 if ( $fn['functionName'] === $tool ) {
606 $match = $fn;
607 break;
608 }
609 }
610 if ( $match === null ) {
611 return $result;
612 }
613
614 if ( !$this->api ) {
615 return [ 'success' => false, 'error' => 'Code Engine API not initialized' ];
616 }
617
618 // getSnippets() rows expose the database id as 'id' (function metadata uses 'snippetId').
619 $snippetId = $match['id'] ?? $match['snippetId'] ?? null;
620
621 try {
622 $output = $this->api->executeSnippet( $snippetId, is_array( $args ) ? $args : [] );
623 return [ 'success' => true, 'data' => $output ];
624 }
625 catch ( Exception $e ) {
626 return [ 'success' => false, 'error' => $e->getMessage() ];
627 }
628 }
629
630 public function handle_tool_execution( $result, $tool, $args, $id ) {
631 // Individual Callable functions exposed via MCP take priority (named after the function).
632 $handled = $this->handle_function_execution( $result, $tool, $args );
633 if ( $handled !== $result ) {
634 return $handled;
635 }
636
637 // Management/internal API tools are gated behind the master switch.
638 if ( !$this->core->get_option( 'mcp_support', false ) ) {
639 return $result;
640 }
641
642 // Only handle our tools
643 if ( strpos( $tool, 'mwcode_' ) !== 0 ) {
644 return $result;
645 }
646
647 // Ensure API is initialized
648 if ( !$this->api ) {
649 return [ 'success' => false, 'error' => 'Code Engine API not initialized' ];
650 }
651
652 try {
653 switch ( $tool ) {
654 case 'mwcode_get_snippet':
655 $data = $this->api->getSnippet( $args['id'], $args['options'] ?? [] );
656 return [ 'success' => true, 'data' => $data ];
657
658 case 'mwcode_get_snippet_by_name':
659 $data = $this->api->getSnippetByName( $args['name'], $args['options'] ?? [] );
660 return [ 'success' => true, 'data' => $data ];
661
662 case 'mwcode_get_snippets':
663 $data = $this->api->getSnippets( $args['safe'] ?? true, $args['scope'] ?? null );
664 return [ 'success' => true, 'data' => $data ];
665
666 case 'mwcode_execute_snippet':
667 $data = $this->api->executeSnippet( $args['id'], $args['args'] ?? [] );
668 return [ 'success' => true, 'data' => $data ];
669
670 case 'mwcode_execute_snippet_by_name':
671 $data = $this->api->executeSnippetByName( $args['name'], $args['args'] ?? [] );
672 return [ 'success' => true, 'data' => $data ];
673
674 case 'mwcode_create_snippet':
675 $data = $this->api->createSnippet(
676 $args['name'],
677 $args['code'],
678 $args['scope'] ?? 'function',
679 $args['options'] ?? []
680 );
681 return [ 'success' => true, 'data' => $data ];
682
683 case 'mwcode_update_snippet':
684 $data = $this->api->updateSnippet( $args['id'], $args['params'] ?? [] );
685 return [ 'success' => true, 'data' => $data ];
686
687 case 'mwcode_delete_snippet':
688 $success = $this->api->deleteSnippet( $args['id'] );
689 return [ 'success' => true, 'data' => [ 'deleted' => $success ] ];
690
691 case 'mwcode_delete_snippet_by_name':
692 $success = $this->api->deleteSnippetByName( $args['name'] );
693 return [ 'success' => true, 'data' => [ 'deleted' => $success ] ];
694
695 case 'mwcode_activate_snippet':
696 $success = $this->api->activateSnippet( $args['id'] );
697 return [ 'success' => true, 'data' => [ 'activated' => $success ] ];
698
699 case 'mwcode_deactivate_snippet':
700 $success = $this->api->deactivateSnippet( $args['id'] );
701 return [ 'success' => true, 'data' => [ 'deactivated' => $success ] ];
702
703 case 'mwcode_get_snippets_by_scope':
704 $data = $this->api->getSnippetsByScope( $args['scope'], $args['filters'] ?? [] );
705 return [ 'success' => true, 'data' => $data ];
706
707 case 'mwcode_get_active_snippets':
708 $data = $this->api->getActiveSnippets( $args['scope'] ?? null );
709 return [ 'success' => true, 'data' => $data ];
710
711 case 'mwcode_snippet_exists':
712 $exists = $this->api->snippetExists( $args['id'] );
713 return [ 'success' => true, 'data' => [ 'exists' => $exists ] ];
714
715 case 'mwcode_snippet_exists_by_name':
716 $exists = $this->api->snippetExistsByName( $args['name'] );
717 return [ 'success' => true, 'data' => [ 'exists' => $exists ] ];
718
719 case 'mwcode_validate_snippet_code':
720 $validation = $this->api->validateSnippetCode( $args['code'], $args['target'] ?? 'php' );
721 return [ 'success' => true, 'data' => $validation ];
722 }
723 }
724 catch ( Exception $e ) {
725 return [ 'success' => false, 'error' => $e->getMessage() ];
726 }
727
728 return $result;
729 }
730 }