PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / trunk
Code Engine – PHP Snippets, AI Functions & Automation for WordPress vtrunk
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
← All changes | classes/mcp.php +208 -26 0.3.4trunk View file →
@@ -2,9 +2,20 @@
2 2
3 3 class Meow_MWCODE_MCP {
4 4 private $core;
5 5 private $api;
6 -
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 +
7 18 public function __construct( $core ) {
8 19 $this->core = $core;
9 20
10 21 // Initialize everything on 'init' to ensure options are loaded
@@ -13,20 +24,125 @@
13 24
14 25 public function init() {
15 26 global $mwcode, $mwai;
16 27 $this->api = $mwcode;
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 );
28 +
29 + // Nothing to do without AI Engine's MCP server.
30 + if ( !isset( $mwai ) ) {
31 + return;
25 32 }
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 );
26 42 }
27 -
43 +
28 44 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 ) {
29 145 // Get Snippet
30 146 $tools[] = [
31 147 'name' => 'mwcode_get_snippet',
32 148 'description' => 'Get a Code Engine snippet by its ID',
@@ -43,9 +159,9 @@
43 159 'description' => 'Optional filtering options',
44 160 'properties' => [
45 161 'php_ready_args' => [
46 162 'type' => 'boolean',
47 - 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
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").'
48 164 ]
49 165 ]
50 166 ]
51 167 ],
@@ -70,9 +186,9 @@
70 186 'description' => 'Optional filtering options',
71 187 'properties' => [
72 188 'php_ready_args' => [
73 189 'type' => 'boolean',
74 - 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)'
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").'
75 191 ]
76 192 ]
77 193 ]
78 194 ],
@@ -89,20 +205,20 @@
89 205 'type' => 'object',
90 206 'properties' => [
91 207 'safe' => [
92 208 'type' => 'boolean',
93 - 'description' => 'Whether to filter out snippets with invalid names',
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.',
94 210 'default' => true
95 211 ],
96 212 'scope' => [
97 213 'type' => 'string',
98 - 'description' => 'Optional scope filter',
99 - 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
214 + 'description' => 'Optional scope filter. ' . self::SCOPE_DESC,
215 + 'enum' => self::SCOPES
100 216 ]
101 217 ]
102 218 ]
103 219 ];
104 -
220 +
105 221 // Execute Snippet
106 222 $tools[] = [
107 223 'name' => 'mwcode_execute_snippet',
108 224 'description' => 'Execute a Code Engine snippet by its ID',
@@ -163,10 +279,10 @@
163 279 'description' => 'Code of the snippet'
164 280 ],
165 281 'scope' => [
166 282 'type' => 'string',
167 - 'description' => 'Scope of the snippet',
168 - 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'],
283 + 'description' => 'Scope of the snippet. ' . self::SCOPE_DESC . ' Defaults to "function".',
284 + 'enum' => self::SCOPES,
169 285 'default' => 'function'
170 286 ],
171 287 'options' => [
172 288 'type' => 'object',
@@ -202,8 +318,12 @@
202 318 'type' => 'string',
203 319 'enum' => ['dynamic', 'static'],
204 320 'description' => 'Behavior for function snippets'
205 321 ],
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 + ],
206 326 'tags' => [
207 327 'type' => 'array',
208 328 'description' => 'Array of tags',
209 329 'items' => [ 'type' => 'string' ]
@@ -259,9 +379,10 @@
259 379 'name' => [ 'type' => 'string' ],
260 380 'code' => [ 'type' => 'string' ],
261 381 'scope' => [
262 382 'type' => 'string',
263 - 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
383 + 'description' => self::SCOPE_DESC,
384 + 'enum' => self::SCOPES
264 385 ],
265 386 'description' => [ 'type' => 'string' ],
266 387 'active' => [ 'type' => 'boolean' ],
267 388 'priority' => [ 'type' => 'integer' ],
@@ -289,8 +410,12 @@
289 410 'behavior' => [
290 411 'type' => 'string',
291 412 'enum' => ['dynamic', 'static']
292 413 ],
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 + ],
293 418 'intervalHours' => [ 'type' => 'integer' ],
294 419 'intervalMinutes' => [ 'type' => 'integer' ]
295 420 ]
296 421 ]
@@ -376,10 +501,10 @@
376 501 'type' => 'object',
377 502 'properties' => [
378 503 'scope' => [
379 504 'type' => 'string',
380 - 'description' => 'The scope to filter by',
381 - 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
505 + 'description' => 'The scope to filter by. ' . self::SCOPE_DESC,
506 + 'enum' => self::SCOPES
382 507 ],
383 508 'filters' => [
384 509 'type' => 'object',
385 510 'description' => 'Additional filters',
@@ -409,15 +534,15 @@
409 534 'type' => 'object',
410 535 'properties' => [
411 536 'scope' => [
412 537 'type' => 'string',
413 - 'description' => 'Optional scope filter',
414 - 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent']
538 + 'description' => 'Optional scope filter. ' . self::SCOPE_DESC,
539 + 'enum' => self::SCOPES
415 540 ]
416 541 ]
417 542 ]
418 543 ];
419 -
544 +
420 545 // Snippet Exists
421 546 $tools[] = [
422 547 'name' => 'mwcode_snippet_exists',
423 548 'description' => 'Check if a Code Engine snippet exists by ID',
@@ -476,14 +601,68 @@
476 601
477 602 return $tools;
478 603 }
479 604
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 +
480 648 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 +
481 660 // Only handle our tools
482 661 if ( strpos( $tool, 'mwcode_' ) !== 0 ) {
483 662 return $result;
484 663 }
485 -
664 +
486 665 // Ensure API is initialized
487 666 if ( !$this->api ) {
488 667 return [ 'success' => false, 'error' => 'Code Engine API not initialized' ];
489 668 }
@@ -559,9 +738,12 @@
559 738 $validation = $this->api->validateSnippetCode( $args['code'], $args['target'] ?? 'php' );
560 739 return [ 'success' => true, 'data' => $validation ];
561 740 }
562 741 }
563 - catch ( Exception $e ) {
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 ) {
564 746 return [ 'success' => false, 'error' => $e->getMessage() ];
565 747 }
566 748
567 749 return $result;