Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
254d671
MCP + Abilities API: Registration Framework
n7studios Apr 20, 2026
6e52159
Remove block abilities
n7studios Apr 20, 2026
1b4a5be
Remove block helpers
n7studios Apr 20, 2026
fd2b520
Abillities API: Block Framework
n7studios Apr 20, 2026
a0ff1b0
Include block framework classes
n7studios Apr 20, 2026
f876f5d
Set `get_category` to not be abstract
n7studios Apr 20, 2026
a4f501f
Fetch input schema from block fields/attributes
n7studios Apr 20, 2026
afa1e7e
Register Form Insertion Ability
n7studios Apr 20, 2026
1c5b9c3
Abilities: Prefix with `block-`
n7studios Apr 20, 2026
db3eec1
Insert Block: Use `index`
n7studios Apr 20, 2026
5b32fce
Merge branch 'main' into abilities-api
n7studios Apr 23, 2026
89968ba
Merge branch 'abilities-api' into abilities-api-block-framework
n7studios Apr 23, 2026
bf522cc
Use ConvertKit_Block_Post_Helper class
n7studios Apr 23, 2026
3ed5446
Fix static method arguments
n7studios Apr 23, 2026
fc4d1ed
Simplify block post helper method
n7studios Apr 23, 2026
63042f1
Move annotations to higher classes
n7studios Apr 23, 2026
a3d689e
Simplify input/output schema
n7studios Apr 23, 2026
81b58e3
Register all block abilities and set correct parameters
n7studios Apr 23, 2026
0fadf1c
Tidy up comments
n7studios Apr 23, 2026
6aa6dcc
Add annotations as class properties
n7studios Apr 23, 2026
e636b9d
Merge branch 'abilities-api' into abilities-api-block-framework
n7studios Apr 23, 2026
fc41b67
Merge branch 'main' into abilities-api
n7studios May 5, 2026
96a704d
Merge branch 'abilities-api' into abilities-api-block-framework
n7studios May 5, 2026
5f362a2
Added MCP Server Tests
n7studios May 8, 2026
8e37e2e
Load MCP Adapter as Composer Dependency
n7studios May 8, 2026
d1ecb12
Load WordPress MCP Adapter directly
n7studios May 8, 2026
f9e6af2
Initialize MCP Adapter
n7studios May 8, 2026
bab16d0
Fix Coding Standards + MCP Adapter on PHP 7.2 + 7.3
n7studios May 8, 2026
94830d8
Use McpAdapter::instance() to initialize the MCP Adapter
n7studios May 8, 2026
4c9682f
Coding standards
n7studios May 8, 2026
7799dc4
MCP Adapter: Only load if Abilities API exists and PHP 7.4+ is used
n7studios May 8, 2026
1caa36b
Coding Standards: Don’t generate `vendor/composer/platform_check.php`…
n7studios May 8, 2026
b3208cc
Revert platform-check changes
n7studios May 8, 2026
e462e24
Coding Standards: Remove `wordpress/mcp-adapter` on PHP 7.2 and 7.3
n7studios May 8, 2026
2777e8e
Merge branch 'abilities-api' into abilities-api-block-framework
n7studios May 8, 2026
abc0dd8
Merge remote-tracking branch 'origin/tests-fix-php-8.3' into abilitie…
n7studios May 11, 2026
4798b1e
Coding standards: Remove wordpress/mcp-adapter on PHP 7.2 + 7.3
n7studios May 11, 2026
2b93768
Coding standards: Use `jq` to remove wordpress/mcp-adapter
n7studios May 11, 2026
dc1cadf
PHPStan compat.
n7studios May 11, 2026
6b9e930
Merge branch 'main' into abilities-api
n7studios May 12, 2026
fb9e790
Reinstate jq command
n7studios May 12, 2026
f72942e
Merge branch 'abilities-api' into abilities-api-block-framework
n7studios May 15, 2026
f0ab76b
Abilites API: Block Post Helper
n7studios May 15, 2026
246fe7c
Started tests
n7studios May 15, 2026
4dab466
Completed tests
n7studios May 15, 2026
a8a442b
PHPStan compat.
n7studios May 15, 2026
678d681
Merge remote-tracking branch 'origin/abilities-api-block-post-helper'…
n7studios May 15, 2026
fcd156f
Remove block post helper class
n7studios May 15, 2026
c618f4f
PHPStan compat.
n7studios May 15, 2026
e558396
Coding standards
n7studios May 15, 2026
8a26dbc
Merge branch 'abilities-api-block-post-helper' into abilities-api-blo…
n7studios May 15, 2026
479a65a
Merge pull request #1096 from Kit/abilities-api-block-framework
n7studios May 19, 2026
97739da
Merge pull request #1095 from Kit/abilities-api-block-post-helper
n7studios May 19, 2026
2cf2bee
Merge branch 'main' into abilities-api
n7studios May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
/node_modules
/resources/frontend/css/*.map
/tests
/vendor/autoload.php
/vendor/composer
/vendor/convertkit/convertkit-wordpress-libraries/.git
/vendor/convertkit/convertkit-wordpress-libraries/.github
/vendor/convertkit/convertkit-wordpress-libraries/tests
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/coding-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,12 @@ jobs:
tools: cs2pr

# Installs wp-browser, Codeception, PHP CodeSniffer and anything else needed to run tests.
# jq is used to remove the wordpress/mcp-adapter package from composer.json, as it requires PHP 7.4+.
- name: Run Composer
working-directory: ${{ env.PLUGIN_DIR }}
run: composer update
run: |
jq 'del(.require."wordpress/mcp-adapter")' composer.json > composer.json.tmp && mv composer.json.tmp composer.json
composer update

# Installs WordPress scripts for CSS and JS Coding Standards.
- name: Run npm install
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ jobs:
"resources/frontend/css/frontend.css"
"resources/frontend/js/dist/frontend.min.asset.php"
"resources/frontend/js/dist/frontend.min.js"
"vendor/autoload.php"
"vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-api-traits.php"
"vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-api-v4.php"
"vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-log.php"
"vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-resource-v4.php"
"vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-review-request.php"
"vendor/wordpress/mcp-adapter/mcp-adapter.php"
)

for file in "${files[@]}"; do
Expand Down
2 changes: 0 additions & 2 deletions .scripts/create-plugin-zip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ zip -r convertkit.zip . \
-x ".wordpress-org/*" \
-x "log/*" \
-x "tests/*" \
-x "vendor/composer/*" \
-x "vendor/convertkit/convertkit-wordpress-libraries/.github" \
-x "vendor/convertkit/convertkit-wordpress-libraries/tests/*" \
-x "vendor/convertkit/convertkit-wordpress-libraries/composer.json" \
-x "vendor/autoload.php" \
-x "*.distignore" \
-x "*.env.*" \
-x ".gitignore" \
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "project",
"license": "GPLv3",
"require": {
"convertkit/convertkit-wordpress-libraries": "2.1.6"
"convertkit/convertkit-wordpress-libraries": "2.1.6",
"wordpress/mcp-adapter": "^0.5.0"
},
"require-dev": {
"php-webdriver/webdriver": "^1.0",
Expand Down
22 changes: 22 additions & 0 deletions includes/blocks/class-convertkit-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ 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_List( $this ),
new ConvertKit_MCP_Ability_Block_Insert( $this ),
new ConvertKit_MCP_Ability_Block_Update( $this ),
new ConvertKit_MCP_Ability_Block_Delete( $this ),
)
);

}

/**
* Returns this block's programmatic name, excluding the convertkit- prefix.
*
Expand Down
301 changes: 301 additions & 0 deletions includes/blocks/helpers/class-convertkit-block-post-helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
<?php
/**
* ConvertKit Block Post Helper class.
*
* @package ConvertKit
* @author ConvertKit
*/

/**
* Helper methods to find, insert, update and delete blocks within a WordPress Post's content.
*
* @package ConvertKit
* @author ConvertKit
*/
class ConvertKit_Block_Post_Helper {

/**
* Finds all blocks matching the given block name in a Post's content.
*
* @since 3.4.0
*
* @param int $post_id Post ID.
* @param string $block_name Programmatic Block Name.
* @return WP_Error|bool|array
*/
public static 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();

$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' => $block['attrs'],
);

++$occurrence_index;
}

// If no blocks found, return false.
if ( empty( $found ) ) {
return false;
}

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 WP_Error|array
*/
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,
);

}

/**
* 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 WP_Error|array
*/
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 - 1 ),
);

}

/**
* 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 WP_Error|array
*/
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 - 1 ),
);

}

}
Loading
Loading