diff --git a/admin/section/class-convertkit-admin-section-base.php b/admin/section/class-convertkit-admin-section-base.php
index 650b0321d..79d3e38c0 100644
--- a/admin/section/class-convertkit-admin-section-base.php
+++ b/admin/section/class-convertkit-admin-section-base.php
@@ -47,7 +47,7 @@ abstract class ConvertKit_Admin_Section_Base {
*
* @since 1.9.6
*
- * @var false|ConvertKit_Settings|ConvertKit_ContactForm7_Settings|ConvertKit_Wishlist_Settings|ConvertKit_Settings_Restrict_Content|ConvertKit_Settings_Broadcasts|ConvertKit_Forminator_Settings
+ * @var false|ConvertKit_Settings|ConvertKit_ContactForm7_Settings|ConvertKit_Wishlist_Settings|ConvertKit_Settings_Restrict_Content|ConvertKit_Settings_Broadcasts|ConvertKit_Forminator_Settings|ConvertKit_Settings_MCP
*/
public $settings;
diff --git a/admin/section/class-convertkit-admin-section-mcp.php b/admin/section/class-convertkit-admin-section-mcp.php
new file mode 100644
index 000000000..8cb7f2be8
--- /dev/null
+++ b/admin/section/class-convertkit-admin-section-mcp.php
@@ -0,0 +1,176 @@
+ Kit > MCP.
+ *
+ * @package ConvertKit
+ * @author ConvertKit
+ */
+class ConvertKit_Admin_Section_MCP extends ConvertKit_Admin_Section_Base {
+
+ /**
+ * Constructor.
+ *
+ * @since 3.4.0
+ */
+ public function __construct() {
+
+ // Define the class that reads/writes settings.
+ $this->settings = new ConvertKit_Settings_MCP();
+
+ // Define the settings key.
+ $this->settings_key = $this->settings::SETTINGS_NAME;
+
+ // Define the programmatic name, Title and Tab Text.
+ $this->name = 'mcp';
+ $this->title = __( 'MCP', 'convertkit' );
+ $this->tab_text = __( 'MCP', 'convertkit' );
+
+ // Identify that this is beta functionality.
+ $this->is_beta = true;
+
+ // Define settings sections.
+ $this->settings_sections = array(
+ 'general' => array(
+ 'title' => $this->title,
+ 'callback' => array( $this, 'print_section_info' ),
+ 'wrap' => true,
+ ),
+ );
+
+ // Register and maybe output notices for this settings screen, and the Intercom messenger.
+ if ( $this->on_settings_screen( $this->name ) ) {
+ add_action( 'convertkit_settings_base_render_before', array( $this, 'maybe_output_notices' ) );
+ }
+
+ // Enqueue scripts and CSS.
+ add_action( 'convertkit_admin_settings_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+
+ parent::__construct();
+
+ }
+
+ /**
+ * Enqueues scripts for the Settings > MCP screen.
+ *
+ * @since 3.4.0
+ *
+ * @param string $section Settings section / tab (general|tools|restrict-content|broadcasts|mcp).
+ */
+ public function enqueue_scripts( $section ) {
+
+ // Bail if we're not on the MCP section.
+ if ( $section !== $this->name ) {
+ return;
+ }
+
+ // Enqueue JS.
+ wp_enqueue_script( 'convertkit-admin-settings-conditional-display', CONVERTKIT_PLUGIN_URL . 'resources/backend/js/settings-conditional-display.js', array( 'jquery' ), CONVERTKIT_PLUGIN_VERSION, true );
+
+ }
+
+ /**
+ * Registers settings fields for this section.
+ *
+ * @since 3.4.0
+ */
+ public function register_fields() {
+
+ // Enable.
+ add_settings_field(
+ 'enabled',
+ __( 'Enable MCP Server', 'convertkit' ),
+ array( $this, 'enabled_callback' ),
+ $this->settings_key,
+ $this->name,
+ array(
+ 'name' => 'enabled',
+ 'label_for' => 'enabled',
+ 'label' => __( 'When enabled, allows AI clients to connect to the Kit Plugin using MCP.', 'convertkit' ),
+ 'description' => sprintf(
+ '%s
%s',
+ __( 'Go to your AI tool to add a custom connector by pasting this URL to connect to this plugin:', 'convertkit' ),
+ get_site_url() . '/wp-json/kit/mcp/v1'
+ ),
+ )
+ );
+
+ }
+
+ /**
+ * Prints help info for this section
+ *
+ * @since 3.4.0
+ */
+ public function print_section_info() {
+
+ ?>
+
+
+ output_checkbox_field(
+ $args['name'],
+ 'on',
+ $this->settings->enabled(),
+ $args['label'],
+ $args['description'],
+ array( 'convertkit-conditional-display' )
+ );
+
+ }
+
+}
+
+// Bootstrap.
+add_filter(
+ 'convertkit_admin_settings_register_sections',
+ function ( $sections ) {
+
+ // Don't register the MCP section if the Abilities API is not available (WordPress < 6.9).
+ if ( ! function_exists( 'wp_register_ability' ) ) {
+ return $sections;
+ }
+
+ // Don't register the MCP section if PHP 7.4+ is not installed.
+ if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
+ return $sections;
+ }
+
+ $sections['mcp'] = new ConvertKit_Admin_Section_MCP();
+ return $sections;
+
+ }
+);
diff --git a/includes/class-convertkit-settings-mcp.php b/includes/class-convertkit-settings-mcp.php
new file mode 100644
index 000000000..9e58b50f1
--- /dev/null
+++ b/includes/class-convertkit-settings-mcp.php
@@ -0,0 +1,121 @@
+settings = $this->get_defaults();
+ } else {
+ $this->settings = array_merge( $this->get_defaults(), $settings );
+ }
+
+ }
+
+ /**
+ * Returns Plugin settings.
+ *
+ * @since 3.4.0
+ *
+ * @return array
+ */
+ public function get() {
+
+ return $this->settings;
+
+ }
+
+ /**
+ * Returns whether the MCP server is enabled.
+ *
+ * @since 3.4.0
+ *
+ * @return bool
+ */
+ public function enabled() {
+
+ return ( $this->settings['enabled'] === 'on' ? true : false );
+
+ }
+
+ /**
+ * The default settings, used when the ConvertKit MCP Settings haven't been saved
+ * e.g. on a new installation.
+ *
+ * @since 2.1.0
+ *
+ * @return array
+ */
+ public function get_defaults() {
+
+ $defaults = array(
+ 'enabled' => '', // blank|on.
+ );
+
+ /**
+ * The default settings, used when the ConvertKit MCP Settings haven't been saved
+ * e.g. on a new installation.
+ *
+ * @since 3.4.0
+ *
+ * @param array $defaults Default settings.
+ */
+ $defaults = apply_filters( 'convertkit_settings_mcp_get_defaults', $defaults );
+
+ return $defaults;
+
+ }
+
+ /**
+ * Saves the given array of settings to the WordPress options table.
+ *
+ * @since 3.4.0
+ *
+ * @param array $settings Settings.
+ */
+ public function save( $settings ) {
+
+ update_option( self::SETTINGS_NAME, array_merge( $this->get(), $settings ) );
+
+ }
+
+}
diff --git a/includes/class-wp-convertkit.php b/includes/class-wp-convertkit.php
index 63cfa5263..1e358c92f 100644
--- a/includes/class-wp-convertkit.php
+++ b/includes/class-wp-convertkit.php
@@ -64,6 +64,7 @@ public function initialize() {
$this->initialize_cli_cron();
$this->initialize_frontend();
$this->initialize_global();
+ $this->initialize_mcp();
}
@@ -218,6 +219,49 @@ private function initialize_global() {
}
+ /**
+ * Initializes the MCP server if enabled in the Plugin's settings.
+ *
+ * @since 3.4.0
+ */
+ public function initialize_mcp() {
+
+ // Bail if the MCP server is not enabled.
+ $settings = new ConvertKit_Settings_MCP();
+ if ( ! $settings->enabled() ) {
+ return;
+ }
+
+ // Bail if the Abilities API is unavailable (WordPress < 6.9).
+ if ( ! function_exists( 'wp_register_ability' ) ) {
+ return;
+ }
+
+ // Bail if PHP 7.4+ is not installed, as this is required for the MCP Adapter classes.
+ if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
+ return;
+ }
+
+ // Bail if the WordPress MCP Adapter autoloader is missing.
+ if ( ! file_exists( CONVERTKIT_PLUGIN_PATH . '/vendor/autoload.php' ) ) {
+ return;
+ }
+
+ // Load MCP Adapter.
+ require_once CONVERTKIT_PLUGIN_PATH . '/vendor/autoload.php';
+
+ // Bail if the MCP Adapter class doesn't exist - something went wrong with the autoloader.
+ if ( ! class_exists( 'WP\\MCP\\Core\\McpAdapter' ) ) {
+ return;
+ }
+
+ // Bootstrap the MCP Adapter, per WordPress/mcp-adapter's recommended
+ // integration pattern.
+ // @see https://github.com/WordPress/mcp-adapter#using-mcp-adapter-in-your-plugin.
+ \WP\MCP\Core\McpAdapter::instance();
+
+ }
+
/**
* Runs the Plugin's initialization and update routines, which checks if
* the Plugin has just been updated to a newer version,
diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php
index 05e3dd3df..d16d349ab 100644
--- a/includes/mcp/class-convertkit-mcp.php
+++ b/includes/mcp/class-convertkit-mcp.php
@@ -37,7 +37,7 @@ class ConvertKit_MCP {
*
* @var string
*/
- const SERVER_ID = 'kit-mcp';
+ const SERVER_ID = 'kit/mcp';
/**
* The REST namespace used by the MCP server.
@@ -46,7 +46,7 @@ class ConvertKit_MCP {
*
* @var string
*/
- const SERVER_NAMESPACE = 'kit-mcp';
+ const SERVER_NAMESPACE = 'kit/mcp';
/**
* The REST version number used by the MCP server.
@@ -64,11 +64,6 @@ class ConvertKit_MCP {
*/
public function __construct() {
- // Bail if the Abilities API is unavailable (WordPress < 6.9).
- if ( ! function_exists( 'wp_register_ability' ) ) {
- return;
- }
-
// Register the ability category.
add_action( 'wp_abilities_api_categories_init', array( $this, 'register_abilities_category' ) );
@@ -150,7 +145,7 @@ public function register_mcp_server( $adapter ) {
self::SERVER_ID,
self::SERVER_NAMESPACE,
self::SERVER_ROUTE,
- __( 'Kit MCP', 'convertkit' ),
+ __( 'Kit WordPress Plugin MCP', 'convertkit' ),
__( 'Exposes Kit Plugin abilities over the Model Context Protocol.', 'convertkit' ),
'1.0.0',
array( 'WP\\MCP\\Transport\\HttpTransport' ),
diff --git a/tests/EndToEnd.suite.yml b/tests/EndToEnd.suite.yml
index 85037bdcf..0fdb63bbb 100644
--- a/tests/EndToEnd.suite.yml
+++ b/tests/EndToEnd.suite.yml
@@ -41,6 +41,7 @@ modules:
- \Tests\Support\Helper\WPGutenberg
- \Tests\Support\Helper\WPMetabox
- \Tests\Support\Helper\WPNotices
+ - \Tests\Support\Helper\WPRestAPI
- \Tests\Support\Helper\WPQuickEdit
- \Tests\Support\Helper\WPWidget
- \Tests\Support\Helper\Xdebug
diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php
new file mode 100644
index 000000000..3d7a009ea
--- /dev/null
+++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php
@@ -0,0 +1,94 @@
+ Kit > MCP.
+ *
+ * @since 3.4.0
+ */
+class PluginSettingsMCPCest
+{
+ /**
+ * Run common actions before running the test functions in this class.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function _before(EndToEndTester $I)
+ {
+ // Activate Kit Plugin.
+ $I->activateKitPlugin($I);
+
+ // Setup Plugin.
+ $I->setupKitPlugin($I);
+ }
+
+ /**
+ * Tests that enabling and disabling the MCP server setting works with no errors.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testEnableAndDisableMCPServerSetting(EndToEndTester $I)
+ {
+ // Check that the MCP server is not registered.
+ $I->doesNotHaveRoute($I, '/kit-mcp');
+
+ // Go to the Plugin's MCP Screen.
+ $I->loadKitSettingsMCPScreen($I);
+
+ // Enable MCP server.
+ $I->checkOption('#enabled');
+ $I->click('Save Changes');
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+
+ // Check that the MCP server is enabled.
+ $I->waitForElementVisible('#enabled');
+ $I->seeCheckboxIsChecked('#enabled');
+
+ // Check that the MCP server is registered.
+ $I->hasRoute($I, '/kit/mcp');
+ $I->hasRoute($I, '/kit/mcp/v1');
+
+ // Disable MCP server.
+ $I->uncheckOption('#enabled');
+ $I->click('Save Changes');
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+
+ // Check that the MCP server is disabled.
+ $I->waitForElementVisible('#enabled');
+ $I->dontSeeCheckboxIsChecked('#enabled');
+
+ // Go to the Plugin's MCP Screen.
+ $I->loadKitSettingsMCPScreen($I);
+ $I->wait(2);
+
+ // Check that the MCP server is not registered.
+ $I->doesNotHaveRoute($I, '/kit/mcp');
+ $I->doesNotHaveRoute($I, '/kit/mcp/v1');
+ }
+
+ /**
+ * Deactivate and reset Plugin(s) after each test, if the test passes.
+ * We don't use _after, as this would provide a screenshot of the Plugin
+ * deactivation and not the true test error.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function _passed(EndToEndTester $I)
+ {
+ $I->deactivateKitPlugin($I);
+ $I->resetKitPlugin($I);
+ }
+}
diff --git a/tests/Integration/MCPTest.php b/tests/Integration/MCPTest.php
deleted file mode 100644
index 3f79ea1c5..000000000
--- a/tests/Integration/MCPTest.php
+++ /dev/null
@@ -1,109 +0,0 @@
-dispatch( $request );
-
- // Assert response is unsuccessful.
- $this->assertSame( 401, $response->get_status() );
- }
-
- /**
- * Test that the Kit MCP server is registered with the MCP Adapter and
- * exposes its discovery endpoint at /wp-json/kit-mcp/v1.
- *
- * @since 3.4.0
- */
- public function testKitMCPServerCreated()
- {
- // Create and become administrator.
- $this->actAsAdministrator();
-
- // Make request.
- $request = new \WP_REST_Request('POST', '/kit-mcp/v1');
- $request->set_header('Content-Type', 'application/json');
- $request->set_body(
- wp_json_encode(
- [
- 'jsonrpc' => '2.0',
- 'id' => 1,
- 'method' => 'initialize',
- 'params' => [
- 'protocolVersion' => '2024-11-05',
- 'capabilities' => new \stdClass(),
- 'clientInfo' => [
- 'name' => 'test',
- 'version' => '1.0',
- ],
- ],
- ]
- )
- );
- $response = rest_get_server()->dispatch($request);
-
- // Assert the discovery endpoint is registered and responds successfully.
- $this->assertSame(200, $response->get_status());
-
- // Assert the response identifies itself as the Kit MCP server.
- $data = $response->get_data();
- $this->assertSame('Kit MCP', $data['result']->serverInfo['name'] ?? null);
- }
-
- /**
- * Act as an administrator user.
- *
- * @since 3.4.0
- */
- private function actAsAdministrator()
- {
- $administrator_id = static::factory()->user->create( [ 'role' => 'administrator' ] );
- wp_set_current_user( $administrator_id );
- }
-}
diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php
index 93fee63e9..a656c238b 100644
--- a/tests/Support/Helper/KitPlugin.php
+++ b/tests/Support/Helper/KitPlugin.php
@@ -575,6 +575,7 @@ public function resetKitPlugin($I)
$I->dontHaveOptionInDatabase('_wp_convertkit_settings');
$I->dontHaveOptionInDatabase('_wp_convertkit_settings_restrict_content');
$I->dontHaveOptionInDatabase('_wp_convertkit_settings_broadcasts');
+ $I->dontHaveOptionInDatabase('_wp_convertkit_settings_mcp');
$I->dontHaveOptionInDatabase('convertkit_version');
// Resources.
diff --git a/tests/Support/Helper/KitRestrictContent.php b/tests/Support/Helper/KitRestrictContent.php
index 7c325e08b..ea1882947 100644
--- a/tests/Support/Helper/KitRestrictContent.php
+++ b/tests/Support/Helper/KitRestrictContent.php
@@ -43,6 +43,21 @@ public function loadKitSettingsRestrictContentScreen($I)
$I->checkNoWarningsAndNoticesOnScreen($I);
}
+ /**
+ * Helper method to load the Plugin's Settings > MCP screen.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I EndToEndTester.
+ */
+ public function loadKitSettingsMCPScreen($I)
+ {
+ $I->amOnAdminPage('options-general.php?page=_wp_convertkit_settings&tab=mcp');
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+ }
+
/**
* Returns the expected default settings for Restricted Content.
*
diff --git a/tests/Support/Helper/WPRestAPI.php b/tests/Support/Helper/WPRestAPI.php
new file mode 100644
index 000000000..26011ebe2
--- /dev/null
+++ b/tests/Support/Helper/WPRestAPI.php
@@ -0,0 +1,51 @@
+{yourFunctionName}.
+ *
+ * @since 3.4.0
+ */
+class WPRestAPI extends \Codeception\Module
+{
+ /**
+ * Check that the given route is registered in the REST API.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I EndToEndTester.
+ * @param string $route Route.
+ */
+ public function hasRoute($I, $route)
+ {
+ $I->assertTrue( in_array( $route, $this->getRoutes(), true ) );
+ }
+
+ /**
+ * Check that the given route is not registered in the REST API.
+ *
+ * @since 3.4.0
+ *
+ * @param EndToEndTester $I EndToEndTester.
+ * @param string $route Route.
+ */
+ public function doesNotHaveRoute($I, $route)
+ {
+ $I->assertFalse( in_array( $route, $this->getRoutes(), true ) );
+ }
+
+ /**
+ * Get the routes registered in the REST API.
+ *
+ * @since 3.4.0
+ *
+ * @return array
+ */
+ private function getRoutes()
+ {
+ $response = wp_remote_get( rest_url() );
+ $body = json_decode( wp_remote_retrieve_body( $response ), true );
+ return array_keys( $body['routes'] ?? [] );
+ }
+}
diff --git a/wp-convertkit.php b/wp-convertkit.php
index 1b6d3ea7e..f46a3708d 100644
--- a/wp-convertkit.php
+++ b/wp-convertkit.php
@@ -31,18 +31,6 @@
define( 'CONVERTKIT_OAUTH_CLIENT_ID', 'HXZlOCj-K5r0ufuWCtyoyo3f688VmMAYSsKg1eGvw0Y' );
define( 'CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI', 'https://app.kit.com/wordpress/redirect' );
-// Load WordPress MCP Adapter if the Abilities API is available (WordPress 6.9+)
-// and PHP 7.4+ is installed.
-if ( file_exists( CONVERTKIT_PLUGIN_PATH . '/vendor/autoload.php' ) && function_exists( 'wp_register_ability' ) && version_compare( PHP_VERSION, '7.4', '>=' ) ) {
- require_once CONVERTKIT_PLUGIN_PATH . '/vendor/autoload.php';
-
- // Bootstrap the MCP Adapter, per WordPress/mcp-adapter's recommended
- // integration pattern.
- // @see https://github.com/WordPress/mcp-adapter#using-mcp-adapter-in-your-plugin.
- if ( class_exists( 'WP\\MCP\\Core\\McpAdapter' ) ) {
- \WP\MCP\Core\McpAdapter::instance();
- }
-}
// Load shared classes, if they have not been included by another Kit Plugin.
if ( ! trait_exists( 'ConvertKit_API_Traits' ) && ! trait_exists( 'ConvertKit_API\ConvertKit_API_Traits' ) ) {
require_once CONVERTKIT_PLUGIN_PATH . '/vendor/convertkit/convertkit-wordpress-libraries/src/class-convertkit-api-traits.php';
@@ -89,6 +77,7 @@
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-resource-tags.php';
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-settings.php';
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-settings-broadcasts.php';
+require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-settings-mcp.php';
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-settings-restrict-content.php';
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-setup.php';
require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-shortcodes.php';
@@ -150,6 +139,7 @@
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-broadcasts.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-form-entries.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-general.php';
+require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-mcp.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-oauth.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-restrict-content.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-tools.php';