| @@ -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 |
| @@ -30,13 +19,11 @@ | ||
| 30 | 19 | if ( !isset( $mwai ) ) { |
| 31 | 20 | return; |
| 32 | 21 | } |
| 33 | 22 | |
| 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 | |
| 23 | + // Two independent surfaces share AI Engine's MCP server: | |
| 24 | + // - the management/internal API (master switch: the 'mcp_support' option) | |
| 25 | + // - individual Callable functions opted-in per snippet (the 'functionMcp' flag) | |
| 39 | 26 | // Either one alone is enough to justify hooking the filters. |
| 40 | 27 | add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) ); |
| 41 | 28 | add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 ); |
| 42 | 29 | } |
| @@ -42,13 +29,9 @@ | ||
| 42 | 29 | } |
| 43 | 30 | |
| 44 | 31 | public function register_tools( $tools ) { |
| 45 | 32 | // 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 | - } | |
| 33 | + $tools = $this->register_function_tools( $tools ); | |
| 51 | 34 | |
| 52 | 35 | // Code Engine's management/internal API (Settings > For Developers > MCP Support). |
| 53 | 36 | if ( $this->core->get_option( 'mcp_support', false ) ) { |
| 54 | 37 | $tools = $this->register_management_tools( $tools ); |
| @@ -75,22 +58,19 @@ | ||
| 75 | 58 | /** |
| 76 | 59 | * Return the active Callable (function) snippets that opted in to MCP exposure. |
| 77 | 60 | */ |
| 78 | 61 | private function get_mcp_functions() { |
| 79 | - if ( $this->mcp_functions_cache !== null ) { | |
| 80 | - return $this->mcp_functions_cache; | |
| 81 | - } | |
| 82 | 62 | global $mwcode; |
| 83 | 63 | if ( !isset( $mwcode ) || !method_exists( $mwcode, 'getSnippets' ) ) { |
| 84 | - return ( $this->mcp_functions_cache = [] ); | |
| 64 | + return []; | |
| 85 | 65 | } |
| 86 | 66 | $functions = $mwcode->getSnippets( true, 'function' ); |
| 87 | 67 | if ( empty( $functions ) ) { |
| 88 | - return ( $this->mcp_functions_cache = [] ); | |
| 68 | + return []; | |
| 89 | 69 | } |
| 90 | - return ( $this->mcp_functions_cache = array_values( array_filter( $functions, function ( $fn ) { | |
| 70 | + return array_values( array_filter( $functions, function ( $fn ) { | |
| 91 | 71 | return !empty( $fn['functionMcp'] ) && !empty( $fn['functionName'] ); |
| 92 | - } ) ) ); | |
| 72 | + } ) ); | |
| 93 | 73 | } |
| 94 | 74 | |
| 95 | 75 | /** |
| 96 | 76 | * Register each opted-in Callable function as its own MCP tool, named after the |
| @@ -159,9 +139,9 @@ | ||
| 159 | 139 | 'description' => 'Optional filtering options', |
| 160 | 140 | 'properties' => [ |
| 161 | 141 | 'php_ready_args' => [ |
| 162 | 142 | '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").' | |
| 143 | + 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)' | |
| 164 | 144 | ] |
| 165 | 145 | ] |
| 166 | 146 | ] |
| 167 | 147 | ], |
| @@ -186,9 +166,9 @@ | ||
| 186 | 166 | 'description' => 'Optional filtering options', |
| 187 | 167 | 'properties' => [ |
| 188 | 168 | 'php_ready_args' => [ |
| 189 | 169 | '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").' | |
| 170 | + 'description' => 'If false, arguments will not be formatted for PHP (no $ before names)' | |
| 191 | 171 | ] |
| 192 | 172 | ] |
| 193 | 173 | ] |
| 194 | 174 | ], |
| @@ -205,20 +185,20 @@ | ||
| 205 | 185 | 'type' => 'object', |
| 206 | 186 | 'properties' => [ |
| 207 | 187 | 'safe' => [ |
| 208 | 188 | '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.', | |
| 189 | + 'description' => 'Whether to filter out snippets with invalid names', | |
| 210 | 190 | 'default' => true |
| 211 | 191 | ], |
| 212 | 192 | 'scope' => [ |
| 213 | 193 | 'type' => 'string', |
| 214 | - 'description' => 'Optional scope filter. ' . self::SCOPE_DESC, | |
| 215 | - 'enum' => self::SCOPES | |
| 194 | + 'description' => 'Optional scope filter', | |
| 195 | + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'] | |
| 216 | 196 | ] |
| 217 | 197 | ] |
| 218 | 198 | ] |
| 219 | 199 | ]; |
| 220 | - | |
| 200 | + | |
| 221 | 201 | // Execute Snippet |
| 222 | 202 | $tools[] = [ |
| 223 | 203 | 'name' => 'mwcode_execute_snippet', |
| 224 | 204 | 'description' => 'Execute a Code Engine snippet by its ID', |
| @@ -279,10 +259,10 @@ | ||
| 279 | 259 | 'description' => 'Code of the snippet' |
| 280 | 260 | ], |
| 281 | 261 | 'scope' => [ |
| 282 | 262 | 'type' => 'string', |
| 283 | - 'description' => 'Scope of the snippet. ' . self::SCOPE_DESC . ' Defaults to "function".', | |
| 284 | - 'enum' => self::SCOPES, | |
| 263 | + 'description' => 'Scope of the snippet', | |
| 264 | + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'], | |
| 285 | 265 | 'default' => 'function' |
| 286 | 266 | ], |
| 287 | 267 | 'options' => [ |
| 288 | 268 | 'type' => 'object', |
| @@ -379,10 +359,9 @@ | ||
| 379 | 359 | 'name' => [ 'type' => 'string' ], |
| 380 | 360 | 'code' => [ 'type' => 'string' ], |
| 381 | 361 | 'scope' => [ |
| 382 | 362 | 'type' => 'string', |
| 383 | - 'description' => self::SCOPE_DESC, | |
| 384 | - 'enum' => self::SCOPES | |
| 363 | + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'] | |
| 385 | 364 | ], |
| 386 | 365 | 'description' => [ 'type' => 'string' ], |
| 387 | 366 | 'active' => [ 'type' => 'boolean' ], |
| 388 | 367 | 'priority' => [ 'type' => 'integer' ], |
| @@ -501,10 +480,10 @@ | ||
| 501 | 480 | 'type' => 'object', |
| 502 | 481 | 'properties' => [ |
| 503 | 482 | 'scope' => [ |
| 504 | 483 | 'type' => 'string', |
| 505 | - 'description' => 'The scope to filter by. ' . self::SCOPE_DESC, | |
| 506 | - 'enum' => self::SCOPES | |
| 484 | + 'description' => 'The scope to filter by', | |
| 485 | + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'] | |
| 507 | 486 | ], |
| 508 | 487 | 'filters' => [ |
| 509 | 488 | 'type' => 'object', |
| 510 | 489 | 'description' => 'Additional filters', |
| @@ -534,15 +513,15 @@ | ||
| 534 | 513 | 'type' => 'object', |
| 535 | 514 | 'properties' => [ |
| 536 | 515 | 'scope' => [ |
| 537 | 516 | 'type' => 'string', |
| 538 | - 'description' => 'Optional scope filter. ' . self::SCOPE_DESC, | |
| 539 | - 'enum' => self::SCOPES | |
| 517 | + 'description' => 'Optional scope filter', | |
| 518 | + 'enum' => ['function', 'backend', 'frontend', 'scheduled', 'persistent'] | |
| 540 | 519 | ] |
| 541 | 520 | ] |
| 542 | 521 | ] |
| 543 | 522 | ]; |
| 544 | - | |
| 523 | + | |
| 545 | 524 | // Snippet Exists |
| 546 | 525 | $tools[] = [ |
| 547 | 526 | 'name' => 'mwcode_snippet_exists', |
| 548 | 527 | 'description' => 'Check if a Code Engine snippet exists by ID', |
| @@ -607,15 +586,8 @@ | ||
| 607 | 586 | * tool name does not match one of our opted-in functions, so the filter chain |
| 608 | 587 | * continues to the management tools (and other plugins). |
| 609 | 588 | */ |
| 610 | 589 | 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 | 590 | $match = null; |
| 619 | 591 | foreach ( $this->get_mcp_functions() as $fn ) { |
| 620 | 592 | if ( $fn['functionName'] === $tool ) { |
| 621 | 593 | $match = $fn; |
| @@ -636,12 +608,9 @@ | ||
| 636 | 608 | try { |
| 637 | 609 | $output = $this->api->executeSnippet( $snippetId, is_array( $args ) ? $args : [] ); |
| 638 | 610 | return [ 'success' => true, 'data' => $output ]; |
| 639 | 611 | } |
| 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 ) { | |
| 612 | + catch ( Exception $e ) { | |
| 644 | 613 | return [ 'success' => false, 'error' => $e->getMessage() ]; |
| 645 | 614 | } |
| 646 | 615 | } |
| 647 | 616 | |
| @@ -738,12 +707,9 @@ | ||
| 738 | 707 | $validation = $this->api->validateSnippetCode( $args['code'], $args['target'] ?? 'php' ); |
| 739 | 708 | return [ 'success' => true, 'data' => $validation ]; |
| 740 | 709 | } |
| 741 | 710 | } |
| 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 ) { | |
| 711 | + catch ( Exception $e ) { | |
| 746 | 712 | return [ 'success' => false, 'error' => $e->getMessage() ]; |
| 747 | 713 | } |
| 748 | 714 | |
| 749 | 715 | return $result; |