From 3cad96e02d690998291953a27ff7e78c527bbd91 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 18 Jun 2026 14:27:03 +0800 Subject: [PATCH 1/5] Abilities API: Plugin Settings --- .../class-convertkit-admin-section-base.php | 20 ++ ...class-convertkit-admin-section-general.php | 3 + includes/class-convertkit-settings.php | 103 ++++++++++ ...ss-convertkit-mcp-ability-settings-get.php | 144 ++++++++++++++ ...convertkit-mcp-ability-settings-update.php | 182 ++++++++++++++++++ .../class-convertkit-mcp-ability-settings.php | 110 +++++++++++ wp-convertkit.php | 3 + 7 files changed, 565 insertions(+) create mode 100644 includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php create mode 100644 includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php create mode 100644 includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings.php diff --git a/admin/section/class-convertkit-admin-section-base.php b/admin/section/class-convertkit-admin-section-base.php index 79d3e38c0..93170908e 100644 --- a/admin/section/class-convertkit-admin-section-base.php +++ b/admin/section/class-convertkit-admin-section-base.php @@ -99,6 +99,26 @@ public function __construct() { } + /** + * Registers this settings section'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( + 'kit/' . $this->settings->get_name() . '-get' => new ConvertKit_MCP_Ability_Settings_Get( $this->settings ), + 'kit/' . $this->settings->get_name() . '-update' => new ConvertKit_MCP_Ability_Settings_Update( $this->settings ), + ) + ); + + } + /** * Helper method to determine if we're viewing the current settings screen. * diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index dc10bbb5b..e69373eaa 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -93,6 +93,9 @@ public function __construct() { add_action( 'convertkit_admin_settings_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'convertkit_admin_settings_enqueue_styles', array( $this, 'enqueue_styles' ) ); + // Register MCP abilities. + add_filter( 'convertkit_abilities', array( $this, 'register_abilities' ) ); + parent::__construct(); $this->check_credentials(); diff --git a/includes/class-convertkit-settings.php b/includes/class-convertkit-settings.php index 13c5234b2..ace2b8c3a 100644 --- a/includes/class-convertkit-settings.php +++ b/includes/class-convertkit-settings.php @@ -568,6 +568,109 @@ public function usage_tracking() { } + /** + * Returns this settings group's programmatic name. + * + * @since 3.4.0 + * + * @return string + */ + public function get_name() { + + return 'general'; + + } + + /** + * Returns the keys in this settings group that hold credentials or other + * sensitive values. + * + * @since 3.4.0 + * + * @return string[] + */ + public function get_secret_keys() { + + return array( + 'access_token', + 'refresh_token', + 'token_expires', + 'api_key', + 'api_secret', + 'recaptcha_secret_key', + ); + + } + + /** + * Returns the JSON Schema describing this settings group, in the shape + * stored by save() / returned by get(), excluding secret keys. + * + * @since 3.4.0 + * + * @return array + */ + public function get_schema() { + + return array( + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => array( + 'non_inline_form' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'description' => __( 'IDs of non-inline Forms to display site-wide.', 'convertkit' ), + ), + 'non_inline_form_honor_none_setting' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether the site-wide non-inline Form honors a per-Page / per-Post "None" Form setting.', 'convertkit' ), + ), + 'non_inline_form_limit_per_session' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether to limit non-inline Form display to once per session.', 'convertkit' ), + ), + 'recaptcha_site_key' => array( + 'type' => 'string', + 'description' => __( 'Google reCAPTCHA v3 site key.', 'convertkit' ), + ), + 'recaptcha_minimum_score' => array( + 'type' => 'number', + 'minimum' => 0, + 'maximum' => 1, + 'description' => __( 'Minimum Google reCAPTCHA v3 score (0.0 - 1.0) below which a request is treated as spam.', 'convertkit' ), + ), + 'debug' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether debug logging is enabled.', 'convertkit' ), + ), + 'no_scripts' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether the Plugin\'s frontend JavaScript is disabled.', 'convertkit' ), + ), + 'no_css' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether the Plugin\'s frontend CSS is disabled.', 'convertkit' ), + ), + 'no_add_new_button' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether the "Add New" button for Landing Pages / Member Content is hidden in the WordPress Admin.', 'convertkit' ), + ), + 'usage_tracking' => array( + 'type' => 'string', + 'enum' => array( '', 'on' ), + 'description' => __( 'Whether anonymous usage tracking is enabled.', 'convertkit' ), + ), + ), + ); + + } + /** * The default settings, used when the ConvertKit Plugin Settings haven't been saved * e.g. on a new installation. diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php new file mode 100644 index 000000000..710727b42 --- /dev/null +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php @@ -0,0 +1,144 @@ +-get` (e.g. `kit/settings-general-get`). + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Settings_Get extends ConvertKit_MCP_Ability_Settings { + + /** + * Sets whether the ability is readonly. + * + * @since 3.4.0 + * + * @var bool + */ + private $readonly = true; // @phpstan-ignore-line + + /** + * Sets whether the ability is idempotent. + * + * @since 3.4.0 + * + * @var bool + */ + private $idempotent = true; // @phpstan-ignore-line + + /** + * Returns the operation suffix used in the ability name. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_operation() { + + return 'get'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: Settings group slug, e.g. 'general'. */ + __( 'Get Kit %s settings', 'convertkit' ), + $this->settings->get_name() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: %s: Settings group slug, e.g. 'general'. */ + __( 'Returns the current values of the Kit "%s" settings group.', 'convertkit' ), + $this->settings->get_name() + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * Get takes no input. + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return array( + 'type' => 'object', + 'properties' => new stdClass(), + ); + + } + + /** + * Returns the ability's output JSON Schema. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return $this->get_public_schema(); + + } + + /** + * Executes the ability: returns the current settings, scoped to the keys + * declared in the public schema. + * + * @since 3.4.0 + * + * @param array $input Ability input (unused). + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + $values = $this->settings->get(); + $schema = $this->get_public_schema(); + $result = array(); + + if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) { + return $result; + } + + foreach ( array_keys( $schema['properties'] ) as $key ) { + if ( array_key_exists( $key, $values ) ) { + $result[ $key ] = $values[ $key ]; + } + } + + return $result; + + } + +} diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php new file mode 100644 index 000000000..06bcfd808 --- /dev/null +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php @@ -0,0 +1,182 @@ +-update` (e.g. `kit/settings-general-update`). + * + * @package ConvertKit + * @author ConvertKit + */ +class ConvertKit_MCP_Ability_Settings_Update extends ConvertKit_MCP_Ability_Settings { + + /** + * Sets whether the ability is idempotent. + * + * @since 3.4.0 + * + * @var bool + */ + private $idempotent = true; // @phpstan-ignore-line + + /** + * Returns the operation suffix used in the ability name. + * + * @since 3.4.0 + * + * @return string + */ + protected function get_operation() { + + return 'update'; + + } + + /** + * Returns the ability's human-readable label. + * + * @since 3.4.0 + * + * @return string + */ + public function get_label() { + + return sprintf( + /* translators: %s: Settings group slug, e.g. 'general'. */ + __( 'Update Kit %s settings', 'convertkit' ), + $this->settings->get_slug() + ); + + } + + /** + * Returns the ability's human-readable description. + * + * @since 3.4.0 + * + * @return string + */ + public function get_description() { + + return sprintf( + /* translators: %s: Settings group slug, e.g. 'general'. */ + __( 'Updates one or more values in the Kit "%s" settings group. Only keys declared in the input schema can be updated; secret values (API keys, OAuth tokens) cannot be set via this ability.', 'convertkit' ), + $this->settings->get_name() + ); + + } + + /** + * Returns the ability's input JSON Schema. + * + * Mirrors the settings class's get_schema() with secret keys removed, so + * partial updates are possible (no top-level `required`). + * + * @since 3.4.0 + * + * @return array + */ + public function get_input_schema() { + + return $this->get_public_schema(); + + } + + /** + * Returns the ability's output JSON Schema. + * + * Returns the same shape as kit/settings--get so a caller can chain + * update → confirm in one round trip. + * + * @since 3.4.0 + * + * @return array + */ + public function get_output_schema() { + + return $this->get_public_schema(); + + } + + /** + * Executes the ability. + * + * Validates the input, rejecting unknown and secret keys + * and saves via the settings class. + * + * @since 3.4.0 + * + * @param array $input Ability input. + * @return array|WP_Error + */ + public function execute_callback( $input ) { + + // Bail if no input is provided. + if ( ! is_array( $input ) ) { + return new WP_Error( + 'convertkit_mcp_settings_invalid_input', + __( 'Input must be an object of settings keys and values.', 'convertkit' ) + ); + } + + // Get the public schema, allowed and secret keys. + $schema = $this->get_public_schema(); + $allowed_keys = array_keys( $schema['properties'] ); + $secret_keys = $this->settings->get_secret_keys(); + + // Bail if any secret keys are provided in the input. + $secret_attempts = array_intersect( array_keys( $input ), $secret_keys ); + if ( ! empty( $secret_attempts ) ) { + return new WP_Error( + 'convertkit_mcp_settings_secret_write', + sprintf( + /* translators: %s: Comma-separated list of secret keys. */ + __( 'The following settings cannot be updated via MCP: %s.', 'convertkit' ), + implode( ', ', $secret_attempts ) + ) + ); + } + + // Bail if any unknown keys are provided in the input. + $unknown_attempts = array_diff( array_keys( $input ), $allowed_keys ); + if ( ! empty( $unknown_attempts ) ) { + return new WP_Error( + 'convertkit_mcp_settings_unknown_keys', + sprintf( + /* translators: %s: Comma-separated list of unknown keys. */ + __( 'The following settings keys are not recognised: %s.', 'convertkit' ), + implode( ', ', $unknown_attempts ) + ) + ); + } + + // Validate each provided value against its declared schema. + $validated = array(); + foreach ( $input as $key => $value ) { + $valid = rest_validate_value_from_schema( $value, $schema['properties'][ $key ], $key ); + + // Bail if the value is invalid. + if ( is_wp_error( $valid ) ) { + return $valid; + } + + $validated[ $key ] = rest_sanitize_value_from_schema( $value, $schema['properties'][ $key ], $key ); + } + + // Save via the settings class so its own sanitisation runs. + $this->settings->save( $validated ); + + // Return the post-save state. + $get_ability = new ConvertKit_MCP_Ability_Settings_Get( $this->settings ); + return $get_ability->execute_callback( array() ); + + } + +} diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings.php new file mode 100644 index 000000000..ccc25e700 --- /dev/null +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings.php @@ -0,0 +1,110 @@ +settings = $settings; + + } + + /** + * Returns the operation suffix used in the ability name (e.g. 'get', + * 'update'). Combined with the settings name to produce the full + * `kit/settings--` name. + * + * @since 3.4.0 + * + * @return string + */ + abstract protected function get_operation(); + + /** + * Returns the ability name, derived from the settings name and operation + * (e.g. `kit/settings-general-get`). + * + * @since 3.4.0 + * + * @return string + */ + public function get_name() { + + return 'kit/settings-' . $this->settings->get_name() . '-' . $this->get_operation(); + + } + + /** + * Permission callback for settings abilities. + * + * Plugin settings are restricted to users who can manage options, matching + * the capability that gates the Plugin's own settings screens. + * + * @since 3.4.0 + * + * @param array $input Ability input (unused). + * @return bool|WP_Error + */ + public function permission_callback( $input ) { + + if ( ! current_user_can( 'manage_options' ) ) { + return new WP_Error( + 'convertkit_mcp_cannot_manage_settings', + __( 'You do not have permission to read or update Kit Plugin settings.', 'convertkit' ) + ); + } + + return true; + + } + + /** + * Returns the ability's input and output JSON schemas. + * + * @since 3.4.0 + * + * @return array + */ + protected function get_public_schema() { + + $schema = $this->settings->get_schema(); + $secret = $this->settings->get_secret_keys(); + + if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { + foreach ( $secret as $key ) { + unset( $schema['properties'][ $key ] ); + } + } + + return $schema; + + } + +} diff --git a/wp-convertkit.php b/wp-convertkit.php index 38fa70679..ef8dea066 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -114,6 +114,9 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/resources/class-convertkit-mcp-ability-resource-tags.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/resources/class-convertkit-mcp-ability-resource-landing-pages.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/resources/class-convertkit-mcp-ability-resource-products.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.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 405e3fa6b51116921e83b332e6ed310beb7545fa Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 18 Jun 2026 14:27:08 +0800 Subject: [PATCH 2/5] Coding standards --- .../settings/class-convertkit-mcp-ability-settings-update.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php index 06bcfd808..9b2e40001 100644 --- a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php @@ -107,7 +107,7 @@ public function get_output_schema() { /** * Executes the ability. - * + * * Validates the input, rejecting unknown and secret keys * and saves via the settings class. * @@ -127,7 +127,7 @@ public function execute_callback( $input ) { } // Get the public schema, allowed and secret keys. - $schema = $this->get_public_schema(); + $schema = $this->get_public_schema(); $allowed_keys = array_keys( $schema['properties'] ); $secret_keys = $this->settings->get_secret_keys(); From ee69daddfeb28ad48e40ad921751c61f4cefa029 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 18 Jun 2026 15:18:02 +0800 Subject: [PATCH 3/5] Move registration to MCP class --- ...class-convertkit-admin-section-general.php | 4 +-- includes/class-convertkit-settings.php | 15 +++++++- ...ss-convertkit-mcp-ability-settings-get.php | 12 +++---- ...convertkit-mcp-ability-settings-update.php | 12 +++---- includes/mcp/class-convertkit-mcp.php | 35 +++++++++++++++++++ 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index e69373eaa..4d9b18054 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -53,8 +53,8 @@ public function __construct() { $this->settings_key = $this->settings::SETTINGS_NAME; // Define the programmatic name, Title and Tab Text. - $this->name = 'general'; - $this->title = __( 'General Settings', 'convertkit' ); + $this->name = $this->settings->get_name(); + $this->title = $this->settings->get_title(); $this->tab_text = __( 'General', 'convertkit' ); // Define settings sections. diff --git a/includes/class-convertkit-settings.php b/includes/class-convertkit-settings.php index ace2b8c3a..c1f69423c 100644 --- a/includes/class-convertkit-settings.php +++ b/includes/class-convertkit-settings.php @@ -581,6 +581,19 @@ public function get_name() { } + /** + * Returns the title of this settings group. + * + * @since 3.4.0 + * + * @return string + */ + public function get_title() { + + return __( 'General Settings', 'convertkit' ); + + } + /** * Returns the keys in this settings group that hold credentials or other * sensitive values. @@ -636,7 +649,7 @@ public function get_schema() { 'description' => __( 'Google reCAPTCHA v3 site key.', 'convertkit' ), ), 'recaptcha_minimum_score' => array( - 'type' => 'number', + 'type' => 'float', 'minimum' => 0, 'maximum' => 1, 'description' => __( 'Minimum Google reCAPTCHA v3 score (0.0 - 1.0) below which a request is treated as spam.', 'convertkit' ), diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php index 710727b42..f7be8eab8 100644 --- a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-get.php @@ -57,9 +57,9 @@ protected function get_operation() { public function get_label() { return sprintf( - /* translators: %s: Settings group slug, e.g. 'general'. */ - __( 'Get Kit %s settings', 'convertkit' ), - $this->settings->get_name() + /* translators: %s: Settings Title, e.g. 'General Settings'. */ + __( 'Get Kit Plugin %s', 'convertkit' ), + $this->settings->get_title() ); } @@ -74,9 +74,9 @@ public function get_label() { public function get_description() { return sprintf( - /* translators: %s: Settings group slug, e.g. 'general'. */ - __( 'Returns the current values of the Kit "%s" settings group.', 'convertkit' ), - $this->settings->get_name() + /* translators: %s: Settings Title, e.g. 'General Settings'. */ + __( 'Returns the current values of the Kit Plugin "%s".', 'convertkit' ), + $this->settings->get_title() ); } diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php index 9b2e40001..cbb5f12c9 100644 --- a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php @@ -49,9 +49,9 @@ protected function get_operation() { public function get_label() { return sprintf( - /* translators: %s: Settings group slug, e.g. 'general'. */ - __( 'Update Kit %s settings', 'convertkit' ), - $this->settings->get_slug() + /* translators: %s: Settings Title, e.g. 'General Settings'. */ + __( 'Update Kit Plugin %s', 'convertkit' ), + $this->settings->get_title() ); } @@ -66,9 +66,9 @@ public function get_label() { public function get_description() { return sprintf( - /* translators: %s: Settings group slug, e.g. 'general'. */ - __( 'Updates one or more values in the Kit "%s" settings group. Only keys declared in the input schema can be updated; secret values (API keys, OAuth tokens) cannot be set via this ability.', 'convertkit' ), - $this->settings->get_name() + /* translators: %s: Settings Title, e.g. 'General Settings'. */ + __( 'Updates one or more values in the Kit Plugin "%s". Only keys declared in the input schema can be updated; secret values (API keys, OAuth tokens) cannot be set via this ability.', 'convertkit' ), + $this->settings->get_title() ); } diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index 33cec9fe2..5a736a38f 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -75,11 +75,46 @@ public function __construct() { // so they're added here rather than via a per-class register_abilities(). add_filter( 'convertkit_abilities', array( $this, 'register_resource_abilities' ) ); + // Register settings get / update abilities for each Plugin settings + // These are owned by the Plugin (not by any single feature), + // so they're added here rather than via a per-class register_abilities(). + add_filter( 'convertkit_abilities', array( $this, 'register_settings_abilities' ) ); + // Register the MCP server. add_action( 'mcp_adapter_init', array( $this, 'register_mcp_server' ) ); } + /** + * Appends the settings get / update abilities for each Plugin settings + * group to the convertkit_abilities filter, so they are registered with + * the Abilities API and exposed via the MCP server. + * + * @since 3.4.0 + * + * @param array $abilities Abilities to register. + * @return array + */ + public function register_settings_abilities( $abilities ) { + + // Settings instances to register with MCP. + $groups = array( + new ConvertKit_Settings(), + ); + + // Iterate through settings groups, registering the get and update abilities. + foreach ( $groups as $settings ) { + $get = new ConvertKit_MCP_Ability_Settings_Get( $settings ); + $update = new ConvertKit_MCP_Ability_Settings_Update( $settings ); + + $abilities[ $get->get_name() ] = $get; + $abilities[ $update->get_name() ] = $update; + } + + return $abilities; + + } + /** * Appends the resource-list abilities (Forms, Tags, Landing Pages, * Products) to the convertkit_abilities filter, so they are registered From 1a8417c8ecb65926b8e16b21ed7cdae5af2b1ea1 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 18 Jun 2026 15:48:03 +0800 Subject: [PATCH 4/5] Added tests --- tests/Integration/MCPSettingsGeneralTest.php | 271 +++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 tests/Integration/MCPSettingsGeneralTest.php diff --git a/tests/Integration/MCPSettingsGeneralTest.php b/tests/Integration/MCPSettingsGeneralTest.php new file mode 100644 index 000000000..abf84942e --- /dev/null +++ b/tests/Integration/MCPSettingsGeneralTest.php @@ -0,0 +1,271 @@ + \ConvertKit_MCP_Ability_Settings_Get::class, + 'kit/settings-general-update' => \ConvertKit_MCP_Ability_Settings_Update::class, + ); + + // Assert that the abilities are registered and are instances of the expected classes. + foreach ( $expected as $name => $class ) { + $this->assertArrayHasKey($name, $abilities); + $this->assertInstanceOf($class, $abilities[ $name ]); + } + } + + /** + * Test that the permission_callback() rejects a user who cannot manage options. + * + * @since 3.4.0 + */ + public function testPermissionCallbackDeniesWithoutManageOptionsCapability() + { + // Become a Subscriber (no manage_options capability). + $subscriber_id = static::factory()->user->create([ 'role' => 'subscriber' ]); + wp_set_current_user($subscriber_id); + + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Assert that the abilities are permission denied. + foreach ( self::ABILITY_NAMES as $name ) { + // Execute the ability. + $result = $abilities[ $name ]->permission_callback([]); + + // Assert that the result is a WP_Error. + $this->assertInstanceOf(\WP_Error::class, $result); + } + } + + /** + * Test that kit/settings-general-get returns the current settings. + * + * @since 3.4.0 + */ + public function testGetSettings() + { + // Populate settings. + $this->populateSettings(); + + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-general-get']->execute_callback([]); + + // Confirm secret keys are not returned. + $this->assertArrayNotHasKey('access_token', $result); + $this->assertArrayNotHasKey('refresh_token', $result); + $this->assertArrayNotHasKey('token_expires', $result); + $this->assertArrayNotHasKey('api_key', $result); + $this->assertArrayNotHasKey('api_secret', $result); + $this->assertArrayNotHasKey('recaptcha_secret_key', $result); + + // Confirm expected settings are returned. + $this->assertArrayHasKey('non_inline_form', $result); + $this->assertArrayHasKey('non_inline_form_honor_none_setting', $result); + $this->assertArrayHasKey('non_inline_form_limit_per_session', $result); + $this->assertArrayHasKey('recaptcha_site_key', $result); + $this->assertArrayHasKey('recaptcha_minimum_score', $result); + $this->assertArrayHasKey('debug', $result); + $this->assertEquals('on', $result['debug']); + $this->assertArrayHasKey('no_scripts', $result); + $this->assertArrayHasKey('no_css', $result); + $this->assertArrayHasKey('no_add_new_button', $result); + $this->assertArrayHasKey('usage_tracking', $result); + } + + /** + * Test that kit/settings-general-update updates the settings. + * + * @since 3.4.0 + */ + public function testUpdateSettings() + { + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-general-update']->execute_callback( + [ + 'recaptcha_site_key' => '12345', + 'debug' => '', + 'no_scripts' => 'on', + 'no_css' => 'on', + 'no_add_new_button' => 'on', + 'usage_tracking' => 'on', + ] + ); + + // Confirm secret keys are not returned. + $this->assertArrayNotHasKey('access_token', $result); + $this->assertArrayNotHasKey('refresh_token', $result); + $this->assertArrayNotHasKey('token_expires', $result); + $this->assertArrayNotHasKey('api_key', $result); + $this->assertArrayNotHasKey('api_secret', $result); + $this->assertArrayNotHasKey('recaptcha_secret_key', $result); + + // Confirm expected settings are returned. + $this->assertArrayHasKey('non_inline_form', $result); + $this->assertArrayHasKey('non_inline_form_honor_none_setting', $result); + $this->assertArrayHasKey('non_inline_form_limit_per_session', $result); + $this->assertArrayHasKey('recaptcha_site_key', $result); + $this->assertArrayHasKey('recaptcha_minimum_score', $result); + $this->assertArrayHasKey('debug', $result); + $this->assertArrayHasKey('no_scripts', $result); + $this->assertArrayHasKey('no_css', $result); + $this->assertArrayHasKey('no_add_new_button', $result); + $this->assertArrayHasKey('usage_tracking', $result); + + // Confirm settings are updated. + $this->assertEquals('12345', $result['recaptcha_site_key']); + $this->assertEquals('', $result['debug']); + $this->assertEquals('on', $result['no_scripts']); + $this->assertEquals('on', $result['no_css']); + $this->assertEquals('on', $result['no_add_new_button']); + $this->assertEquals('on', $result['usage_tracking']); + } + + /** + * Test that kit/settings-general-update returns an error if an invalid key is provided. + * + * @since 3.4.0 + */ + public function testUpdateSettingsWithInvalidKeyReturnsError() + { + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-general-update']->execute_callback([ 'invalid_key' => 'invalid_value' ]); + } + + /** + * Test that kit/settings-general-update returns an error if a secret key is provided. + * + * @since 3.4.0 + */ + public function testUpdateSettingsWithSecretKeyReturnsError() + { + // Resolve the abilities array via the same helper the MCP server uses. + $abilities = convertkit_get_abilities(); + + // Execute the ability. + $result = $abilities['kit/settings-general-update']->execute_callback([ 'access_token' => 'invalid_value' ]); + } + + /** + * Populate the settings with some sensible values for testing. + * + * @since 3.4.0 + */ + private function populateSettings() + { + update_option( + self::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + 'debug' => 'on', + 'no_scripts' => '', + 'no_css' => '', + 'no_add_new_button' => '', + 'usage_tracking' => '', + 'post_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'page_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'article_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'product_form' => $_ENV['CONVERTKIT_API_FORM_ID'], + 'non_inline_form' => array(), + 'non_inline_form_honor_none_setting' => '', + 'recaptcha_site_key' => '', + 'recaptcha_secret_key' => '', + 'recaptcha_minimum_score' => '', + ] + ); + } +} From c568d9942ae3d3c3d780ae6e47b592cd0a213b2c Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 18 Jun 2026 15:58:04 +0800 Subject: [PATCH 5/5] PHPStan compat. --- .../settings/class-convertkit-mcp-ability-settings-update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php index cbb5f12c9..a8e30438a 100644 --- a/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php +++ b/includes/mcp/abilities/settings/class-convertkit-mcp-ability-settings-update.php @@ -119,7 +119,7 @@ public function get_output_schema() { public function execute_callback( $input ) { // Bail if no input is provided. - if ( ! is_array( $input ) ) { + if ( ! count( $input ) ) { return new WP_Error( 'convertkit_mcp_settings_invalid_input', __( 'Input must be an object of settings keys and values.', 'convertkit' )