Files
2026-04-10 11:32:42 +02:00

273 lines
10 KiB
PHP

<?php
/**
* Plugin Name: Royal MCP
* Plugin URI: https://royalplugins.com/support/royal-mcp/
* Description: Integrate Model Context Protocol (MCP) servers with WordPress to enable LLM interactions with your site
* Version: 1.4.3
* Author: Royal Plugins
* Author URI: https://www.royalplugins.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Domain Path: /languages
* Text Domain: royal-mcp
* Requires at least: 5.8
* Requires PHP: 7.4
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('ROYAL_MCP_VERSION', '1.4.3');
define('ROYAL_MCP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('ROYAL_MCP_PLUGIN_URL', plugin_dir_url(__FILE__));
define('ROYAL_MCP_PLUGIN_FILE', __FILE__);
// Autoloader
spl_autoload_register(function ($class) {
$prefix = 'Royal_MCP\\';
$base_dir = ROYAL_MCP_PLUGIN_DIR . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
/**
* Main plugin class
*/
class Royal_MCP_Plugin {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
add_action('plugins_loaded', [$this, 'init']);
add_action('rest_api_init', [$this, 'register_rest_routes']);
add_action('rest_api_init', [$this, 'register_mcp_endpoint']);
// OAuth 2.0 endpoints (served at domain root, not under /wp-json/).
add_action('init', [$this, 'register_oauth_rewrites']);
add_filter('query_vars', [$this, 'register_oauth_query_vars']);
add_action('parse_request', [$this, 'handle_oauth_request']);
// Scheduled token cleanup.
add_action('royal_mcp_token_cleanup', [\Royal_MCP\OAuth\Token_Store::class, 'cleanup_expired']);
// Add plugin action links (Settings, Docs)
add_filter('plugin_action_links_' . plugin_basename(__FILE__), [$this, 'add_action_links']);
}
/**
* Add action links to plugins page
*/
public function add_action_links($links) {
$plugin_links = [
'<a href="' . admin_url('admin.php?page=royal-mcp') . '">' . __('Settings', 'royal-mcp') . '</a>',
'<a href="https://royalplugins.com/support/royal-mcp/" target="_blank">' . __('Docs', 'royal-mcp') . '</a>',
];
return array_merge($plugin_links, $links);
}
public function activate() {
// Create necessary database tables and options
$this->create_tables();
// Create OAuth tables.
if ( class_exists( '\Royal_MCP\OAuth\Token_Store' ) ) {
\Royal_MCP\OAuth\Token_Store::create_tables();
} else {
// Force-load if autoloader hasn't fired yet (WP 7.0+ activation flow)
$token_store_file = ROYAL_MCP_PLUGIN_DIR . 'includes/OAuth/Token_Store.php';
if ( file_exists( $token_store_file ) ) {
require_once $token_store_file;
\Royal_MCP\OAuth\Token_Store::create_tables();
}
}
// Set default options
add_option('royal_mcp_settings', [
'enabled' => false,
'platforms' => [],
'mcp_servers' => [],
'api_key' => wp_generate_password(32, false),
]);
// Register OAuth rewrite rules before flushing.
$this->register_oauth_rewrites();
// Flush rewrite rules
flush_rewrite_rules();
// Schedule daily token cleanup.
if ( ! wp_next_scheduled( 'royal_mcp_token_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'royal_mcp_token_cleanup' );
}
}
public function deactivate() {
// Clear scheduled events.
wp_clear_scheduled_hook( 'royal_mcp_token_cleanup' );
// Flush rewrite rules
flush_rewrite_rules();
}
private function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'royal_mcp_logs';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
mcp_server varchar(255) NOT NULL,
action varchar(100) NOT NULL,
request_data longtext,
response_data longtext,
status varchar(50) NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp),
KEY mcp_server (mcp_server)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
/* ------------------------------------------------------------------
* OAuth 2.0 rewrite rules & request handling
* ----------------------------------------------------------------*/
/**
* Register rewrite rules for OAuth endpoints at domain root.
*/
public function register_oauth_rewrites() {
add_rewrite_rule( '\.well-known/oauth-protected-resource(/.*)?$', 'index.php?royal_mcp_oauth=protected_resource', 'top' );
add_rewrite_rule( '\.well-known/oauth-authorization-server$', 'index.php?royal_mcp_oauth=metadata', 'top' );
add_rewrite_rule( 'authorize$', 'index.php?royal_mcp_oauth=authorize', 'top' );
add_rewrite_rule( 'token$', 'index.php?royal_mcp_oauth=token', 'top' );
add_rewrite_rule( 'register$', 'index.php?royal_mcp_oauth=register', 'top' );
}
/**
* Register the query variable used by OAuth rewrite rules.
*/
public function register_oauth_query_vars( $vars ) {
$vars[] = 'royal_mcp_oauth';
return $vars;
}
/**
* Intercept requests that match OAuth rewrite rules and dispatch to OAuth\Server.
*/
public function handle_oauth_request( $wp ) {
if ( empty( $wp->query_vars['royal_mcp_oauth'] ) ) {
return;
}
// Only handle OAuth if plugin is enabled (allow metadata always for discovery).
$action = sanitize_text_field( $wp->query_vars['royal_mcp_oauth'] );
if ( 'metadata' !== $action ) {
$settings = get_option( 'royal_mcp_settings', [] );
if ( empty( $settings['enabled'] ) ) {
status_header( 503 );
header( 'Content-Type: application/json' );
echo wp_json_encode( [ 'error' => 'server_error', 'error_description' => 'Royal MCP is currently disabled.' ] );
exit;
}
}
$oauth_server = new Royal_MCP\OAuth\Server();
$oauth_server->dispatch( $action );
// dispatch() calls exit, but just in case:
exit;
}
public function init() {
// Text domain is automatically loaded by WordPress 4.6+ for plugins hosted on WordPress.org
// No need to call load_plugin_textdomain() manually
// Initialize components
if (is_admin()) {
new Royal_MCP\Admin\Settings_Page();
}
}
public function register_rest_routes() {
$api = new Royal_MCP\API\REST_Controller();
$api->register_routes();
}
public function register_mcp_endpoint() {
$server = new Royal_MCP\MCP\Server();
// NEW: Streamable HTTP endpoint (2025-03-26 spec)
// Single endpoint for all MCP communication - no SSE connection needed
// MCP protocol requires public REST endpoints — auth enforced inside
// Server::validate_auth() on every request (API key or Bearer token).
// @security-ignore WP-AUTH-001 — verified: auth on all code paths in Server.php
register_rest_route('royal-mcp/v1', '/mcp', [
'methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
'callback' => [$server, 'handle_mcp'],
'permission_callback' => '__return_true', // @security-ignore — auth in validate_auth()
]);
// Also register at namespace root path — Claude Desktop may post to /wp-json/royal-mcp/v1
// when it strips the last path segment from the configured MCP URL.
// @security-ignore WP-AUTH-001 — same handler as above
register_rest_route('royal-mcp', '/v1', [
'methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
'callback' => [$server, 'handle_mcp'],
'permission_callback' => '__return_true', // @security-ignore — auth in validate_auth()
]);
// LEGACY: SSE endpoint (deprecated, returns redirect info)
// @security-ignore WP-AUTH-001 — deprecated, returns error message only
register_rest_route('royal-mcp/v1', '/sse', [
'methods' => 'GET',
'callback' => [$server, 'handle_sse'],
'permission_callback' => '__return_true', // @security-ignore — deprecated endpoint
]);
// LEGACY: Messages endpoint (forwards to new handler with full auth)
// @security-ignore WP-AUTH-001 — forwards to handle_mcp() which has validate_auth()
register_rest_route('royal-mcp/v1', '/messages', [
'methods' => 'POST',
'callback' => [$server, 'handle_message'],
'permission_callback' => '__return_true', // @security-ignore — auth in validate_auth()
]);
}
}
// Initialize the plugin
function royal_mcp_init() {
return Royal_MCP_Plugin::get_instance();
}
// Start the plugin
royal_mcp_init();