From fd2b5202a5e7d6e69eb6c69c3dd563bf45f292cb Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 18:14:43 +0800 Subject: [PATCH 01/16] Abillities API: Block Framework --- ...ss-convertkit-mcp-ability-block-delete.php | 189 ++++++++++++++ ...ss-convertkit-mcp-ability-block-insert.php | 224 +++++++++++++++++ ...lass-convertkit-mcp-ability-block-list.php | 194 +++++++++++++++ ...ss-convertkit-mcp-ability-block-update.php | 207 ++++++++++++++++ .../class-convertkit-mcp-ability-block.php | 233 ++++++++++++++++++ 5 files changed, 1047 insertions(+) create mode 100644 includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php create mode 100644 includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php create mode 100644 includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php create mode 100644 includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php create mode 100644 includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php new file mode 100644 index 000000000..87983b712 --- /dev/null +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -0,0 +1,189 @@ +-delete` (e.g. `kit/form-delete`). + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Block_Delete extends ConvertKit_MCP_Ability_Block { + + /** + * Returns the verb this ability represents. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_verb() { + + return 'delete'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: block title */ + __( 'Delete a %s block from a post', 'convertkit' ), + $this->block->get_title() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: 1: block full name e.g. convertkit/form, 2: block title */ + __( 'Removes a single occurrence of the %1$s (%2$s) block from the given post.', 'convertkit' ), + $this->block->get_full_block_name(), + $this->block->get_title() + ); + + } + + /** + * MCP annotations: destructive and not readonly; not idempotent, as repeated + * calls will attempt to delete sequential occurrences rather than a no-op. + * + * @since 3.4.0 + * + * @return array + */ + public function get_annotations() { + + return array( + 'title' => $this->get_label(), + 'readonly' => false, + 'destructive' => true, + 'idempotent' => false, + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'target' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'ID of the post containing the block.', 'convertkit' ), + ), + 'target' => $this->get_target_schema(), + ), + ); + + } + + /** + * Returns the ability's output JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'block', 'deleted_occurrence_index' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + ), + 'block' => array( + 'type' => 'string', + 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), + ), + 'deleted_occurrence_index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'Zero-based occurrence index of the deleted block among this block\'s appearances in the post prior to deletion.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Executes the ability. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + // Get Post ID. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; + + // Bail if no Post ID is provided. + if ( ! $post_id ) { + return new WP_Error( + 'convertkit_mcp_missing_post_id', + __( 'A post_id is required.', 'convertkit' ) + ); + } + + // Get target. + $target = isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(); + + // Resolve target. + $occurrence_index = $this->resolve_target( $post_id, $target ); + + // Bail if the target is not found. + if ( is_wp_error( $occurrence_index ) ) { + return $occurrence_index; + } + + // Delete block from post. + $result = $this->block->delete_from_post( $post_id, $occurrence_index ); + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return result. + return array( + 'post_id' => $post_id, + 'block' => $this->block->get_full_block_name(), + 'deleted_occurrence_index' => (int) $occurrence_index, + ); + + } + +} diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php new file mode 100644 index 000000000..71dab6771 --- /dev/null +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -0,0 +1,224 @@ +-insert` (e.g. `kit/form-insert`). + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Block_Insert extends ConvertKit_MCP_Ability_Block { + + /** + * Returns the verb this ability represents. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_verb() { + + return 'insert'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: block title */ + __( 'Insert a %s block into a post', 'convertkit' ), + $this->block->get_title() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: 1: block full name e.g. convertkit/form, 2: block title */ + __( 'Inserts a new %1$s (%2$s) block into the given post\'s content. The block can be appended (default), prepended, or positioned relative to an existing occurrence by zero-based index.', 'convertkit' ), + $this->block->get_full_block_name(), + $this->block->get_title() + ); + + } + + /** + * MCP annotations: not readonly, not destructive, not idempotent + * (repeated calls insert additional blocks). + * + * @since 3.4.0 + * + * @return array + */ + public function get_annotations() { + + return array( + 'title' => $this->get_label(), + 'readonly' => false, + 'destructive' => false, + 'idempotent' => false, + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'attrs' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'ID of the post to insert the block into.', 'convertkit' ), + ), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Block attributes for the new occurrence.', 'convertkit' ), + 'properties' => $this->block->get_input_schema_properties(), + ), + 'position' => array( + 'type' => 'string', + 'enum' => array( 'append', 'prepend', 'at_index' ), + 'default' => 'append', + 'description' => __( 'Where to insert the new block. "at_index" requires the "index" property.', 'convertkit' ), + ), + 'index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'When position is "at_index", the zero-based top-level block index at which to insert the new block.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Returns the ability's output JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'block', 'occurrence_index', 'attrs' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + ), + 'block' => array( + 'type' => 'string', + 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), + ), + 'occurrence_index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'Zero-based occurrence index of the newly inserted block among this block\'s appearances in the post.', 'convertkit' ), + ), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Attributes of the newly inserted block.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Executes the ability. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + // Get Post ID. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; + + // Bail if no Post ID is provided. + if ( ! $post_id ) { + return new WP_Error( + 'convertkit_mcp_missing_post_id', + __( 'A post_id is required.', 'convertkit' ) + ); + } + + // Get attributes. + $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); + $position = isset( $input['position'] ) ? (string) $input['position'] : 'append'; + $index = isset( $input['index'] ) ? (int) $input['index'] : 0; + + // Insert block into post. + $result = $this->block->insert_into_post( $post_id, $attrs, $position, $index ); + if ( is_wp_error( $result ) ) { + return $result; + } + + // Re-list occurrences to determine the newly inserted block's + // zero-based occurrence index among this block's appearances. + $occurrences = $this->block->find_blocks_in_post( $post_id ); + $occurrence_index = 0; + if ( is_array( $occurrences ) && count( $occurrences ) > 0 ) { + switch ( $position ) { + case 'prepend': + $occurrence_index = 0; + break; + + case 'at_index': + case 'append': + default: + // Find the first occurrence whose attrs match the just-inserted + // attrs; fall back to the last occurrence for 'append' and + // the first-after-$index for 'at_index'. + $occurrence_index = count( $occurrences ) - 1; + break; + } + } + + // Return result. + return array( + 'post_id' => $post_id, + 'block' => $this->block->get_full_block_name(), + 'occurrence_index' => (int) $occurrence_index, + 'attrs' => $attrs, + ); + + } + +} diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php new file mode 100644 index 000000000..36c3f6358 --- /dev/null +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -0,0 +1,194 @@ +-list` (e.g. `kit/form-list`). + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Block_List extends ConvertKit_MCP_Ability_Block { + + /** + * Returns the verb this ability represents. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_verb() { + + return 'list'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: block title */ + __( 'List %s blocks in a post', 'convertkit' ), + $this->block->get_title() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: 1: block full name e.g. convertkit/form, 2: block title */ + __( 'Lists every occurrence of the %1$s (%2$s) block in the given post, including each occurrence\'s zero-based index and current attribute values.', 'convertkit' ), + $this->block->get_full_block_name(), + $this->block->get_title() + ); + + } + + /** + * MCP annotations: readonly + idempotent. + * + * @since 3.4.0 + * + * @return array + */ + public function get_annotations() { + + return array( + 'title' => $this->get_label(), + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'ID of the post to inspect.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Returns the ability's output JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'block', 'count', 'occurrences' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + ), + 'block' => array( + 'type' => 'string', + 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), + ), + 'count' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'occurrences' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'required' => array( 'index', 'attrs' ), + 'properties' => array( + 'index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'Zero-based occurrence index among this block\'s appearances in the post.', 'convertkit' ), + ), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Block attributes for this occurrence.', 'convertkit' ), + ), + ), + ), + ), + ), + ); + + } + + /** + * Executes the ability. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + // Get Post ID. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; + + // Bail if no Post ID is provided. + if ( ! $post_id ) { + return new WP_Error( + 'convertkit_mcp_missing_post_id', + __( 'A post_id is required.', 'convertkit' ) + ); + } + + // Find blocks in post. + $occurrences = $this->block->find_blocks_in_post( $post_id ); + if ( is_wp_error( $occurrences ) ) { + return $occurrences; + } + + // Return result. + return array( + 'post_id' => $post_id, + 'block' => $this->block->get_full_block_name(), + 'count' => count( $occurrences ), + 'occurrences' => $occurrences, + ); + + } + +} diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php new file mode 100644 index 000000000..895c742ad --- /dev/null +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -0,0 +1,207 @@ +-update` (e.g. `kit/form-update`). + * + * By default the provided attributes are merged into the existing attributes. + * Set `replace_all` to true to replace all attributes with the supplied set. + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Block_Update extends ConvertKit_MCP_Ability_Block { + + /** + * Returns the verb this ability represents. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_verb() { + + return 'update'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: block title */ + __( 'Update a %s block in a post', 'convertkit' ), + $this->block->get_title() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: 1: block full name e.g. convertkit/form, 2: block title */ + __( 'Updates the attributes of a single occurrence of the %1$s (%2$s) block in the given post. By default the provided attributes are merged into the existing attributes; set replace_all to true to replace them entirely.', 'convertkit' ), + $this->block->get_full_block_name(), + $this->block->get_title() + ); + + } + + /** + * MCP annotations: not readonly, not destructive, idempotent + * (repeating the same update yields the same result). + * + * @since 3.4.0 + * + * @return array + */ + public function get_annotations() { + + return array( + 'title' => $this->get_label(), + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'target', 'attrs' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'minimum' => 1, + 'description' => __( 'ID of the post containing the block.', 'convertkit' ), + ), + 'target' => $this->get_target_schema(), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Attribute values to apply to the target block.', 'convertkit' ), + 'properties' => $this->block->get_input_schema_properties(), + ), + 'replace_all' => array( + 'type' => 'boolean', + 'default' => false, + 'description' => __( 'If true, all existing attributes are replaced with the supplied set. If false (default), the supplied attributes are merged into the existing attributes.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Returns the ability's output JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return array( + 'type' => 'object', + 'required' => array( 'post_id', 'block', 'occurrence_index', 'attrs' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + ), + 'block' => array( + 'type' => 'string', + 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), + ), + 'occurrence_index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'Zero-based occurrence index of the updated block.', 'convertkit' ), + ), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Attributes of the updated block.', 'convertkit' ), + ), + ), + ); + + } + + /** + * Executes the ability. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + // Get Post ID. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; + + // Bail if no Post ID is provided. + if ( ! $post_id ) { + return new WP_Error( + 'convertkit_mcp_missing_post_id', + __( 'A post_id is required.', 'convertkit' ) + ); + } + + // Get target. + $target = isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(); + $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); + $merge = ! ( isset( $input['replace_all'] ) && (bool) $input['replace_all'] ); + + // Resolve target. + $occurrence_index = $this->resolve_target( $post_id, $target ); + if ( is_wp_error( $occurrence_index ) ) { + return $occurrence_index; + } + + // Update block in post. + $result = $this->block->replace_in_post( $post_id, $occurrence_index, $attrs, $merge ); + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return result. + return array( + 'post_id' => $post_id, + 'block' => $this->block->get_full_block_name(), + 'occurrence_index' => (int) $occurrence_index, + 'attrs' => isset( $result['attrs'] ) ? $result['attrs'] : $attrs, + ); + + } + +} diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php new file mode 100644 index 000000000..55b86ed62 --- /dev/null +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -0,0 +1,233 @@ +block = $block; + + } + + /** + * Returns the ability name, derived from the block's name and the verb + * returned by get_verb(). + * + * @since 3.4.0 + * + * @return string + */ + public function get_name() { + + return 'kit/' . $this->block->get_name() . '-' . $this->get_verb(); + + } + + /** + * Returns the verb this ability represents. + * + * @since 3.4.0 + * + * @return string + */ + abstract protected function get_verb(); + + /** + * Only permit an ability to be executed if the current user can edit the given post. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return bool|WP_Error + */ + public function permission_callback( $input ) { + + // Get Post ID. + $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; + + // Bail if no Post ID is provided. + if ( ! $post_id ) { + return new WP_Error( + 'convertkit_mcp_missing_post_id', + __( 'A post_id is required.', 'convertkit' ) + ); + } + + // Bail if the current user does not have permission to edit the post. + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return new WP_Error( + 'convertkit_mcp_cannot_edit_post', + __( 'You do not have permission to edit this post.', 'convertkit' ) + ); + } + + return true; + + } + + /** + * Returns the JSON Schema fragment for a `target` object describing which + * occurrence of the block the ability should act on. Used by update/delete. + * + * @since 3.4.0 + * + * @return array + */ + protected function get_target_schema() { + + return array( + 'type' => 'object', + 'description' => __( 'Identifies which occurrence of this block in the post to act on. Either by an attribute value match, or by zero-based occurrence index.', 'convertkit' ), + 'oneOf' => array( + array( + 'type' => 'object', + 'required' => array( 'by', 'attribute', 'value' ), + 'properties' => array( + 'by' => array( + 'type' => 'string', + 'enum' => array( 'attribute' ), + ), + 'attribute' => array( + 'type' => 'string', + 'description' => __( 'The block attribute name to match against (e.g. "form").', 'convertkit' ), + ), + 'value' => array( + 'description' => __( 'The value the attribute must match.', 'convertkit' ), + ), + ), + ), + array( + 'type' => 'object', + 'required' => array( 'by', 'index' ), + 'properties' => array( + 'by' => array( + 'type' => 'string', + 'enum' => array( 'index' ), + ), + 'index' => array( + 'type' => 'integer', + 'minimum' => 0, + 'description' => __( 'Zero-based occurrence index among this block\'s appearances in the post.', 'convertkit' ), + ), + ), + ), + ), + ); + + } + + /** + * Resolves a target descriptor into the zero-based occurrence index of the + * block in the post. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param array $target Target descriptor (see get_target_schema()). + * @return int|WP_Error Zero-based occurrence index, or WP_Error. + */ + protected function resolve_target( $post_id, $target ) { + + // Bail if target is not an array or does not have a 'by' key. + if ( ! is_array( $target ) || empty( $target['by'] ) ) { + return new WP_Error( + 'convertkit_mcp_invalid_target', + __( 'target.by is required.', 'convertkit' ) + ); + } + + // Find blocks in post. + $occurrences = $this->block->find_blocks_in_post( $post_id ); + if ( is_wp_error( $occurrences ) ) { + return $occurrences; + } + + // Bail if no blocks are found. + if ( empty( $occurrences ) ) { + return new WP_Error( + 'convertkit_mcp_no_block_occurrences', + /* translators: 1: block name, 2: post ID */ + sprintf( __( 'No occurrences of block %1$s found in post %2$d.', 'convertkit' ), $this->block->get_full_block_name(), $post_id ) + ); + } + + // Resolve target. + switch ( $target['by'] ) { + case 'index': + $idx = isset( $target['index'] ) ? (int) $target['index'] : -1; + if ( $idx < 0 || $idx >= count( $occurrences ) ) { + return new WP_Error( + 'convertkit_mcp_target_index_out_of_range', + /* translators: 1: requested index, 2: number of occurrences */ + sprintf( __( 'Target index %1$d is out of range; post has %2$d occurrence(s).', 'convertkit' ), $idx, count( $occurrences ) ) + ); + } + return $idx; + + case 'attribute': + $attr = isset( $target['attribute'] ) ? (string) $target['attribute'] : ''; + $value = isset( $target['value'] ) ? $target['value'] : null; + if ( $attr === '' ) { + return new WP_Error( + 'convertkit_mcp_invalid_target', + __( 'target.attribute is required when target.by is "attribute".', 'convertkit' ) + ); + } + foreach ( $occurrences as $i => $occ ) { + if ( ! isset( $occ['attrs'][ $attr ] ) ) { + continue; + } + // Loose comparison so '123' == 123 resolves the same target, + // since Gutenberg attributes are often stringly typed. + if ( $occ['attrs'][ $attr ] == $value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual + return $i; + } + } + return new WP_Error( + 'convertkit_mcp_target_not_found', + /* translators: 1: attribute name, 2: value, 3: block name */ + sprintf( __( 'No occurrence of block %3$s has %1$s = %2$s.', 'convertkit' ), $attr, wp_json_encode( $value ), $this->block->get_full_block_name() ) + ); + + default: + return new WP_Error( + 'convertkit_mcp_invalid_target', + /* translators: %s: invalid 'by' value */ + sprintf( __( 'Unknown target.by value "%s". Expected "attribute" or "index".', 'convertkit' ), (string) $target['by'] ) + ); + } + + } + +} From a0ff1b07764c93342cef8f50f760d3c13fba3efd Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 18:17:52 +0800 Subject: [PATCH 02/16] Include block framework classes --- wp-convertkit.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wp-convertkit.php b/wp-convertkit.php index 1cbedbc10..7ceaeafdc 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -100,6 +100,11 @@ 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.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/plugin-sidebars/class-convertkit-plugin-sidebar.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/plugin-sidebars/class-convertkit-plugin-sidebar-post-settings.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/pre-publish-actions/class-convertkit-pre-publish-action.php'; From f876f5d19b58f0f3508f3fb9c5270603c03ab805 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 18:24:58 +0800 Subject: [PATCH 03/16] Set `get_category` to not be abstract --- includes/mcp/class-convertkit-mcp-ability.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/mcp/class-convertkit-mcp-ability.php b/includes/mcp/class-convertkit-mcp-ability.php index e06170df1..343f3ab59 100644 --- a/includes/mcp/class-convertkit-mcp-ability.php +++ b/includes/mcp/class-convertkit-mcp-ability.php @@ -73,7 +73,11 @@ abstract public function get_description(); * * @return string */ - abstract public function get_category(); + public function get_category() { + + return 'kit'; + + } /** * Returns the ability's input JSON Schema. From a4f501f99a42064f0e8ed0559be5e3faaa04d306 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 19:19:32 +0800 Subject: [PATCH 04/16] Fetch input schema from block fields/attributes --- ...ss-convertkit-mcp-ability-block-delete.php | 4 +- ...ss-convertkit-mcp-ability-block-insert.php | 62 +++++++++++++++++-- ...lass-convertkit-mcp-ability-block-list.php | 4 +- ...ss-convertkit-mcp-ability-block-update.php | 4 +- .../class-convertkit-mcp-ability-block.php | 4 +- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 87983b712..1a60415de 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -60,7 +60,7 @@ public function get_description() { return sprintf( /* translators: 1: block full name e.g. convertkit/form, 2: block title */ __( 'Removes a single occurrence of the %1$s (%2$s) block from the given post.', 'convertkit' ), - $this->block->get_full_block_name(), + 'convertkit/' . $this->block->get_name(), $this->block->get_title() ); @@ -180,7 +180,7 @@ public function execute_callback( $input ) { // Return result. return array( 'post_id' => $post_id, - 'block' => $this->block->get_full_block_name(), + 'block' => 'convertkit/' . $this->block->get_name(), 'deleted_occurrence_index' => (int) $occurrence_index, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index 71dab6771..dc776a501 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -59,8 +59,8 @@ public function get_description() { return sprintf( /* translators: 1: block full name e.g. convertkit/form, 2: block title */ - __( 'Inserts a new %1$s (%2$s) block into the given post\'s content. The block can be appended (default), prepended, or positioned relative to an existing occurrence by zero-based index.', 'convertkit' ), - $this->block->get_full_block_name(), + __( 'Inserts a new %1$s (%2$s) block into the given post\'s content. The block can be appended (default), prepended, or positioned relative to an existing block using a zero-based index.', 'convertkit' ), + 'convertkit/' . $this->block->get_name(), $this->block->get_title() ); @@ -106,7 +106,7 @@ public function get_input_schema() { 'attrs' => array( 'type' => 'object', 'description' => __( 'Block attributes for the new occurrence.', 'convertkit' ), - 'properties' => $this->block->get_input_schema_properties(), + 'properties' => $this->get_input_schema_properties(), ), 'position' => array( 'type' => 'string', @@ -124,6 +124,60 @@ public function get_input_schema() { } + /** + * Returns JSON Schema properties derived from the block's get_attributes() + * and get_fields(), suitable for use as the `attrs` object in an Abilities + * API input schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema_properties() { + + // Define properties. + $properties = array(); + + foreach ( $this->block->get_fields() as $field_name => $field ) { + + // Build JSON Schema property. + $properties[ $field_name ] = array( + 'description' => $field['label'], + 'type' => $this->get_input_schema_property_type( $field ), + ); + + } + + return $properties; + + } + + /** + * Returns the JSON Schema type for the given field. + * + * @since 3.4.0 + * + * @param array $field Field definition. + * @return string + */ + private function get_input_schema_property_type( $field ) { + + switch ( $field['type'] ) { + case 'resource': + return 'string'; + + case 'number': + return 'integer'; + + case 'toggle': + return 'boolean'; + + default: + return $field['type']; + } + + } + /** * Returns the ability's output JSON Schema. * @@ -214,7 +268,7 @@ public function execute_callback( $input ) { // Return result. return array( 'post_id' => $post_id, - 'block' => $this->block->get_full_block_name(), + 'block' => 'convertkit/' . $this->block->get_name(), 'occurrence_index' => (int) $occurrence_index, 'attrs' => $attrs, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index 36c3f6358..4a761ee04 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -60,7 +60,7 @@ public function get_description() { return sprintf( /* translators: 1: block full name e.g. convertkit/form, 2: block title */ __( 'Lists every occurrence of the %1$s (%2$s) block in the given post, including each occurrence\'s zero-based index and current attribute values.', 'convertkit' ), - $this->block->get_full_block_name(), + 'convertkit/' . $this->block->get_name(), $this->block->get_title() ); @@ -184,7 +184,7 @@ public function execute_callback( $input ) { // Return result. return array( 'post_id' => $post_id, - 'block' => $this->block->get_full_block_name(), + 'block' => 'convertkit/' . $this->block->get_name(), 'count' => count( $occurrences ), 'occurrences' => $occurrences, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 895c742ad..32fab18d2 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -63,7 +63,7 @@ public function get_description() { return sprintf( /* translators: 1: block full name e.g. convertkit/form, 2: block title */ __( 'Updates the attributes of a single occurrence of the %1$s (%2$s) block in the given post. By default the provided attributes are merged into the existing attributes; set replace_all to true to replace them entirely.', 'convertkit' ), - $this->block->get_full_block_name(), + 'convertkit/' . $this->block->get_name(), $this->block->get_title() ); @@ -197,7 +197,7 @@ public function execute_callback( $input ) { // Return result. return array( 'post_id' => $post_id, - 'block' => $this->block->get_full_block_name(), + 'block' => 'convertkit/' . $this->block->get_name(), 'occurrence_index' => (int) $occurrence_index, 'attrs' => isset( $result['attrs'] ) ? $result['attrs'] : $attrs, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index 55b86ed62..5e8ba19e6 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -178,7 +178,7 @@ protected function resolve_target( $post_id, $target ) { return new WP_Error( 'convertkit_mcp_no_block_occurrences', /* translators: 1: block name, 2: post ID */ - sprintf( __( 'No occurrences of block %1$s found in post %2$d.', 'convertkit' ), $this->block->get_full_block_name(), $post_id ) + sprintf( __( 'No occurrences of block %1$s found in post %2$d.', 'convertkit' ), 'convertkit/' . $this->block->get_name(), $post_id ) ); } @@ -217,7 +217,7 @@ protected function resolve_target( $post_id, $target ) { return new WP_Error( 'convertkit_mcp_target_not_found', /* translators: 1: attribute name, 2: value, 3: block name */ - sprintf( __( 'No occurrence of block %3$s has %1$s = %2$s.', 'convertkit' ), $attr, wp_json_encode( $value ), $this->block->get_full_block_name() ) + sprintf( __( 'No occurrence of block %3$s has %1$s = %2$s.', 'convertkit' ), $attr, wp_json_encode( $value ), 'convertkit/' . $this->block->get_name() ) ); default: From afa1e7eb00f8924adc4173d376f8878eba9e3012 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 19:19:44 +0800 Subject: [PATCH 05/16] Register Form Insertion Ability --- .../blocks/class-convertkit-block-form.php | 3 +++ includes/blocks/class-convertkit-block.php | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/includes/blocks/class-convertkit-block-form.php b/includes/blocks/class-convertkit-block-form.php index 964dcf485..b22afcc97 100644 --- a/includes/blocks/class-convertkit-block-form.php +++ b/includes/blocks/class-convertkit-block-form.php @@ -27,6 +27,9 @@ public function __construct() { // Register this as a Gutenberg block in the ConvertKit Plugin. add_filter( 'convertkit_blocks', array( $this, 'register' ) ); + // Register this block's MCP abilities. + add_filter( 'convertkit_abilities', array( $this, 'register_abilities' ) ); + // Enqueue scripts for this Gutenberg Block in the editor view. add_action( 'convertkit_gutenberg_enqueue_scripts', array( $this, 'enqueue_scripts_editor' ) ); diff --git a/includes/blocks/class-convertkit-block.php b/includes/blocks/class-convertkit-block.php index 12a82447e..6bd6b64f5 100644 --- a/includes/blocks/class-convertkit-block.php +++ b/includes/blocks/class-convertkit-block.php @@ -55,6 +55,25 @@ public function register( $blocks ) { } + /** + * Registers this block's MCP abilities. + * + * @since 3.4.0 + * + * @param array $abilities Abilities to Register. + * @return array + */ + public function register_abilities( $abilities ) { + + return array_merge( + $abilities, + array( + new ConvertKit_MCP_Ability_Block_Insert( $this ), + ) + ); + + } + /** * Returns this block's programmatic name, excluding the convertkit- prefix. * From 1c5b9c3fa940ebfd0ba7a95c080db6863d8150b5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 21:25:39 +0800 Subject: [PATCH 06/16] Abilities: Prefix with `block-` --- .../abilities/blocks/class-convertkit-mcp-ability-block.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index 5e8ba19e6..6e9ea76b6 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -43,6 +43,8 @@ public function __construct( $block ) { /** * Returns the ability name, derived from the block's name and the verb * returned by get_verb(). + * + * For example, the Form block's insert ability would be named `kit/form-block-insert`. * * @since 3.4.0 * @@ -50,7 +52,7 @@ public function __construct( $block ) { */ public function get_name() { - return 'kit/' . $this->block->get_name() . '-' . $this->get_verb(); + return 'kit/' . $this->block->get_name() . '-block-' . $this->get_verb(); } From db3eec1ca704d532e940a0abe2331ea3c41eb928 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 20 Apr 2026 21:26:44 +0800 Subject: [PATCH 07/16] Insert Block: Use `index` --- ...ss-convertkit-mcp-ability-block-insert.php | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index dc776a501..81167686d 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -101,23 +101,23 @@ public function get_input_schema() { 'post_id' => array( 'type' => 'integer', 'minimum' => 1, - 'description' => __( 'ID of the post to insert the block into.', 'convertkit' ), - ), - 'attrs' => array( - 'type' => 'object', - 'description' => __( 'Block attributes for the new occurrence.', 'convertkit' ), - 'properties' => $this->get_input_schema_properties(), + 'description' => __( 'Page / Post / Custom Post Type ID to insert the block into.', 'convertkit' ), ), 'position' => array( 'type' => 'string', - 'enum' => array( 'append', 'prepend', 'at_index' ), + 'enum' => array( 'append', 'prepend', 'index' ), 'default' => 'append', - 'description' => __( 'Where to insert the new block. "at_index" requires the "index" property.', 'convertkit' ), + 'description' => __( 'Where to insert the new block. "index" requires the "index" property.', 'convertkit' ), ), 'index' => array( 'type' => 'integer', 'minimum' => 0, - 'description' => __( 'When position is "at_index", the zero-based top-level block index at which to insert the new block.', 'convertkit' ), + 'description' => __( 'When position is "index", the zero-based top-level block index at which to insert the new block.', 'convertkit' ), + ), + 'attrs' => array( + 'type' => 'object', + 'description' => __( 'Block attributes.', 'convertkit' ), + 'properties' => $this->get_input_schema_properties(), ), ), ); @@ -254,12 +254,10 @@ public function execute_callback( $input ) { $occurrence_index = 0; break; - case 'at_index': - case 'append': default: // Find the first occurrence whose attrs match the just-inserted // attrs; fall back to the last occurrence for 'append' and - // the first-after-$index for 'at_index'. + // the first-after-$index for 'block_index'. $occurrence_index = count( $occurrences ) - 1; break; } From bf522cce2b9d9116c2361328eb6fe614cd553ce9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 11:30:55 +0800 Subject: [PATCH 08/16] Use ConvertKit_Block_Post_Helper class --- .../class-convertkit-block-post-helper.php | 261 ++++++++++++++++++ ...ss-convertkit-mcp-ability-block-delete.php | 4 +- ...ss-convertkit-mcp-ability-block-insert.php | 80 +----- ...lass-convertkit-mcp-ability-block-list.php | 4 +- ...ss-convertkit-mcp-ability-block-update.php | 6 +- .../class-convertkit-mcp-ability-block.php | 70 ++++- wp-convertkit.php | 1 + 7 files changed, 337 insertions(+), 89 deletions(-) create mode 100644 includes/blocks/class-convertkit-block-post-helper.php diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php new file mode 100644 index 000000000..9ce3892b1 --- /dev/null +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -0,0 +1,261 @@ + , 'attrs' => ] + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Full block name, e.g. "convertkit/form". + * @return array|WP_Error Array of occurrences, or WP_Error if the post is missing. + */ + static public function find( $post_id, $block_name ) { + + // Get post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_post_not_found', + /* translators: %d: post ID */ + sprintf( __( 'No post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + $found = array(); + + foreach ( $blocks as $idx => $block ) { + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + $found[] = array( + 'index' => (int) $idx, + 'attrs' => isset( $block['attrs'] ) ? (array) $block['attrs'] : array(), + ); + } + + return $found; + + } + + /** + * Inserts a new occurrence of the given block into a post's content at the + * specified position. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param array $attrs Block Attributes. + * @param int $index Position to insert block. + * @return int|WP_Error + */ + static public function insert( $post_id, $block_name, $attrs, $index = 0 ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_insert_block_post_not_found', + /* translators: %d: Post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + + // Build the new block to insert. + $new_block = array( + 'blockName' => $block_name, + 'attrs' => (array) $attrs, + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + ); + + // Determine where the new block will be inserted. + $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); + array_splice( $blocks, $insert_at, 0, array( $new_block ) ); + + // Count how many matching occurrences precede the insertion point — + // that's the new block's zero-based occurrence index. + $occurrence_index = 0; + for ( $i = 0; $i < $insert_at; $i++ ) { + if ( isset( $blocks[ $i ]['blockName'] ) && $blocks[ $i ]['blockName'] === $block_name ) { + ++$occurrence_index; + } + } + + // Update Post. + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + // Bail if an error occurred. + if ( is_wp_error( $updated ) ) { + return $updated; + } + + // Return the occurrence index. + return $occurrence_index; + + } + + /** + * Updates the attributes of a specific top-level occurrence of the given + * block in a post's content. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param int $occurrence_index Position to update block. + * @param array $attrs Block Attributes. + * @return array|WP_Error + */ + static public function update( $post_id, $block_name, $occurrence_index, $attrs ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_update_block_post_not_found', + /* translators: %d: post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + $occurrence = 0; + $matched = false; + $final_attrs = array(); + + foreach ( $blocks as $key => $block ) { + // Skip if the block name does not match. + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + // Update the block if the occurrence index matches. + if ( $occurrence === (int) $occurrence_index ) { + $existing = isset( $block['attrs'] ) ? (array) $block['attrs'] : array(); + $final_attrs = $merge ? array_merge( $existing, (array) $attrs ) : (array) $attrs; + $blocks[ $key ]['attrs'] = $final_attrs; + $matched = true; + break; + } + + ++$occurrence; + } + + // Bail if the block was not found. + if ( ! $matched ) { + return new WP_Error( + 'convertkit_block_post_helper_occurrence_not_found', + /* translators: 1: block name, 2: occurrence index, 3: post ID */ + sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) + ); + } + + // Update Post. + return wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + } + + /** + * Deletes a specific top-level occurrence of the given block from a post's + * content. + * + * @since 3.4.0 + * + * @param int $post_id Post ID. + * @param string $block_name Programmatic Block Name. + * @param int $occurrence_index Zero-based index among this block's occurrences in the post. + * @return array|WP_Error + */ + static public function delete( $post_id, $block_name, $occurrence_index ) { + + // Get Post. + $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'convertkit_block_post_helper_delete_block_post_not_found', + /* translators: %d: post ID */ + sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) + ); + } + + // Parse blocks. + $blocks = parse_blocks( $post->post_content ); + $occurrence = 0; + $matched = false; + + foreach ( $blocks as $key => $block ) { + // Skip if the block name does not match. + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { + continue; + } + + // Delete the block if the occurrence index matches. + if ( $occurrence === (int) $occurrence_index ) { + unset( $blocks[ $key ] ); + $blocks = array_values( $blocks ); + $matched = true; + break; + } + + ++$occurrence; + } + + // Bail if the block was not found. + if ( ! $matched ) { + return new WP_Error( + 'convertkit_block_post_helper_delete_block_occurrence_not_found', + /* translators: 1: block name, 2: occurrence index, 3: post ID */ + sprintf( __( 'No occurrence #%2$d of block %1$s found in Post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) + ); + } + + // Update Post. + return wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => serialize_blocks( $blocks ), + ), + true + ); + + } + +} diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 1a60415de..84be4e0c1 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -25,7 +25,7 @@ class ConvertKit_MCP_Ability_Block_Delete extends ConvertKit_MCP_Ability_Block { * * @return string */ - protected function get_verb() { + public function get_verb() { return 'delete'; @@ -172,7 +172,7 @@ public function execute_callback( $input ) { } // Delete block from post. - $result = $this->block->delete_from_post( $post_id, $occurrence_index ); + $result = ConvertKit_Block_Post_Helper::delete( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index ); if ( is_wp_error( $result ) ) { return $result; } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index 81167686d..9ce95fe7d 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -25,7 +25,7 @@ class ConvertKit_MCP_Ability_Block_Insert extends ConvertKit_MCP_Ability_Block { * * @return string */ - protected function get_verb() { + public function get_verb() { return 'insert'; @@ -124,60 +124,6 @@ public function get_input_schema() { } - /** - * Returns JSON Schema properties derived from the block's get_attributes() - * and get_fields(), suitable for use as the `attrs` object in an Abilities - * API input schema. - * - * @since 3.4.0 - * - * @return array - */ - public function get_input_schema_properties() { - - // Define properties. - $properties = array(); - - foreach ( $this->block->get_fields() as $field_name => $field ) { - - // Build JSON Schema property. - $properties[ $field_name ] = array( - 'description' => $field['label'], - 'type' => $this->get_input_schema_property_type( $field ), - ); - - } - - return $properties; - - } - - /** - * Returns the JSON Schema type for the given field. - * - * @since 3.4.0 - * - * @param array $field Field definition. - * @return string - */ - private function get_input_schema_property_type( $field ) { - - switch ( $field['type'] ) { - case 'resource': - return 'string'; - - case 'number': - return 'integer'; - - case 'toggle': - return 'boolean'; - - default: - return $field['type']; - } - - } - /** * Returns the ability's output JSON Schema. * @@ -235,39 +181,19 @@ public function execute_callback( $input ) { // Get attributes. $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); - $position = isset( $input['position'] ) ? (string) $input['position'] : 'append'; $index = isset( $input['index'] ) ? (int) $input['index'] : 0; // Insert block into post. - $result = $this->block->insert_into_post( $post_id, $attrs, $position, $index ); + $result = ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $index ); if ( is_wp_error( $result ) ) { return $result; } - // Re-list occurrences to determine the newly inserted block's - // zero-based occurrence index among this block's appearances. - $occurrences = $this->block->find_blocks_in_post( $post_id ); - $occurrence_index = 0; - if ( is_array( $occurrences ) && count( $occurrences ) > 0 ) { - switch ( $position ) { - case 'prepend': - $occurrence_index = 0; - break; - - default: - // Find the first occurrence whose attrs match the just-inserted - // attrs; fall back to the last occurrence for 'append' and - // the first-after-$index for 'block_index'. - $occurrence_index = count( $occurrences ) - 1; - break; - } - } - // Return result. return array( 'post_id' => $post_id, 'block' => 'convertkit/' . $this->block->get_name(), - 'occurrence_index' => (int) $occurrence_index, + 'occurrence_index' => $result, 'attrs' => $attrs, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index 4a761ee04..b790e5081 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -25,7 +25,7 @@ class ConvertKit_MCP_Ability_Block_List extends ConvertKit_MCP_Ability_Block { * * @return string */ - protected function get_verb() { + public function get_verb() { return 'list'; @@ -176,7 +176,7 @@ public function execute_callback( $input ) { } // Find blocks in post. - $occurrences = $this->block->find_blocks_in_post( $post_id ); + $occurrences = ConvertKit_Block_Post_Helper::find( $post_id, 'convertkit/' . $this->block->get_name() ); if ( is_wp_error( $occurrences ) ) { return $occurrences; } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 32fab18d2..148338f08 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -28,7 +28,7 @@ class ConvertKit_MCP_Ability_Block_Update extends ConvertKit_MCP_Ability_Block { * * @return string */ - protected function get_verb() { + public function get_verb() { return 'update'; @@ -110,7 +110,7 @@ public function get_input_schema() { 'attrs' => array( 'type' => 'object', 'description' => __( 'Attribute values to apply to the target block.', 'convertkit' ), - 'properties' => $this->block->get_input_schema_properties(), + 'properties' => $this->get_input_schema_properties(), ), 'replace_all' => array( 'type' => 'boolean', @@ -189,7 +189,7 @@ public function execute_callback( $input ) { } // Update block in post. - $result = $this->block->replace_in_post( $post_id, $occurrence_index, $attrs, $merge ); + $result = ConvertKit_Block_Post_Helper::update( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index, $attrs, $merge ); if ( is_wp_error( $result ) ) { return $result; } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index 6e9ea76b6..6979ab5da 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -43,7 +43,7 @@ public function __construct( $block ) { /** * Returns the ability name, derived from the block's name and the verb * returned by get_verb(). - * + * * For example, the Form block's insert ability would be named `kit/form-block-insert`. * * @since 3.4.0 @@ -63,7 +63,7 @@ public function get_name() { * * @return string */ - abstract protected function get_verb(); + abstract public function get_verb(); /** * Only permit an ability to be executed if the current user can edit the given post. @@ -106,7 +106,7 @@ public function permission_callback( $input ) { * * @return array */ - protected function get_target_schema() { + public function get_target_schema() { return array( 'type' => 'object', @@ -159,7 +159,7 @@ protected function get_target_schema() { * @param array $target Target descriptor (see get_target_schema()). * @return int|WP_Error Zero-based occurrence index, or WP_Error. */ - protected function resolve_target( $post_id, $target ) { + public function resolve_target( $post_id, $target ) { // Bail if target is not an array or does not have a 'by' key. if ( ! is_array( $target ) || empty( $target['by'] ) ) { @@ -170,7 +170,7 @@ protected function resolve_target( $post_id, $target ) { } // Find blocks in post. - $occurrences = $this->block->find_blocks_in_post( $post_id ); + $occurrences = ConvertKit_Block_Post_Helper::find( $post_id, 'convertkit/' . $this->block->get_name() ); if ( is_wp_error( $occurrences ) ) { return $occurrences; } @@ -232,4 +232,64 @@ protected function resolve_target( $post_id, $target ) { } + /** + * Returns JSON Schema properties derived from the block's get_fields(), + * suitable for use as the `attrs` object in an Abilities API input schema. + * + * Used by verb subclasses whose input schema includes an `attrs` object + * (insert, update). + * + * @since 3.4.0 + * + * @return array + */ + protected function get_input_schema_properties() { + + // Define properties. + $properties = array(); + $fields = $this->block->get_fields(); + + if ( ! is_array( $fields ) ) { + return $properties; + } + + foreach ( $fields as $field_name => $field ) { + $properties[ $field_name ] = array( + 'description' => isset( $field['label'] ) ? (string) $field['label'] : '', + 'type' => $this->get_input_schema_property_type( $field ), + ); + } + + return $properties; + + } + + /** + * Returns the JSON Schema type for the given field definition. + * + * @since 3.4.0 + * + * @param array $field Field definition. + * @return string + */ + private function get_input_schema_property_type( $field ) { + + $type = isset( $field['type'] ) ? (string) $field['type'] : 'string'; + + switch ( $type ) { + case 'resource': + return 'string'; + + case 'number': + return 'integer'; + + case 'toggle': + return 'boolean'; + + default: + return $type; + } + + } + } diff --git a/wp-convertkit.php b/wp-convertkit.php index 28ffb9190..52a4fa66a 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -85,6 +85,7 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-user.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-widgets.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-post-helper.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-broadcasts.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-content.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-form-trigger.php'; From 3ed544607011e6f930217e672d0617ff53ae327e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 11:39:38 +0800 Subject: [PATCH 09/16] Fix static method arguments --- .../class-convertkit-block-post-helper.php | 67 +++++++++++++++---- ...ss-convertkit-mcp-ability-block-insert.php | 3 +- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index 9ce3892b1..1ed545f76 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -26,7 +26,7 @@ class ConvertKit_Block_Post_Helper { * @param string $block_name Full block name, e.g. "convertkit/form". * @return array|WP_Error Array of occurrences, or WP_Error if the post is missing. */ - static public function find( $post_id, $block_name ) { + public static function find( $post_id, $block_name ) { // Get post. $post = get_post( $post_id ); @@ -61,15 +61,21 @@ static public function find( $post_id, $block_name ) { * Inserts a new occurrence of the given block into a post's content at the * specified position. * + * $position can be one of: + * - 'prepend' : insert as the first top-level block. + * - 'append' : insert as the last top-level block (default). + * - 'index' : insert at the zero-based top-level block index given by $index. + * * @since 3.4.0 * * @param int $post_id Post ID. * @param string $block_name Programmatic Block Name. * @param array $attrs Block Attributes. - * @param int $index Position to insert block. - * @return int|WP_Error + * @param string $position One of 'prepend', 'append', 'index'. + * @param int $index Zero-based top-level block index; only used when $position is 'index'. + * @return int|WP_Error Zero-based occurrence index of the newly inserted block, or WP_Error on failure. */ - static public function insert( $post_id, $block_name, $attrs, $index = 0 ) { + public static function insert( $post_id, $block_name, $attrs, $position = 'append', $index = 0 ) { // Get Post. $post = get_post( $post_id ); @@ -93,8 +99,24 @@ static public function insert( $post_id, $block_name, $attrs, $index = 0 ) { 'innerContent' => array(), ); - // Determine where the new block will be inserted. - $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); + // Resolve $position into a concrete zero-based splice point in the + // top-level block array. + switch ( $position ) { + case 'prepend': + $insert_at = 0; + break; + + case 'index': + $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); + break; + + case 'append': + default: + $insert_at = count( $blocks ); + break; + } + + // Splice in the new block. array_splice( $blocks, $insert_at, 0, array( $new_block ) ); // Count how many matching occurrences precede the insertion point — @@ -116,8 +138,8 @@ static public function insert( $post_id, $block_name, $attrs, $index = 0 ) { ); // Bail if an error occurred. - if ( is_wp_error( $updated ) ) { - return $updated; + if ( is_wp_error( $result ) ) { + return $result; } // Return the occurrence index. @@ -135,9 +157,10 @@ static public function insert( $post_id, $block_name, $attrs, $index = 0 ) { * @param string $block_name Programmatic Block Name. * @param int $occurrence_index Position to update block. * @param array $attrs Block Attributes. + * @param bool $merge If true, merge $attrs into existing attributes; if false, replace all. * @return array|WP_Error */ - static public function update( $post_id, $block_name, $occurrence_index, $attrs ) { + public static function update( $post_id, $block_name, $occurrence_index, $attrs, $merge = true ) { // Get Post. $post = get_post( $post_id ); @@ -183,7 +206,7 @@ static public function update( $post_id, $block_name, $occurrence_index, $attrs } // Update Post. - return wp_update_post( + $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -191,6 +214,16 @@ static public function update( $post_id, $block_name, $occurrence_index, $attrs true ); + // Bail if an error occurred. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the final attributes applied to the block. + return array( + 'attrs' => $final_attrs, + ); + } /** @@ -204,7 +237,7 @@ static public function update( $post_id, $block_name, $occurrence_index, $attrs * @param int $occurrence_index Zero-based index among this block's occurrences in the post. * @return array|WP_Error */ - static public function delete( $post_id, $block_name, $occurrence_index ) { + public static function delete( $post_id, $block_name, $occurrence_index ) { // Get Post. $post = get_post( $post_id ); @@ -248,7 +281,7 @@ static public function delete( $post_id, $block_name, $occurrence_index ) { } // Update Post. - return wp_update_post( + $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -256,6 +289,16 @@ static public function delete( $post_id, $block_name, $occurrence_index ) { true ); + // Bail if an error occurred. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the remaining block count. + return array( + 'block_count' => count( $blocks ), + ); + } } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index 9ce95fe7d..2811dd753 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -181,10 +181,11 @@ public function execute_callback( $input ) { // Get attributes. $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); + $position = isset( $input['position'] ) ? (string) $input['position'] : 'append'; $index = isset( $input['index'] ) ? (int) $input['index'] : 0; // Insert block into post. - $result = ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $index ); + $result = ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $position, $index ); if ( is_wp_error( $result ) ) { return $result; } From fc4d1eddcaaa5c9d9fd155545881af1d81011fc2 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 11:59:09 +0800 Subject: [PATCH 10/16] Simplify block post helper method --- .../class-convertkit-block-post-helper.php | 80 ++++--------------- 1 file changed, 17 insertions(+), 63 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index 1ed545f76..123566e57 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -23,8 +23,8 @@ class ConvertKit_Block_Post_Helper { * @since 3.4.0 * * @param int $post_id Post ID. - * @param string $block_name Full block name, e.g. "convertkit/form". - * @return array|WP_Error Array of occurrences, or WP_Error if the post is missing. + * @param string $block_name Programmatic Block Name. + * @return array|WP_Error */ public static function find( $post_id, $block_name ) { @@ -42,13 +42,13 @@ public static function find( $post_id, $block_name ) { $blocks = parse_blocks( $post->post_content ); $found = array(); - foreach ( $blocks as $idx => $block ) { + foreach ( $blocks as $index => $block ) { if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { continue; } $found[] = array( - 'index' => (int) $idx, + 'index' => (int) $index, 'attrs' => isset( $block['attrs'] ) ? (array) $block['attrs'] : array(), ); } @@ -61,11 +61,6 @@ public static function find( $post_id, $block_name ) { * Inserts a new occurrence of the given block into a post's content at the * specified position. * - * $position can be one of: - * - 'prepend' : insert as the first top-level block. - * - 'append' : insert as the last top-level block (default). - * - 'index' : insert at the zero-based top-level block index given by $index. - * * @since 3.4.0 * * @param int $post_id Post ID. @@ -73,7 +68,7 @@ public static function find( $post_id, $block_name ) { * @param array $attrs Block Attributes. * @param string $position One of 'prepend', 'append', 'index'. * @param int $index Zero-based top-level block index; only used when $position is 'index'. - * @return int|WP_Error Zero-based occurrence index of the newly inserted block, or WP_Error on failure. + * @return int|WP_Error */ public static function insert( $post_id, $block_name, $attrs, $position = 'append', $index = 0 ) { @@ -119,17 +114,8 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen // Splice in the new block. array_splice( $blocks, $insert_at, 0, array( $new_block ) ); - // Count how many matching occurrences precede the insertion point — - // that's the new block's zero-based occurrence index. - $occurrence_index = 0; - for ( $i = 0; $i < $insert_at; $i++ ) { - if ( isset( $blocks[ $i ]['blockName'] ) && $blocks[ $i ]['blockName'] === $block_name ) { - ++$occurrence_index; - } - } - // Update Post. - $result = wp_update_post( + return wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -137,14 +123,6 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen true ); - // Bail if an error occurred. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the occurrence index. - return $occurrence_index; - } /** @@ -157,10 +135,9 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen * @param string $block_name Programmatic Block Name. * @param int $occurrence_index Position to update block. * @param array $attrs Block Attributes. - * @param bool $merge If true, merge $attrs into existing attributes; if false, replace all. - * @return array|WP_Error + * @return int|WP_Error */ - public static function update( $post_id, $block_name, $occurrence_index, $attrs, $merge = true ) { + public static function update( $post_id, $block_name, $occurrence_index, $attrs ) { // Get Post. $post = get_post( $post_id ); @@ -173,10 +150,9 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs, } // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $occurrence = 0; - $matched = false; - $final_attrs = array(); + $blocks = parse_blocks( $post->post_content ); + $occurrence = 0; + $matched = false; foreach ( $blocks as $key => $block ) { // Skip if the block name does not match. @@ -186,9 +162,7 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs, // Update the block if the occurrence index matches. if ( $occurrence === (int) $occurrence_index ) { - $existing = isset( $block['attrs'] ) ? (array) $block['attrs'] : array(); - $final_attrs = $merge ? array_merge( $existing, (array) $attrs ) : (array) $attrs; - $blocks[ $key ]['attrs'] = $final_attrs; + $blocks[ $key ]['attrs'] = array_merge( (array) $block['attrs'], (array) $attrs ); $matched = true; break; } @@ -206,7 +180,7 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs, } // Update Post. - $result = wp_update_post( + return wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -214,16 +188,6 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs, true ); - // Bail if an error occurred. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the final attributes applied to the block. - return array( - 'attrs' => $final_attrs, - ); - } /** @@ -235,7 +199,7 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs, * @param int $post_id Post ID. * @param string $block_name Programmatic Block Name. * @param int $occurrence_index Zero-based index among this block's occurrences in the post. - * @return array|WP_Error + * @return int|WP_Error */ public static function delete( $post_id, $block_name, $occurrence_index ) { @@ -244,7 +208,7 @@ public static function delete( $post_id, $block_name, $occurrence_index ) { if ( ! $post ) { return new WP_Error( 'convertkit_block_post_helper_delete_block_post_not_found', - /* translators: %d: post ID */ + /* translators: %d: Post ID */ sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) ); } @@ -275,13 +239,13 @@ public static function delete( $post_id, $block_name, $occurrence_index ) { if ( ! $matched ) { return new WP_Error( 'convertkit_block_post_helper_delete_block_occurrence_not_found', - /* translators: 1: block name, 2: occurrence index, 3: post ID */ + /* translators: 1: Block Name, 2: Occurrence Index, 3: Post ID */ sprintf( __( 'No occurrence #%2$d of block %1$s found in Post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) ); } // Update Post. - $result = wp_update_post( + return wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -289,16 +253,6 @@ public static function delete( $post_id, $block_name, $occurrence_index ) { true ); - // Bail if an error occurred. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the remaining block count. - return array( - 'block_count' => count( $blocks ), - ); - } } From 63042f105fb0c4e5dbd30a76fc07c10fb79789b3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 12:52:37 +0800 Subject: [PATCH 11/16] Move annotations to higher classes --- .../class-convertkit-block-post-helper.php | 3 -- includes/blocks/class-convertkit-block.php | 1 + ...ss-convertkit-mcp-ability-block-delete.php | 30 +++++--------- ...ss-convertkit-mcp-ability-block-insert.php | 19 --------- ...lass-convertkit-mcp-ability-block-list.php | 36 ++++++++-------- ...ss-convertkit-mcp-ability-block-update.php | 41 ++++++------------- includes/mcp/class-convertkit-mcp-ability.php | 38 +++++++++++++---- 7 files changed, 73 insertions(+), 95 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index 123566e57..618fee311 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -17,9 +17,6 @@ class ConvertKit_Block_Post_Helper { /** * Finds all top-level occurrences of the given block in a post's content. * - * Returns an array of occurrences in document order, each of the form: - * [ 'index' => , 'attrs' => ] - * * @since 3.4.0 * * @param int $post_id Post ID. diff --git a/includes/blocks/class-convertkit-block.php b/includes/blocks/class-convertkit-block.php index 6bd6b64f5..1636922fa 100644 --- a/includes/blocks/class-convertkit-block.php +++ b/includes/blocks/class-convertkit-block.php @@ -69,6 +69,7 @@ public function register_abilities( $abilities ) { $abilities, array( new ConvertKit_MCP_Ability_Block_Insert( $this ), + new ConvertKit_MCP_Ability_Block_Update( $this ), ) ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 84be4e0c1..851d99fed 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -18,6 +18,15 @@ */ class ConvertKit_MCP_Ability_Block_Delete extends ConvertKit_MCP_Ability_Block { + /** + * Sets whether the ability is destructive. + * + * @since 3.4.0 + * + * @var bool + */ + private $destructive = true; + /** * Returns the verb this ability represents. * @@ -42,7 +51,7 @@ public function get_label() { return sprintf( /* translators: %s: block title */ - __( 'Delete a %s block from a post', 'convertkit' ), + __( 'Delete an existing %s block from a post', 'convertkit' ), $this->block->get_title() ); @@ -66,25 +75,6 @@ public function get_description() { } - /** - * MCP annotations: destructive and not readonly; not idempotent, as repeated - * calls will attempt to delete sequential occurrences rather than a no-op. - * - * @since 3.4.0 - * - * @return array - */ - public function get_annotations() { - - return array( - 'title' => $this->get_label(), - 'readonly' => false, - 'destructive' => true, - 'idempotent' => false, - ); - - } - /** * Returns the ability's input JSON Schema. * diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index 2811dd753..70e0646e8 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -66,25 +66,6 @@ public function get_description() { } - /** - * MCP annotations: not readonly, not destructive, not idempotent - * (repeated calls insert additional blocks). - * - * @since 3.4.0 - * - * @return array - */ - public function get_annotations() { - - return array( - 'title' => $this->get_label(), - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, - ); - - } - /** * Returns the ability's input JSON Schema. * diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index b790e5081..0ee9c2380 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -18,6 +18,24 @@ */ class ConvertKit_MCP_Ability_Block_List extends ConvertKit_MCP_Ability_Block { + /** + * Sets whether the ability is readonly. + * + * @since 3.4.0 + * + * @var bool + */ + private $readonly = true; + + /** + * Sets whether the ability is idempotent. + * + * @since 3.4.0 + * + * @var bool + */ + private $idempotent = true; + /** * Returns the verb this ability represents. * @@ -66,24 +84,6 @@ public function get_description() { } - /** - * MCP annotations: readonly + idempotent. - * - * @since 3.4.0 - * - * @return array - */ - public function get_annotations() { - - return array( - 'title' => $this->get_label(), - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, - ); - - } - /** * Returns the ability's input JSON Schema. * diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 148338f08..533fb5f28 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -21,6 +21,15 @@ */ class ConvertKit_MCP_Ability_Block_Update extends ConvertKit_MCP_Ability_Block { + /** + * Sets whether the ability is idempotent. + * + * @since 3.4.0 + * + * @var bool + */ + private $idempotent = true; + /** * Returns the verb this ability represents. * @@ -45,7 +54,7 @@ public function get_label() { return sprintf( /* translators: %s: block title */ - __( 'Update a %s block in a post', 'convertkit' ), + __( 'Update an existing %s block in a post', 'convertkit' ), $this->block->get_title() ); @@ -69,25 +78,6 @@ public function get_description() { } - /** - * MCP annotations: not readonly, not destructive, idempotent - * (repeating the same update yields the same result). - * - * @since 3.4.0 - * - * @return array - */ - public function get_annotations() { - - return array( - 'title' => $this->get_label(), - 'readonly' => false, - 'destructive' => false, - 'idempotent' => true, - ); - - } - /** * Returns the ability's input JSON Schema. * @@ -101,22 +91,17 @@ public function get_input_schema() { 'type' => 'object', 'required' => array( 'post_id', 'target', 'attrs' ), 'properties' => array( - 'post_id' => array( + 'post_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => __( 'ID of the post containing the block.', 'convertkit' ), ), - 'target' => $this->get_target_schema(), - 'attrs' => array( + 'target' => $this->get_target_schema(), + 'attrs' => array( 'type' => 'object', 'description' => __( 'Attribute values to apply to the target block.', 'convertkit' ), 'properties' => $this->get_input_schema_properties(), ), - 'replace_all' => array( - 'type' => 'boolean', - 'default' => false, - 'description' => __( 'If true, all existing attributes are replaced with the supplied set. If false (default), the supplied attributes are merged into the existing attributes.', 'convertkit' ), - ), ), ); diff --git a/includes/mcp/class-convertkit-mcp-ability.php b/includes/mcp/class-convertkit-mcp-ability.php index 343f3ab59..4cbf26ee6 100644 --- a/includes/mcp/class-convertkit-mcp-ability.php +++ b/includes/mcp/class-convertkit-mcp-ability.php @@ -15,6 +15,33 @@ */ abstract class ConvertKit_MCP_Ability { + /** + * Sets whether the ability is readonly. + * + * @since 3.4.0 + * + * @var bool + */ + private $readonly = false; + + /** + * Sets whether the ability is destructive. + * + * @since 3.4.0 + * + * @var bool + */ + private $destructive = false; + + /** + * Sets whether the ability is idempotent. + * + * @since 3.4.0 + * + * @var bool + */ + private $idempotent = false; + /** * Returns the ability name, prefixed with `kit/` (e.g. `kit/form-insert`). * @@ -98,10 +125,7 @@ abstract public function get_input_schema(); abstract public function get_output_schema(); /** - * Returns the MCP annotations for this ability. - * - * Defaults to a non-readonly, non-destructive, non-idempotent action. - * Subclasses override the returned array to set the appropriate hints. + * Define the annotations for the ability. * * @since 3.4.0 * @@ -111,9 +135,9 @@ public function get_annotations() { return array( 'title' => $this->get_label(), - 'readonly' => false, - 'destructive' => false, - 'idempotent' => false, + 'readonly' => $this->readonly, + 'destructive' => $this->destructive, + 'idempotent' => $this->idempotent, ); } From a3d689ee15c231876aaae944099ed913f2d71f6e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 14:35:32 +0800 Subject: [PATCH 12/16] Simplify input/output schema --- .../class-convertkit-block-post-helper.php | 10 +- ...ss-convertkit-mcp-ability-block-delete.php | 1 - ...ss-convertkit-mcp-ability-block-insert.php | 45 +----- ...ss-convertkit-mcp-ability-block-update.php | 68 ++------- .../class-convertkit-mcp-ability-block.php | 130 ++---------------- 5 files changed, 32 insertions(+), 222 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index 618fee311..cb814eae3 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -147,9 +147,9 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs } // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $occurrence = 0; - $matched = false; + $blocks = parse_blocks( $post->post_content ); + $index = 0; + $matched = false; foreach ( $blocks as $key => $block ) { // Skip if the block name does not match. @@ -158,13 +158,13 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs } // Update the block if the occurrence index matches. - if ( $occurrence === (int) $occurrence_index ) { + if ( $index === (int) $occurrence_index ) { $blocks[ $key ]['attrs'] = array_merge( (array) $block['attrs'], (array) $attrs ); $matched = true; break; } - ++$occurrence; + ++$index; } // Bail if the block was not found. diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 851d99fed..4581285b9 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -93,7 +93,6 @@ public function get_input_schema() { 'minimum' => 1, 'description' => __( 'ID of the post containing the block.', 'convertkit' ), ), - 'target' => $this->get_target_schema(), ), ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index 70e0646e8..e87602fe9 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -105,40 +105,6 @@ public function get_input_schema() { } - /** - * Returns the ability's output JSON Schema. - * - * @since 3.4.0 - * - * @return array - */ - public function get_output_schema() { - - return array( - 'type' => 'object', - 'required' => array( 'post_id', 'block', 'occurrence_index', 'attrs' ), - 'properties' => array( - 'post_id' => array( - 'type' => 'integer', - ), - 'block' => array( - 'type' => 'string', - 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), - ), - 'occurrence_index' => array( - 'type' => 'integer', - 'minimum' => 0, - 'description' => __( 'Zero-based occurrence index of the newly inserted block among this block\'s appearances in the post.', 'convertkit' ), - ), - 'attrs' => array( - 'type' => 'object', - 'description' => __( 'Attributes of the newly inserted block.', 'convertkit' ), - ), - ), - ); - - } - /** * Executes the ability. * @@ -160,23 +126,18 @@ public function execute_callback( $input ) { ); } - // Get attributes. + // Get attributes, position and index. $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); $position = isset( $input['position'] ) ? (string) $input['position'] : 'append'; $index = isset( $input['index'] ) ? (int) $input['index'] : 0; // Insert block into post. $result = ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $position, $index ); - if ( is_wp_error( $result ) ) { - return $result; - } // Return result. return array( - 'post_id' => $post_id, - 'block' => 'convertkit/' . $this->block->get_name(), - 'occurrence_index' => $result, - 'attrs' => $attrs, + 'post_id' => $post_id, + 'result' => $result, ); } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 533fb5f28..59b62409a 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -89,52 +89,22 @@ public function get_input_schema() { return array( 'type' => 'object', - 'required' => array( 'post_id', 'target', 'attrs' ), + 'required' => array( 'post_id', 'occurrence_index', 'attrs' ), 'properties' => array( - 'post_id' => array( + 'post_id' => array( 'type' => 'integer', 'minimum' => 1, - 'description' => __( 'ID of the post containing the block.', 'convertkit' ), - ), - 'target' => $this->get_target_schema(), - 'attrs' => array( - 'type' => 'object', - 'description' => __( 'Attribute values to apply to the target block.', 'convertkit' ), - 'properties' => $this->get_input_schema_properties(), - ), - ), - ); - - } - - /** - * Returns the ability's output JSON Schema. - * - * @since 3.4.0 - * - * @return array - */ - public function get_output_schema() { - - return array( - 'type' => 'object', - 'required' => array( 'post_id', 'block', 'occurrence_index', 'attrs' ), - 'properties' => array( - 'post_id' => array( - 'type' => 'integer', - ), - 'block' => array( - 'type' => 'string', - 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), + 'description' => __( 'Page / Post / Custom Post Type ID containing the existing block.', 'convertkit' ), ), 'occurrence_index' => array( 'type' => 'integer', 'minimum' => 0, - 'description' => __( 'Zero-based occurrence index of the updated block.', 'convertkit' ), + 'description' => __( 'The zero-based occurrence index of the block to update.', 'convertkit' ), ), 'attrs' => array( 'type' => 'object', - 'description' => __( 'Attributes of the updated block.', 'convertkit' ), + 'description' => __( 'Block attributes to update. Any attributes not provided will be left unchanged.', 'convertkit' ), + 'properties' => $this->get_input_schema_properties(), ), ), ); @@ -162,29 +132,17 @@ public function execute_callback( $input ) { ); } - // Get target. - $target = isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(); - $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); - $merge = ! ( isset( $input['replace_all'] ) && (bool) $input['replace_all'] ); + // Get attributes, position and index. + $attrs = isset( $input['attrs'] ) && is_array( $input['attrs'] ) ? $input['attrs'] : array(); + $occurrence_index = isset( $input['occurrence_index'] ) ? (int) $input['occurrence_index'] : 0; - // Resolve target. - $occurrence_index = $this->resolve_target( $post_id, $target ); - if ( is_wp_error( $occurrence_index ) ) { - return $occurrence_index; - } - - // Update block in post. - $result = ConvertKit_Block_Post_Helper::update( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index, $attrs, $merge ); - if ( is_wp_error( $result ) ) { - return $result; - } + // Update block into post. + $result = ConvertKit_Block_Post_Helper::update( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index, $attrs ); // Return result. return array( - 'post_id' => $post_id, - 'block' => 'convertkit/' . $this->block->get_name(), - 'occurrence_index' => (int) $occurrence_index, - 'attrs' => isset( $result['attrs'] ) ? $result['attrs'] : $attrs, + 'post_id' => $post_id, + 'result' => $result, ); } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index 6979ab5da..a091702ed 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -99,139 +99,31 @@ public function permission_callback( $input ) { } /** - * Returns the JSON Schema fragment for a `target` object describing which - * occurrence of the block the ability should act on. Used by update/delete. + * Returns the ability's output JSON Schema. * * @since 3.4.0 * * @return array */ - public function get_target_schema() { + public function get_output_schema() { return array( - 'type' => 'object', - 'description' => __( 'Identifies which occurrence of this block in the post to act on. Either by an attribute value match, or by zero-based occurrence index.', 'convertkit' ), - 'oneOf' => array( - array( - 'type' => 'object', - 'required' => array( 'by', 'attribute', 'value' ), - 'properties' => array( - 'by' => array( - 'type' => 'string', - 'enum' => array( 'attribute' ), - ), - 'attribute' => array( - 'type' => 'string', - 'description' => __( 'The block attribute name to match against (e.g. "form").', 'convertkit' ), - ), - 'value' => array( - 'description' => __( 'The value the attribute must match.', 'convertkit' ), - ), - ), + 'type' => 'object', + 'required' => array( 'post_id', 'result' ), + 'properties' => array( + 'post_id' => array( + 'type' => 'integer', + 'description' => __( 'The Post/Page/Custom Post Type ID.', 'convertkit' ), ), - array( - 'type' => 'object', - 'required' => array( 'by', 'index' ), - 'properties' => array( - 'by' => array( - 'type' => 'string', - 'enum' => array( 'index' ), - ), - 'index' => array( - 'type' => 'integer', - 'minimum' => 0, - 'description' => __( 'Zero-based occurrence index among this block\'s appearances in the post.', 'convertkit' ), - ), - ), + 'result' => array( + 'type' => 'integer', + 'description' => __( 'The wp_update_post() result.', 'convertkit' ), ), ), ); } - /** - * Resolves a target descriptor into the zero-based occurrence index of the - * block in the post. - * - * @since 3.4.0 - * - * @param int $post_id Post ID. - * @param array $target Target descriptor (see get_target_schema()). - * @return int|WP_Error Zero-based occurrence index, or WP_Error. - */ - public function resolve_target( $post_id, $target ) { - - // Bail if target is not an array or does not have a 'by' key. - if ( ! is_array( $target ) || empty( $target['by'] ) ) { - return new WP_Error( - 'convertkit_mcp_invalid_target', - __( 'target.by is required.', 'convertkit' ) - ); - } - - // Find blocks in post. - $occurrences = ConvertKit_Block_Post_Helper::find( $post_id, 'convertkit/' . $this->block->get_name() ); - if ( is_wp_error( $occurrences ) ) { - return $occurrences; - } - - // Bail if no blocks are found. - if ( empty( $occurrences ) ) { - return new WP_Error( - 'convertkit_mcp_no_block_occurrences', - /* translators: 1: block name, 2: post ID */ - sprintf( __( 'No occurrences of block %1$s found in post %2$d.', 'convertkit' ), 'convertkit/' . $this->block->get_name(), $post_id ) - ); - } - - // Resolve target. - switch ( $target['by'] ) { - case 'index': - $idx = isset( $target['index'] ) ? (int) $target['index'] : -1; - if ( $idx < 0 || $idx >= count( $occurrences ) ) { - return new WP_Error( - 'convertkit_mcp_target_index_out_of_range', - /* translators: 1: requested index, 2: number of occurrences */ - sprintf( __( 'Target index %1$d is out of range; post has %2$d occurrence(s).', 'convertkit' ), $idx, count( $occurrences ) ) - ); - } - return $idx; - - case 'attribute': - $attr = isset( $target['attribute'] ) ? (string) $target['attribute'] : ''; - $value = isset( $target['value'] ) ? $target['value'] : null; - if ( $attr === '' ) { - return new WP_Error( - 'convertkit_mcp_invalid_target', - __( 'target.attribute is required when target.by is "attribute".', 'convertkit' ) - ); - } - foreach ( $occurrences as $i => $occ ) { - if ( ! isset( $occ['attrs'][ $attr ] ) ) { - continue; - } - // Loose comparison so '123' == 123 resolves the same target, - // since Gutenberg attributes are often stringly typed. - if ( $occ['attrs'][ $attr ] == $value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual - return $i; - } - } - return new WP_Error( - 'convertkit_mcp_target_not_found', - /* translators: 1: attribute name, 2: value, 3: block name */ - sprintf( __( 'No occurrence of block %3$s has %1$s = %2$s.', 'convertkit' ), $attr, wp_json_encode( $value ), 'convertkit/' . $this->block->get_name() ) - ); - - default: - return new WP_Error( - 'convertkit_mcp_invalid_target', - /* translators: %s: invalid 'by' value */ - sprintf( __( 'Unknown target.by value "%s". Expected "attribute" or "index".', 'convertkit' ), (string) $target['by'] ) - ); - } - - } - /** * Returns JSON Schema properties derived from the block's get_fields(), * suitable for use as the `attrs` object in an Abilities API input schema. From 81b58e3a9882e8cf06e2af36d8109e9fe2f10b97 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 15:56:36 +0800 Subject: [PATCH 13/16] Register all block abilities and set correct parameters --- .../class-convertkit-block-post-helper.php | 107 ++++++++++++------ includes/blocks/class-convertkit-block.php | 2 + ...ss-convertkit-mcp-ability-block-delete.php | 57 ++-------- ...ss-convertkit-mcp-ability-block-insert.php | 8 +- ...lass-convertkit-mcp-ability-block-list.php | 7 +- ...ss-convertkit-mcp-ability-block-update.php | 8 +- .../class-convertkit-mcp-ability-block.php | 12 +- 7 files changed, 95 insertions(+), 106 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index cb814eae3..9c692f6c5 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -15,7 +15,7 @@ class ConvertKit_Block_Post_Helper { /** - * Finds all top-level occurrences of the given block in a post's content. + * Finds all blocks matching the given block name in a Post's content. * * @since 3.4.0 * @@ -36,8 +36,9 @@ public static function find( $post_id, $block_name ) { } // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $found = array(); + $blocks = parse_blocks( $post->post_content ); + $found = array(); + $occurrence_index = 0; foreach ( $blocks as $index => $block ) { if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { @@ -45,9 +46,12 @@ public static function find( $post_id, $block_name ) { } $found[] = array( - 'index' => (int) $index, - 'attrs' => isset( $block['attrs'] ) ? (array) $block['attrs'] : array(), + 'index' => (int) $index, + 'occurrence_index' => (int) $occurrence_index, + 'attrs' => isset( $block['attrs'] ) ? (array) $block['attrs'] : array(), ); + + ++$occurrence_index; } return $found; @@ -55,8 +59,7 @@ public static function find( $post_id, $block_name ) { } /** - * Inserts a new occurrence of the given block into a post's content at the - * specified position. + * Inserts a new block into the Post's content at the specified position. * * @since 3.4.0 * @@ -99,12 +102,12 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen break; case 'index': - $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); + $insert_at = max( 0, min( (int) $index, ( count( $blocks ) - 1 ) ) ); break; case 'append': default: - $insert_at = count( $blocks ); + $insert_at = ( count( $blocks ) - 1 ); break; } @@ -112,7 +115,7 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen array_splice( $blocks, $insert_at, 0, array( $new_block ) ); // Update Post. - return wp_update_post( + $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -120,11 +123,22 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen true ); + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was inserted at. + return array( + 'post_id' => $post_id, + 'index' => $insert_at, + 'occurrence_index' => 0, // @TODO. + ); + } /** - * Updates the attributes of a specific top-level occurrence of the given - * block in a post's content. + * Updates the attributes of an existing block in the Post's content. * * @since 3.4.0 * @@ -147,24 +161,27 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs } // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $index = 0; - $matched = false; + $blocks = parse_blocks( $post->post_content ); + $update_at = 0; + $block_index = 0; + $matched = false; foreach ( $blocks as $key => $block ) { + ++$update_at; + // Skip if the block name does not match. if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { continue; } // Update the block if the occurrence index matches. - if ( $index === (int) $occurrence_index ) { + if ( $block_index === (int) $occurrence_index ) { $blocks[ $key ]['attrs'] = array_merge( (array) $block['attrs'], (array) $attrs ); $matched = true; break; } - ++$index; + ++$block_index; } // Bail if the block was not found. @@ -177,7 +194,7 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs } // Update Post. - return wp_update_post( + $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -185,11 +202,22 @@ public static function update( $post_id, $block_name, $occurrence_index, $attrs true ); + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was updated at. + return array( + 'post_id' => $post_id, + 'index' => $update_at, + 'occurrence_index' => $occurrence_index, + ); + } /** - * Deletes a specific top-level occurrence of the given block from a post's - * content. + * Deletes a specific block from the Post's content. * * @since 3.4.0 * @@ -204,45 +232,48 @@ public static function delete( $post_id, $block_name, $occurrence_index ) { $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( - 'convertkit_block_post_helper_delete_block_post_not_found', - /* translators: %d: Post ID */ + 'convertkit_block_post_helper_update_block_post_not_found', + /* translators: %d: post ID */ sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) ); } // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $occurrence = 0; - $matched = false; + $blocks = parse_blocks( $post->post_content ); + $delete_at = 0; + $block_index = 0; + $matched = false; foreach ( $blocks as $key => $block ) { + ++$delete_at; + // Skip if the block name does not match. if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { continue; } - // Delete the block if the occurrence index matches. - if ( $occurrence === (int) $occurrence_index ) { + // Update the block if the occurrence index matches. + if ( $block_index === (int) $occurrence_index ) { unset( $blocks[ $key ] ); $blocks = array_values( $blocks ); $matched = true; break; } - ++$occurrence; + ++$block_index; } // Bail if the block was not found. if ( ! $matched ) { return new WP_Error( - 'convertkit_block_post_helper_delete_block_occurrence_not_found', - /* translators: 1: Block Name, 2: Occurrence Index, 3: Post ID */ - sprintf( __( 'No occurrence #%2$d of block %1$s found in Post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) + 'convertkit_block_post_helper_occurrence_not_found', + /* translators: 1: block name, 2: occurrence index, 3: post ID */ + sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) ); } // Update Post. - return wp_update_post( + $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => serialize_blocks( $blocks ), @@ -250,6 +281,18 @@ public static function delete( $post_id, $block_name, $occurrence_index ) { true ); + // Bail if the update failed. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Return the index the block was deleted from. + return array( + 'post_id' => $post_id, + 'index' => $delete_at, + 'occurrence_index' => $occurrence_index, + ); + } } diff --git a/includes/blocks/class-convertkit-block.php b/includes/blocks/class-convertkit-block.php index 1636922fa..1426f5e42 100644 --- a/includes/blocks/class-convertkit-block.php +++ b/includes/blocks/class-convertkit-block.php @@ -68,8 +68,10 @@ public function register_abilities( $abilities ) { return array_merge( $abilities, array( + new ConvertKit_MCP_Ability_Block_List( $this ), new ConvertKit_MCP_Ability_Block_Insert( $this ), new ConvertKit_MCP_Ability_Block_Update( $this ), + new ConvertKit_MCP_Ability_Block_Delete( $this ), ) ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 4581285b9..75fc27548 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -86,42 +86,17 @@ public function get_input_schema() { return array( 'type' => 'object', - 'required' => array( 'post_id', 'target' ), + 'required' => array( 'post_id', 'occurrence_index' ), 'properties' => array( - 'post_id' => array( + 'post_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => __( 'ID of the post containing the block.', 'convertkit' ), ), - ), - ); - - } - - /** - * Returns the ability's output JSON Schema. - * - * @since 3.4.0 - * - * @return array - */ - public function get_output_schema() { - - return array( - 'type' => 'object', - 'required' => array( 'post_id', 'block', 'deleted_occurrence_index' ), - 'properties' => array( - 'post_id' => array( - 'type' => 'integer', - ), - 'block' => array( - 'type' => 'string', - 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), - ), - 'deleted_occurrence_index' => array( + 'occurrence_index' => array( 'type' => 'integer', 'minimum' => 0, - 'description' => __( 'Zero-based occurrence index of the deleted block among this block\'s appearances in the post prior to deletion.', 'convertkit' ), + 'description' => __( 'The zero-based occurrence index of the block to delete.', 'convertkit' ), ), ), ); @@ -149,29 +124,11 @@ public function execute_callback( $input ) { ); } - // Get target. - $target = isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(); - - // Resolve target. - $occurrence_index = $this->resolve_target( $post_id, $target ); - - // Bail if the target is not found. - if ( is_wp_error( $occurrence_index ) ) { - return $occurrence_index; - } + // Get occurrence index. + $occurrence_index = isset( $input['occurrence_index'] ) ? (int) $input['occurrence_index'] : 0; // Delete block from post. - $result = ConvertKit_Block_Post_Helper::delete( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index ); - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return result. - return array( - 'post_id' => $post_id, - 'block' => 'convertkit/' . $this->block->get_name(), - 'deleted_occurrence_index' => (int) $occurrence_index, - ); + return ConvertKit_Block_Post_Helper::delete( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index ); } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php index e87602fe9..c76b30edd 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-insert.php @@ -132,13 +132,7 @@ public function execute_callback( $input ) { $index = isset( $input['index'] ) ? (int) $input['index'] : 0; // Insert block into post. - $result = ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $position, $index ); - - // Return result. - return array( - 'post_id' => $post_id, - 'result' => $result, - ); + return ConvertKit_Block_Post_Helper::insert( $post_id, 'convertkit/' . $this->block->get_name(), $attrs, $position, $index ); } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index 0ee9c2380..3f516a3ba 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -118,15 +118,11 @@ public function get_output_schema() { return array( 'type' => 'object', - 'required' => array( 'post_id', 'block', 'count', 'occurrences' ), + 'required' => array( 'post_id', 'count', 'occurrences' ), 'properties' => array( 'post_id' => array( 'type' => 'integer', ), - 'block' => array( - 'type' => 'string', - 'description' => __( 'The full block name, e.g. convertkit/form.', 'convertkit' ), - ), 'count' => array( 'type' => 'integer', 'minimum' => 0, @@ -184,7 +180,6 @@ public function execute_callback( $input ) { // Return result. return array( 'post_id' => $post_id, - 'block' => 'convertkit/' . $this->block->get_name(), 'count' => count( $occurrences ), 'occurrences' => $occurrences, ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 59b62409a..0c939e8ab 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -137,13 +137,7 @@ public function execute_callback( $input ) { $occurrence_index = isset( $input['occurrence_index'] ) ? (int) $input['occurrence_index'] : 0; // Update block into post. - $result = ConvertKit_Block_Post_Helper::update( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index, $attrs ); - - // Return result. - return array( - 'post_id' => $post_id, - 'result' => $result, - ); + return ConvertKit_Block_Post_Helper::update( $post_id, 'convertkit/' . $this->block->get_name(), $occurrence_index, $attrs ); } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index a091702ed..c1d1586ba 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -109,15 +109,19 @@ public function get_output_schema() { return array( 'type' => 'object', - 'required' => array( 'post_id', 'result' ), + 'required' => array( 'post_id', 'occurrence_index', 'index' ), 'properties' => array( - 'post_id' => array( + 'post_id' => array( 'type' => 'integer', 'description' => __( 'The Post/Page/Custom Post Type ID.', 'convertkit' ), ), - 'result' => array( + 'occurrence_index' => array( 'type' => 'integer', - 'description' => __( 'The wp_update_post() result.', 'convertkit' ), + 'description' => __( 'The zero-based occurrence index of the block in the post.', 'convertkit' ), + ), + 'index' => array( + 'type' => 'integer', + 'description' => __( 'The zero-based index of the block in the post.', 'convertkit' ), ), ), ); From 0fadf1c962853a6b54ac8866ded83b9f98dfc3b5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 23 Apr 2026 17:53:06 +0800 Subject: [PATCH 14/16] Tidy up comments --- includes/blocks/class-convertkit-block-post-helper.php | 4 ++-- .../blocks/class-convertkit-mcp-ability-block-delete.php | 6 +++--- .../blocks/class-convertkit-mcp-ability-block-list.php | 2 +- .../blocks/class-convertkit-mcp-ability-block-update.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php index 9c692f6c5..6d659583b 100644 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ b/includes/blocks/class-convertkit-block-post-helper.php @@ -102,12 +102,12 @@ public static function insert( $post_id, $block_name, $attrs, $position = 'appen break; case 'index': - $insert_at = max( 0, min( (int) $index, ( count( $blocks ) - 1 ) ) ); + $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); break; case 'append': default: - $insert_at = ( count( $blocks ) - 1 ); + $insert_at = count( $blocks ); break; } diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 75fc27548..6973f0ceb 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -1,14 +1,14 @@ -delete` (e.g. `kit/form-delete`). diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index 3f516a3ba..ef95f78cc 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -1,6 +1,6 @@ Date: Fri, 15 May 2026 17:01:37 +0800 Subject: [PATCH 15/16] Remove block post helper class --- .../class-convertkit-block-post-helper.php | 298 ------------------ wp-convertkit.php | 1 - 2 files changed, 299 deletions(-) delete mode 100644 includes/blocks/class-convertkit-block-post-helper.php diff --git a/includes/blocks/class-convertkit-block-post-helper.php b/includes/blocks/class-convertkit-block-post-helper.php deleted file mode 100644 index 6d659583b..000000000 --- a/includes/blocks/class-convertkit-block-post-helper.php +++ /dev/null @@ -1,298 +0,0 @@ -post_content ); - $found = array(); - $occurrence_index = 0; - - foreach ( $blocks as $index => $block ) { - if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { - continue; - } - - $found[] = array( - 'index' => (int) $index, - 'occurrence_index' => (int) $occurrence_index, - 'attrs' => isset( $block['attrs'] ) ? (array) $block['attrs'] : array(), - ); - - ++$occurrence_index; - } - - return $found; - - } - - /** - * Inserts a new block into the Post's content at the specified position. - * - * @since 3.4.0 - * - * @param int $post_id Post ID. - * @param string $block_name Programmatic Block Name. - * @param array $attrs Block Attributes. - * @param string $position One of 'prepend', 'append', 'index'. - * @param int $index Zero-based top-level block index; only used when $position is 'index'. - * @return int|WP_Error - */ - public static function insert( $post_id, $block_name, $attrs, $position = 'append', $index = 0 ) { - - // Get Post. - $post = get_post( $post_id ); - if ( ! $post ) { - return new WP_Error( - 'convertkit_block_post_helper_insert_block_post_not_found', - /* translators: %d: Post ID */ - sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) - ); - } - - // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - - // Build the new block to insert. - $new_block = array( - 'blockName' => $block_name, - 'attrs' => (array) $attrs, - 'innerBlocks' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - ); - - // Resolve $position into a concrete zero-based splice point in the - // top-level block array. - switch ( $position ) { - case 'prepend': - $insert_at = 0; - break; - - case 'index': - $insert_at = max( 0, min( (int) $index, count( $blocks ) ) ); - break; - - case 'append': - default: - $insert_at = count( $blocks ); - break; - } - - // Splice in the new block. - array_splice( $blocks, $insert_at, 0, array( $new_block ) ); - - // Update Post. - $result = wp_update_post( - array( - 'ID' => $post_id, - 'post_content' => serialize_blocks( $blocks ), - ), - true - ); - - // Bail if the update failed. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the index the block was inserted at. - return array( - 'post_id' => $post_id, - 'index' => $insert_at, - 'occurrence_index' => 0, // @TODO. - ); - - } - - /** - * Updates the attributes of an existing block in the Post's content. - * - * @since 3.4.0 - * - * @param int $post_id Post ID. - * @param string $block_name Programmatic Block Name. - * @param int $occurrence_index Position to update block. - * @param array $attrs Block Attributes. - * @return int|WP_Error - */ - public static function update( $post_id, $block_name, $occurrence_index, $attrs ) { - - // Get Post. - $post = get_post( $post_id ); - if ( ! $post ) { - return new WP_Error( - 'convertkit_block_post_helper_update_block_post_not_found', - /* translators: %d: post ID */ - sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) - ); - } - - // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $update_at = 0; - $block_index = 0; - $matched = false; - - foreach ( $blocks as $key => $block ) { - ++$update_at; - - // Skip if the block name does not match. - if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { - continue; - } - - // Update the block if the occurrence index matches. - if ( $block_index === (int) $occurrence_index ) { - $blocks[ $key ]['attrs'] = array_merge( (array) $block['attrs'], (array) $attrs ); - $matched = true; - break; - } - - ++$block_index; - } - - // Bail if the block was not found. - if ( ! $matched ) { - return new WP_Error( - 'convertkit_block_post_helper_occurrence_not_found', - /* translators: 1: block name, 2: occurrence index, 3: post ID */ - sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) - ); - } - - // Update Post. - $result = wp_update_post( - array( - 'ID' => $post_id, - 'post_content' => serialize_blocks( $blocks ), - ), - true - ); - - // Bail if the update failed. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the index the block was updated at. - return array( - 'post_id' => $post_id, - 'index' => $update_at, - 'occurrence_index' => $occurrence_index, - ); - - } - - /** - * Deletes a specific block from the Post's content. - * - * @since 3.4.0 - * - * @param int $post_id Post ID. - * @param string $block_name Programmatic Block Name. - * @param int $occurrence_index Zero-based index among this block's occurrences in the post. - * @return int|WP_Error - */ - public static function delete( $post_id, $block_name, $occurrence_index ) { - - // Get Post. - $post = get_post( $post_id ); - if ( ! $post ) { - return new WP_Error( - 'convertkit_block_post_helper_update_block_post_not_found', - /* translators: %d: post ID */ - sprintf( __( 'No Post exists with ID %d.', 'convertkit' ), $post_id ) - ); - } - - // Parse blocks. - $blocks = parse_blocks( $post->post_content ); - $delete_at = 0; - $block_index = 0; - $matched = false; - - foreach ( $blocks as $key => $block ) { - ++$delete_at; - - // Skip if the block name does not match. - if ( ! isset( $block['blockName'] ) || $block['blockName'] !== $block_name ) { - continue; - } - - // Update the block if the occurrence index matches. - if ( $block_index === (int) $occurrence_index ) { - unset( $blocks[ $key ] ); - $blocks = array_values( $blocks ); - $matched = true; - break; - } - - ++$block_index; - } - - // Bail if the block was not found. - if ( ! $matched ) { - return new WP_Error( - 'convertkit_block_post_helper_occurrence_not_found', - /* translators: 1: block name, 2: occurrence index, 3: post ID */ - sprintf( __( 'No occurrence #%2$d of block %1$s found in post %3$d.', 'convertkit' ), $block_name, (int) $occurrence_index, $post_id ) - ); - } - - // Update Post. - $result = wp_update_post( - array( - 'ID' => $post_id, - 'post_content' => serialize_blocks( $blocks ), - ), - true - ); - - // Bail if the update failed. - if ( is_wp_error( $result ) ) { - return $result; - } - - // Return the index the block was deleted from. - return array( - 'post_id' => $post_id, - 'index' => $delete_at, - 'occurrence_index' => $occurrence_index, - ); - - } - -} diff --git a/wp-convertkit.php b/wp-convertkit.php index 2d3092183..43a8d9713 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -97,7 +97,6 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-user.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-widgets.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block.php'; -require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-post-helper.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-broadcasts.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-content.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/blocks/class-convertkit-block-form-trigger.php'; From c618f4f28cc1b961f933a773bf778b82fa3013dd Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 15 May 2026 17:15:52 +0800 Subject: [PATCH 16/16] PHPStan compat. --- includes/blocks/class-convertkit-block-form.php | 3 --- .../blocks/class-convertkit-mcp-ability-block-delete.php | 2 +- .../blocks/class-convertkit-mcp-ability-block-list.php | 4 ++-- .../blocks/class-convertkit-mcp-ability-block-update.php | 2 +- .../abilities/blocks/class-convertkit-mcp-ability-block.php | 4 ---- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/includes/blocks/class-convertkit-block-form.php b/includes/blocks/class-convertkit-block-form.php index f91f111b4..f5e121ba8 100644 --- a/includes/blocks/class-convertkit-block-form.php +++ b/includes/blocks/class-convertkit-block-form.php @@ -27,9 +27,6 @@ public function __construct() { // Register this as a Gutenberg block in the ConvertKit Plugin. add_filter( 'convertkit_blocks', array( $this, 'register' ) ); - // Register this block's MCP abilities. - add_filter( 'convertkit_abilities', array( $this, 'register_abilities' ) ); - // Enqueue scripts for this Gutenberg Block in the editor view. add_action( 'convertkit_gutenberg_enqueue_scripts', array( $this, 'enqueue_scripts_editor' ) ); diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php index 6973f0ceb..67531683b 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-delete.php @@ -25,7 +25,7 @@ class ConvertKit_MCP_Ability_Block_Delete extends ConvertKit_MCP_Ability_Block { * * @var bool */ - private $destructive = true; + private $destructive = true; // @phpstan-ignore-line /** * Returns the verb this ability represents. diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php index ef95f78cc..2d23b5ac8 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-list.php @@ -25,7 +25,7 @@ class ConvertKit_MCP_Ability_Block_List extends ConvertKit_MCP_Ability_Block { * * @var bool */ - private $readonly = true; + private $readonly = true; // @phpstan-ignore-line /** * Sets whether the ability is idempotent. @@ -34,7 +34,7 @@ class ConvertKit_MCP_Ability_Block_List extends ConvertKit_MCP_Ability_Block { * * @var bool */ - private $idempotent = true; + private $idempotent = true; // @phpstan-ignore-line /** * Returns the verb this ability represents. diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php index 321d25a09..5f6cc3fc0 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block-update.php @@ -28,7 +28,7 @@ class ConvertKit_MCP_Ability_Block_Update extends ConvertKit_MCP_Ability_Block { * * @var bool */ - private $idempotent = true; + private $idempotent = true; // @phpstan-ignore-line /** * Returns the verb this ability represents. diff --git a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php index c1d1586ba..f691e51f9 100644 --- a/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php +++ b/includes/mcp/abilities/blocks/class-convertkit-mcp-ability-block.php @@ -145,10 +145,6 @@ protected function get_input_schema_properties() { $properties = array(); $fields = $this->block->get_fields(); - if ( ! is_array( $fields ) ) { - return $properties; - } - foreach ( $fields as $field_name => $field ) { $properties[ $field_name ] = array( 'description' => isset( $field['label'] ) ? (string) $field['label'] : '',