PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.4.2
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.4.2
0.5.7 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 All 33 releases
← All changes | classes/mcp.php +26 -208 0.5.30.4.2 View file →
@@ -2,20 +2,9 @@
2 2
3 3 class Meow_MWCODE_MCP {
4 4 private $core;
5 5 private $api;
6 - // Per-request memo for the opted-in Callable list, so listing tools and executing
7 - // one don't each reload every snippet from the database. Reset on any mutation.
8 - private $mcp_functions_cache = null;
9 -
10 - // Shared, model-facing explanation of what each scope means. Without this an agent
11 - // sees a bare enum and has to guess which scope to pick.
12 - const SCOPE_DESC = "Where the snippet lives and runs: 'function' = Callable, run on demand (via REST, AI Engine, MCP); 'persistent' = Global, always loaded on both the front-end and wp-admin; 'frontend' = loaded on the front-end only; 'backend' = loaded in wp-admin only; 'scheduled' = run automatically on a schedule (cron); 'content_php' = a PHP snippet output where its [code-engine id=...] shortcode/block is placed; 'content_js' = a JavaScript snippet emitted as a <script> tag via the same shortcode/block.";
13 -
14 - // Scope enums that appear across every management tool. Kept in one place so the
15 - // list can never drift between tools (all seven scopes the API actually accepts).
16 - const SCOPES = ['function', 'backend', 'frontend', 'scheduled', 'persistent', 'content_php', 'content_js'];
17 -
6 +
18 7 public function __construct( $core ) {
19 8 $this->core = $core;
20 9
21 10 // Initialize everything on 'init' to ensure options are loaded
@@ -24,125 +13,20 @@
24 13
25 14 public function init() {
26 15 global $mwcode, $mwai;
27 16 $this->api = $mwcode;
28 -
29 - // Nothing to do without AI Engine's MCP server.
30 - if ( !isset( $mwai ) ) {
31 - return;
17 +
18 + // Only register MCP if enabled AND AI Engine is available
19 + if ( $this->core->get_option( 'mcp_support', false ) && isset( $mwai ) ) {
20 + // Register MCP tools
21 + add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) );
22 +
23 + // Handle MCP tool execution
24 + add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 );
32 25 }
33 -
34 - // Two independent surfaces share AI Engine's MCP server, each behind its own
35 - // global master switch (and AI Engine's own MCP auth gate upstream):
36 - // - the management/internal API (the 'mcp_support' option)
37 - // - individual Callable functions, opted-in per snippet via 'functionMcp' and
38 - // only exposed when the 'mcp_functions' option is enabled
39 - // Either one alone is enough to justify hooking the filters.
40 - add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) );
41 - add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 );
42 26 }
43 -
27 +
44 28 public function register_tools( $tools ) {
45 - // Individual Callable functions that opted in to MCP, exposed as first-class tools.
46 - // Gated behind a global master switch (Settings > For Developers > MCP Functions)
47 - // in addition to each snippet's per-function opt-in.
48 - if ( $this->core->get_option( 'mcp_functions', false ) ) {
49 - $tools = $this->register_function_tools( $tools );
50 - }
51 -
52 - // Code Engine's management/internal API (Settings > For Developers > MCP Support).
53 - if ( $this->core->get_option( 'mcp_support', false ) ) {
54 - $tools = $this->register_management_tools( $tools );
55 - }
56 -
57 - return $tools;
58 - }
59 -
60 - /**
61 - * Map a Code Engine argument type to a JSON Schema type.
62 - * Returns null for 'mixed'/unknown so the schema leaves the type open.
63 - */
64 - private function mcp_type( $type ) {
65 - switch ( $type ) {
66 - case 'number': return 'number';
67 - case 'boolean': return 'boolean';
68 - case 'array': return 'array';
69 - case 'object': return 'object';
70 - case 'string': return 'string';
71 - default: return null;
72 - }
73 - }
74 -
75 - /**
76 - * Return the active Callable (function) snippets that opted in to MCP exposure.
77 - */
78 - private function get_mcp_functions() {
79 - if ( $this->mcp_functions_cache !== null ) {
80 - return $this->mcp_functions_cache;
81 - }
82 - global $mwcode;
83 - if ( !isset( $mwcode ) || !method_exists( $mwcode, 'getSnippets' ) ) {
84 - return ( $this->mcp_functions_cache = [] );
85 - }
86 - $functions = $mwcode->getSnippets( true, 'function' );
87 - if ( empty( $functions ) ) {
88 - return ( $this->mcp_functions_cache = [] );
89 - }
90 - return ( $this->mcp_functions_cache = array_values( array_filter( $functions, function ( $fn ) {
91 - return !empty( $fn['functionMcp'] ) && !empty( $fn['functionName'] );
92 - } ) ) );
93 - }
94 -
95 - /**
96 - * Register each opted-in Callable function as its own MCP tool, named after the
97 - * function, with an input schema derived from its declared arguments.
98 - */
99 - public function register_function_tools( $tools ) {
100 - foreach ( $this->get_mcp_functions() as $fn ) {
101 - $properties = [];
102 - $required = [];
103 -
104 - $argsDict = isset( $fn['functionArgsDict'] ) && is_array( $fn['functionArgsDict'] ) ? $fn['functionArgsDict'] : [];
105 - foreach ( $argsDict as $argName => $arg ) {
106 - $name = ltrim( $argName, '$' );
107 - if ( $name === '' ) {
108 - continue;
109 - }
110 - $prop = [];
111 - $type = $this->mcp_type( $arg['type'] ?? 'string' );
112 - if ( $type !== null ) {
113 - $prop['type'] = $type;
114 - }
115 - if ( !empty( $arg['desc'] ) ) {
116 - $prop['description'] = $arg['desc'];
117 - }
118 - $properties[ $name ] = $prop;
119 - if ( !empty( $arg['required'] ) ) {
120 - $required[] = $name;
121 - }
122 - }
123 -
124 - $schema = [
125 - 'type' => 'object',
126 - // Cast so an argument-less function still serializes as {} and not [].
127 - 'properties' => (object) $properties,
128 - ];
129 - if ( !empty( $required ) ) {
130 - $schema['required'] = $required;
131 - }
132 -
133 - $tools[] = [
134 - 'name' => $fn['functionName'],
135 - 'description' => !empty( $fn['description'] ) ? $fn['description'] : ( 'Code Engine function: ' . $fn['functionName'] ),
136 - 'category' => 'Code Engine (Functions)',
137 - 'inputSchema' => $schema,
138 - 'annotations' => [ 'openWorldHint' => true ],
139 - ];
140 - }
141 - return $tools;
142 - }
143 -
144 - public function register_management_tools( $tools ) {
145 29 // Get Snippet
146 30 $tools[] = [
147 31 'name' => 'mwcode_get_snippet',
148 32 'description' => 'Get a Code Engine snippet by its ID',
@@ -159,9 +43,9 @@
159 43 'description' => 'Optional filtering options',
160 44 'properties' => [
161 45 'php_ready_args' => [
162 46 'type' => 'boolean',
163 - 'description' => 'When true (default), function argument names are returned PHP-ready with a leading $ (e.g. "$id"). Set false to get plain names (e.g. "id").'
47 + 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
164 48 ]
165 49 ]
166 50 ]
167 51 ],
@@ -186,9 +70,9 @@
186 70 'description' => 'Optional filtering options',
187 71 'properties' => [
188 72 'php_ready_args' => [
189 73 'type' => 'boolean',
190 - 'description' => 'When true (default), function argument names are returned PHP-ready with a leading $ (e.g. "$id"). Set false to get plain names (e.g. "id").'
74 + 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
191 75 ]
192 76 ]
193 77 ]
194 78 ],
@@ -205,20 +89,20 @@
205 89 'type' => 'object',
206 90 'properties' => [
207 91 'safe' => [
208 92 'type' => 'boolean',
209 - 'description' => 'When true (default), skip function snippets whose function name is empty or invalid. Leave true unless you specifically need to inspect malformed snippets.',
93 + 'description' => 'Whether to filter out snippets with invalid names',
210 94 'default' => true
211 95 ],
212 96 'scope' => [
213 97 'type' => 'string',
214 - 'description' => 'Optional scope filter. ' . self::SCOPE_DESC,
215 - 'enum' => self::SCOPES
98 + 'description' => 'Optional scope filter',
99 + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
216 100 ]
217 101 ]
218 102 ]
219 103 ];
220 -
104 +
221 105 // Execute Snippet
222 106 $tools[] = [
223 107 'name' => 'mwcode_execute_snippet',
224 108 'description' => 'Execute a Code Engine snippet by its ID',
@@ -279,10 +163,10 @@
279 163 'description' => 'Code of the snippet'
280 164 ],
281 165 'scope' => [
282 166 'type' => 'string',
283 - 'description' => 'Scope of the snippet. ' . self::SCOPE_DESC . ' Defaults to "function".',
284 - 'enum' => self::SCOPES,
167 + 'description' => 'Scope of the snippet',
168 + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'],
285 169 'default' => 'function'
286 170 ],
287 171 'options' => [
288 172 'type' => 'object',
@@ -318,12 +202,8 @@
318 202 'type' => 'string',
319 203 'enum' => ['dynamic', 'static'],
320 204 'description' => 'Behavior for function snippets'
321 205 ],
322 - 'mcp' => [
323 - 'type' => 'boolean',
324 - '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.'
325 - ],
326 206 'tags' => [
327 207 'type' => 'array',
328 208 'description' => 'Array of tags',
329 209 'items' => [ 'type' => 'string' ]
@@ -379,10 +259,9 @@
379 259 'name' => [ 'type' => 'string' ],
380 260 'code' => [ 'type' => 'string' ],
381 261 'scope' => [
382 262 'type' => 'string',
383 - 'description' => self::SCOPE_DESC,
384 - 'enum' => self::SCOPES
263 + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
385 264 ],
386 265 'description' => [ 'type' => 'string' ],
387 266 'active' => [ 'type' => 'boolean' ],
388 267 'priority' => [ 'type' => 'integer' ],
@@ -410,12 +289,8 @@
410 289 'behavior' => [
411 290 'type' => 'string',
412 291 'enum' => ['dynamic', 'static']
413 292 ],
414 - 'mcp' => [
415 - 'type' => 'boolean',
416 - '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.'
417 - ],
418 293 'intervalHours' => [ 'type' => 'integer' ],
419 294 'intervalMinutes' => [ 'type' => 'integer' ]
420 295 ]
421 296 ]
@@ -501,10 +376,10 @@
501 376 'type' => 'object',
502 377 'properties' => [
503 378 'scope' => [
504 379 'type' => 'string',
505 - 'description' => 'The scope to filter by. ' . self::SCOPE_DESC,
506 - 'enum' => self::SCOPES
380 + 'description' => 'The scope to filter by',
381 + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
507 382 ],
508 383 'filters' => [
509 384 'type' => 'object',
510 385 'description' => 'Additional filters',
@@ -534,15 +409,15 @@
534 409 'type' => 'object',
535 410 'properties' => [
536 411 'scope' => [
537 412 'type' => 'string',
538 - 'description' => 'Optional scope filter. ' . self::SCOPE_DESC,
539 - 'enum' => self::SCOPES
413 + 'description' => 'Optional scope filter',
414 + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
540 415 ]
541 416 ]
542 417 ]
543 418 ];
544 -
419 +
545 420 // Snippet Exists
546 421 $tools[] = [
547 422 'name' => 'mwcode_snippet_exists',
548 423 'description' => 'Check if a Code Engine snippet exists by ID',
@@ -601,68 +476,14 @@
601 476
602 477 return $tools;
603 478 }
604 479
605 - /**
606 - * Execute a Callable function exposed via MCP. Returns $result unchanged when the
607 - * tool name does not match one of our opted-in functions, so the filter chain
608 - * continues to the management tools (and other plugins).
609 - */
610 - private function handle_function_execution( $result, $tool, $args ) {
611 - // Master switch: even an opted-in Callable is unreachable via MCP unless the
612 - // site has explicitly enabled function exposure. Returning $result unchanged
613 - // lets the filter chain fall through to the management tools and other plugins.
614 - if ( !$this->core->get_option( 'mcp_functions', false ) ) {
615 - return $result;
616 - }
617 -
618 - $match = null;
619 - foreach ( $this->get_mcp_functions() as $fn ) {
620 - if ( $fn['functionName'] === $tool ) {
621 - $match = $fn;
622 - break;
623 - }
624 - }
625 - if ( $match === null ) {
626 - return $result;
627 - }
628 -
629 - if ( !$this->api ) {
630 - return [ 'success' => false, 'error' => 'Code Engine API not initialized' ];
631 - }
632 -
633 - // getSnippets() rows expose the database id as 'id' (function metadata uses 'snippetId').
634 - $snippetId = $match['id'] ?? $match['snippetId'] ?? null;
635 -
636 - try {
637 - $output = $this->api->executeSnippet( $snippetId, is_array( $args ) ? $args : [] );
638 - return [ 'success' => true, 'data' => $output ];
639 - }
640 - // Snippet code is arbitrary PHP: a fatal surfaces as Error/TypeError/ParseError,
641 - // none of which are Exceptions. Catch Throwable so a bad snippet can never take
642 - // down the MCP request.
643 - catch ( \Throwable $e ) {
644 - return [ 'success' => false, 'error' => $e->getMessage() ];
645 - }
646 - }
647 -
648 480 public function handle_tool_execution( $result, $tool, $args, $id ) {
649 - // Individual Callable functions exposed via MCP take priority (named after the function).
650 - $handled = $this->handle_function_execution( $result, $tool, $args );
651 - if ( $handled !== $result ) {
652 - return $handled;
653 - }
654 -
655 - // Management/internal API tools are gated behind the master switch.
656 - if ( !$this->core->get_option( 'mcp_support', false ) ) {
657 - return $result;
658 - }
659 -
660 481 // Only handle our tools
661 482 if ( strpos( $tool, 'mwcode_' ) !== 0 ) {
662 483 return $result;
663 484 }
664 -
485 +
665 486 // Ensure API is initialized
666 487 if ( !$this->api ) {
667 488 return [ 'success' => false, 'error' => 'Code Engine API not initialized' ];
668 489 }
@@ -738,12 +559,9 @@
738 559 $validation = $this->api->validateSnippetCode( $args['code'], $args['target'] ?? 'php' );
739 560 return [ 'success' => true, 'data' => $validation ];
740 561 }
741 562 }
742 - // executeSnippet() runs arbitrary snippet PHP, whose fatals are Errors, not
743 - // Exceptions. Catch Throwable so a broken snippet returns a clean error rather
744 - // than crashing the MCP request.
745 - catch ( \Throwable $e ) {
563 + catch ( Exception $e ) {
746 564 return [ 'success' => false, 'error' => $e->getMessage() ];
747 565 }
748 566
749 567 return $result;