| @@ -13,116 +13,20 @@ | ||
| 13 | 13 | |
| 14 | 14 | public function init() { |
| 15 | 15 | global $mwcode, $mwai; |
| 16 | 16 | $this->api = $mwcode; |
| 17 | - | |
| 18 | - // Nothing to do without AI Engine's MCP server. | |
| 19 | - if ( !isset( $mwai ) ) { | |
| 20 | - 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 ); | |
| 21 | 25 | } |
| 22 | - | |
| 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) | |
| 26 | - // Either one alone is enough to justify hooking the filters. | |
| 27 | - add_filter( 'mwai_mcp_tools', array( $this, 'register_tools' ) ); | |
| 28 | - add_filter( 'mwai_mcp_callback', array( $this, 'handle_tool_execution' ), 10, 4 ); | |
| 29 | 26 | } |
| 30 | - | |
| 27 | + | |
| 31 | 28 | public function register_tools( $tools ) { |
| 32 | - // Individual Callable functions that opted in to MCP, exposed as first-class tools. | |
| 33 | - $tools = $this->register_function_tools( $tools ); | |
| 34 | - | |
| 35 | - // Code Engine's management/internal API (Settings > For Developers > MCP Support). | |
| 36 | - if ( $this->core->get_option( 'mcp_support', false ) ) { | |
| 37 | - $tools = $this->register_management_tools( $tools ); | |
| 38 | - } | |
| 39 | - | |
| 40 | - return $tools; | |
| 41 | - } | |
| 42 | - | |
| 43 | - /** | |
| 44 | - * Map a Code Engine argument type to a JSON Schema type. | |
| 45 | - * Returns null for 'mixed'/unknown so the schema leaves the type open. | |
| 46 | - */ | |
| 47 | - private function mcp_type( $type ) { | |
| 48 | - switch ( $type ) { | |
| 49 | - case 'number': return 'number'; | |
| 50 | - case 'boolean': return 'boolean'; | |
| 51 | - case 'array': return 'array'; | |
| 52 | - case 'object': return 'object'; | |
| 53 | - case 'string': return 'string'; | |
| 54 | - default: return null; | |
| 55 | - } | |
| 56 | - } | |
| 57 | - | |
| 58 | - /** | |
| 59 | - * Return the active Callable (function) snippets that opted in to MCP exposure. | |
| 60 | - */ | |
| 61 | - private function get_mcp_functions() { | |
| 62 | - global $mwcode; | |
| 63 | - if ( !isset( $mwcode ) || !method_exists( $mwcode, 'getSnippets' ) ) { | |
| 64 | - return []; | |
| 65 | - } | |
| 66 | - $functions = $mwcode->getSnippets( true, 'function' ); | |
| 67 | - if ( empty( $functions ) ) { | |
| 68 | - return []; | |
| 69 | - } | |
| 70 | - return array_values( array_filter( $functions, function ( $fn ) { | |
| 71 | - return !empty( $fn['functionMcp'] ) && !empty( $fn['functionName'] ); | |
| 72 | - } ) ); | |
| 73 | - } | |
| 74 | - | |
| 75 | - /** | |
| 76 | - * Register each opted-in Callable function as its own MCP tool, named after the | |
| 77 | - * function, with an input schema derived from its declared arguments. | |
| 78 | - */ | |
| 79 | - public function register_function_tools( $tools ) { | |
| 80 | - foreach ( $this->get_mcp_functions() as $fn ) { | |
| 81 | - $properties = []; | |
| 82 | - $required = []; | |
| 83 | - | |
| 84 | - $argsDict = isset( $fn['functionArgsDict'] ) && is_array( $fn['functionArgsDict'] ) ? $fn['functionArgsDict'] : []; | |
| 85 | - foreach ( $argsDict as $argName => $arg ) { | |
| 86 | - $name = ltrim( $argName, '$' ); | |
| 87 | - if ( $name === '' ) { | |
| 88 | - continue; | |
| 89 | - } | |
| 90 | - $prop = []; | |
| 91 | - $type = $this->mcp_type( $arg['type'] ?? 'string' ); | |
| 92 | - if ( $type !== null ) { | |
| 93 | - $prop['type'] = $type; | |
| 94 | - } | |
| 95 | - if ( !empty( $arg['desc'] ) ) { | |
| 96 | - $prop['description'] = $arg['desc']; | |
| 97 | - } | |
| 98 | - $properties[ $name ] = $prop; | |
| 99 | - if ( !empty( $arg['required'] ) ) { | |
| 100 | - $required[] = $name; | |
| 101 | - } | |
| 102 | - } | |
| 103 | - | |
| 104 | - $schema = [ | |
| 105 | - 'type' => 'object', | |
| 106 | - // Cast so an argument-less function still serializes as {} and not []. | |
| 107 | - 'properties' => (object) $properties, | |
| 108 | - ]; | |
| 109 | - if ( !empty( $required ) ) { | |
| 110 | - $schema['required'] = $required; | |
| 111 | - } | |
| 112 | - | |
| 113 | - $tools[] = [ | |
| 114 | - 'name' => $fn['functionName'], | |
| 115 | - 'description' => !empty( $fn['description'] ) ? $fn['description'] : ( 'Code Engine function: ' . $fn['functionName'] ), | |
| 116 | - 'category' => 'Code Engine (Functions)', | |
| 117 | - 'inputSchema' => $schema, | |
| 118 | - 'annotations' => [ 'openWorldHint' => true ], | |
| 119 | - ]; | |
| 120 | - } | |
| 121 | - return $tools; | |
| 122 | - } | |
| 123 | - | |
| 124 | - public function register_management_tools( $tools ) { | |
| 125 | 29 | // Get Snippet |
| 126 | 30 | $tools[] = [ |
| 127 | 31 | 'name' => 'mwcode_get_snippet', |
| 128 | 32 | 'description' => 'Get a Code Engine snippet by its ID', |
| @@ -298,12 +202,8 @@ | ||
| 298 | 202 | 'type' => 'string', |
| 299 | 203 | 'enum' => ['dynamic', 'static'], |
| 300 | 204 | 'description' => 'Behavior for function snippets' |
| 301 | 205 | ], |
| 302 | - 'mcp' => [ | |
| 303 | - 'type' => 'boolean', | |
| 304 | - '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.' | |
| 305 | - ], | |
| 306 | 206 | 'tags' => [ |
| 307 | 207 | 'type' => 'array', |
| 308 | 208 | 'description' => 'Array of tags', |
| 309 | 209 | 'items' => [ 'type' => 'string' ] |
| @@ -389,12 +289,8 @@ | ||
| 389 | 289 | 'behavior' => [ |
| 390 | 290 | 'type' => 'string', |
| 391 | 291 | 'enum' => ['dynamic', 'static'] |
| 392 | 292 | ], |
| 393 | - 'mcp' => [ | |
| 394 | - 'type' => 'boolean', | |
| 395 | - '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.' | |
| 396 | - ], | |
| 397 | 293 | 'intervalHours' => [ 'type' => 'integer' ], |
| 398 | 294 | 'intervalMinutes' => [ 'type' => 'integer' ] |
| 399 | 295 | ] |
| 400 | 296 | ] |
| @@ -580,58 +476,14 @@ | ||
| 580 | 476 | |
| 581 | 477 | return $tools; |
| 582 | 478 | } |
| 583 | 479 | |
| 584 | - /** | |
| 585 | - * Execute a Callable function exposed via MCP. Returns $result unchanged when the | |
| 586 | - * tool name does not match one of our opted-in functions, so the filter chain | |
| 587 | - * continues to the management tools (and other plugins). | |
| 588 | - */ | |
| 589 | - private function handle_function_execution( $result, $tool, $args ) { | |
| 590 | - $match = null; | |
| 591 | - foreach ( $this->get_mcp_functions() as $fn ) { | |
| 592 | - if ( $fn['functionName'] === $tool ) { | |
| 593 | - $match = $fn; | |
| 594 | - break; | |
| 595 | - } | |
| 596 | - } | |
| 597 | - if ( $match === null ) { | |
| 598 | - return $result; | |
| 599 | - } | |
| 600 | - | |
| 601 | - if ( !$this->api ) { | |
| 602 | - return [ 'success' => false, 'error' => 'Code Engine API not initialized' ]; | |
| 603 | - } | |
| 604 | - | |
| 605 | - // getSnippets() rows expose the database id as 'id' (function metadata uses 'snippetId'). | |
| 606 | - $snippetId = $match['id'] ?? $match['snippetId'] ?? null; | |
| 607 | - | |
| 608 | - try { | |
| 609 | - $output = $this->api->executeSnippet( $snippetId, is_array( $args ) ? $args : [] ); | |
| 610 | - return [ 'success' => true, 'data' => $output ]; | |
| 611 | - } | |
| 612 | - catch ( Exception $e ) { | |
| 613 | - return [ 'success' => false, 'error' => $e->getMessage() ]; | |
| 614 | - } | |
| 615 | - } | |
| 616 | - | |
| 617 | 480 | public function handle_tool_execution( $result, $tool, $args, $id ) { |
| 618 | - // Individual Callable functions exposed via MCP take priority (named after the function). | |
| 619 | - $handled = $this->handle_function_execution( $result, $tool, $args ); | |
| 620 | - if ( $handled !== $result ) { | |
| 621 | - return $handled; | |
| 622 | - } | |
| 623 | - | |
| 624 | - // Management/internal API tools are gated behind the master switch. | |
| 625 | - if ( !$this->core->get_option( 'mcp_support', false ) ) { | |
| 626 | - return $result; | |
| 627 | - } | |
| 628 | - | |
| 629 | 481 | // Only handle our tools |
| 630 | 482 | if ( strpos( $tool, 'mwcode_' ) !== 0 ) { |
| 631 | 483 | return $result; |
| 632 | 484 | } |
| 633 | - | |
| 485 | + | |
| 634 | 486 | // Ensure API is initialized |
| 635 | 487 | if ( !$this->api ) { |
| 636 | 488 | return [ 'success' => false, 'error' => 'Code Engine API not initialized' ]; |
| 637 | 489 | } |