diff --git a/includes/functions.php b/includes/functions.php index 8110d95f2..e401920c8 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -351,6 +351,54 @@ function convertkit_get_abilities() { } +/** + * Helper method to get registered MCP resources. + * + * @since 3.4.2 + * + * @return array Resources. + */ +function convertkit_get_resources() { + + $resources = array(); + + /** + * Registers MCP resources for the Kit Plugin. + * + * @since 3.4.2 + * + * @param array $resources Resources. + */ + $resources = apply_filters( 'convertkit_resources', $resources ); + + return $resources; + +} + +/** + * Helper method to get registered MCP prompts. + * + * @since 3.4.2 + * + * @return array Prompts. + */ +function convertkit_get_prompts() { + + $prompts = array(); + + /** + * Registers MCP prompts for the Kit Plugin. + * + * @since 3.4.2 + * + * @param array $prompts Prompts. + */ + $prompts = apply_filters( 'convertkit_prompts', $prompts ); + + return $prompts; + +} + /** * Helper method to return the Plugin Settings Link * diff --git a/includes/mcp/class-convertkit-mcp-prompt.php b/includes/mcp/class-convertkit-mcp-prompt.php new file mode 100644 index 000000000..3b4c4de1d --- /dev/null +++ b/includes/mcp/class-convertkit-mcp-prompt.php @@ -0,0 +1,219 @@ +get_prompt_name(); + + } + + /** + * Returns the prompt's human-readable label / title. + * + * @since 3.4.2 + * + * @return string + */ + abstract public function get_label(); + + /** + * Returns the prompt's human-readable description. + * + * @since 3.4.2 + * + * @return string + */ + abstract public function get_description(); + + /** + * Returns the prompt's arguments, keyed by argument name. Each value is an + * array with a `description` and an optional `required` flag. MCP prompt + * arguments are always strings. + * + * @since 3.4.2 + * + * @return array + */ + protected function get_arguments() { + + return array(); + + } + + /** + * Returns the prompt's category. + * + * @since 3.4.2 + * + * @return string + */ + public function get_category() { + + return 'kit'; + + } + + /** + * Returns the prompt's input JSON Schema, built from get_arguments(). The MCP + * Adapter converts each property to a prompt argument. + * + * @since 3.4.2 + * + * @return array + */ + public function get_input_schema() { + + $properties = array(); + $required = array(); + + foreach ( $this->get_arguments() as $name => $argument ) { + $properties[ $name ] = array( + 'type' => 'string', + 'description' => isset( $argument['description'] ) ? $argument['description'] : '', + ); + + if ( ! empty( $argument['required'] ) ) { + $required[] = $name; + } + } + + $schema = array( + 'type' => 'object', + 'properties' => $properties, + ); + + if ( count( $required ) ) { + $schema['required'] = $required; + } + + return $schema; + + } + + /** + * Returns the prompt's output JSON Schema. Prompt content is returned as a + * text string, wrapped as a user message by the MCP Adapter. + * + * @since 3.4.2 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'object', + ); + + } + + /** + * Permission callback. + * + * @since 3.4.2 + * + * @param array $input Prompt input (unused). + * @return bool|WP_Error + */ + public function permission_callback( $input ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + + if ( ! current_user_can( 'manage_options' ) ) { + return new WP_Error( + 'convertkit_mcp_prompt_permission_callback', + __( 'You do not have permission for this operation.', 'convertkit' ) + ); + } + + return true; + + } + + /** + * Returns the arguments array passed to wp_register_ability(). + * + * @since 3.4.2 + * + * @return array + */ + public function get_ability_args() { + + return array( + 'label' => $this->get_label(), + 'description' => $this->get_description(), + 'category' => $this->get_category(), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'permission_callback' => array( $this, 'permission_callback' ), + 'execute_callback' => array( $this, 'execute_callback' ), + 'meta' => array( + 'mcp' => array( + 'type' => 'prompt', + 'public' => true, + ), + ), + ); + + } + + /** + * Execute callback for this prompt, returning its prompt text. + * + * Sub classes build their prompt and return $this->render( array( ...sections... ) ). + * + * @since 3.4.2 + * + * @param array $input Prompt arguments. + * @return array|WP_Error + */ + abstract public function execute_callback( $input ); + + /** + * Assembles the given text sections into a single prompt message, skipping + * empty sections. + * + * @since 3.4.2 + * + * @param array $sections Text sections. + * @return array Prompt result ( text ). + */ + protected function render( $sections ) { + + $sections = array_filter( array_map( 'trim', (array) $sections ), 'strlen' ); + + return array( + 'text' => implode( "\n\n", $sections ), + ); + + } + +} diff --git a/includes/mcp/class-convertkit-mcp-resource.php b/includes/mcp/class-convertkit-mcp-resource.php new file mode 100644 index 000000000..3c70c79e4 --- /dev/null +++ b/includes/mcp/class-convertkit-mcp-resource.php @@ -0,0 +1,179 @@ + array( 'object', 'null' ), + ); + + } + + /** + * Returns the resource's output JSON Schema. + * + * Resource content is returned as a single string, wrapped as text content + * by the MCP Adapter. + * + * @since 3.4.2 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'string', + ); + + } + + /** + * Permission callback. + * + * Sub classes can override this to implement their own permission callback. + * + * @since 3.4.2 + * + * @param array $input Resource input (unused). + * @return bool|WP_Error + */ + public function permission_callback( $input ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + + if ( ! current_user_can( 'manage_options' ) ) { + return new WP_Error( + 'convertkit_mcp_resource_permission_callback', + __( 'You do not have permission for this operation.', 'convertkit' ) + ); + } + + return true; + + } + + /** + * Returns the arguments array passed to wp_register_ability(). + * + * @since 3.4.2 + * + * @return array + */ + public function get_ability_args() { + + return array( + 'label' => $this->get_label(), + 'description' => $this->get_description(), + 'category' => $this->get_category(), + 'input_schema' => $this->get_input_schema(), + 'output_schema' => $this->get_output_schema(), + 'permission_callback' => array( $this, 'permission_callback' ), + 'execute_callback' => array( $this, 'execute_callback' ), + 'meta' => array( + 'mcp' => array( + 'uri' => $this->get_uri(), + 'mimeType' => $this->get_mime_type(), + ), + ), + ); + + } + + /** + * Execute callback for this resource, returning its content. + * + * @since 3.4.2 + * + * @param array $input Resource input. + * @return string|WP_Error + */ + abstract public function execute_callback( $input ); + +} diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index a4e7784b4..9d0f9ae23 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -83,6 +83,10 @@ public function __construct() { // Register abilities. add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); + // Register resources and prompts. + add_action( 'wp_abilities_api_init', array( $this, 'register_resources' ) ); + add_action( 'wp_abilities_api_init', array( $this, 'register_prompts' ) ); + // Register resource-list abilities (Forms, Tags, Landing Pages, Products). // These are owned by the Plugin (not by any single block or feature), // so they're added here rather than via a per-class register_abilities(). @@ -249,6 +253,64 @@ public function register_abilities() { } + /** + * Register MCP resources with the WordPress Abilities API. + * + * @since 3.4.2 + */ + public function register_resources() { + + // Get resources. + $resources = convertkit_get_resources(); + + // Bail if no resources are available. + if ( ! count( $resources ) ) { + return; + } + + // Iterate through resources, registering each as an ability. + foreach ( $resources as $resource ) { + + // Skip if this resource is not an instance of ConvertKit_MCP_Resource. + if ( ! ( $resource instanceof ConvertKit_MCP_Resource ) ) { + continue; + } + + // Register resource. + wp_register_ability( $resource->get_name(), $resource->get_ability_args() ); + } + + } + + /** + * Register MCP prompts with the WordPress Abilities API. + * + * @since 3.4.2 + */ + public function register_prompts() { + + // Get prompts. + $prompts = convertkit_get_prompts(); + + // Bail if no prompts are available. + if ( ! count( $prompts ) ) { + return; + } + + // Iterate through prompts, registering each as an ability. + foreach ( $prompts as $prompt ) { + + // Skip if this prompt is not an instance of ConvertKit_MCP_Prompt. + if ( ! ( $prompt instanceof ConvertKit_MCP_Prompt ) ) { + continue; + } + + // Register prompt. + wp_register_ability( $prompt->get_name(), $prompt->get_ability_args() ); + } + + } + /** * Register an MCP server that exposes Kit abilities as MCP tools. * @@ -275,6 +337,18 @@ public function register_mcp_server( $adapter ) { $ability_names[] = $ability->get_name(); } + // Build array of resource names. + $resource_names = array(); + foreach ( convertkit_get_resources() as $resource ) { + $resource_names[] = $resource->get_name(); + } + + // Build array of prompt names. + $prompt_names = array(); + foreach ( convertkit_get_prompts() as $prompt ) { + $prompt_names[] = $prompt->get_name(); + } + // Create the MCP server. $result = $adapter->create_server( self::SERVER_ID, @@ -287,8 +361,8 @@ public function register_mcp_server( $adapter ) { 'WP\\MCP\\Infrastructure\\ErrorHandling\\ErrorLogMcpErrorHandler', 'WP\\MCP\\Infrastructure\\Observability\\NullMcpObservabilityHandler', $ability_names, // Abilities (Tools). - array(), // Resources. - array() // Prompts. + $resource_names, // Resources. + $prompt_names // Prompts. ); // If an error occured when creating the server, log it. diff --git a/wp-convertkit.php b/wp-convertkit.php index 6841571be..ac23fcc03 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -108,6 +108,8 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/block-formatters/class-convertkit-block-formatter-form-link.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/block-formatters/class-convertkit-block-formatter-product-link.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/class-convertkit-mcp-ability.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/class-convertkit-mcp-resource.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/class-convertkit-mcp-prompt.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/class-convertkit-mcp.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/content/class-convertkit-mcp-ability-content.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/content/class-convertkit-mcp-ability-content-list.php';