v2.1.0: includes/Core/AdminMenu.php
This commit is contained in:
+685
-214
@@ -1,261 +1,732 @@
|
||||
<?php
|
||||
|
||||
namespace AskTamMCP\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AdminMenu {
|
||||
|
||||
public function __construct() {
|
||||
add_action('admin_menu', [$this, 'register']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
const OPTION_DISABLED_TOOLS = 'asktam_mcp_disabled_tools';
|
||||
const SETTINGS_GROUP = 'asktam_mcp_tools_settings';
|
||||
const PAGE_SLUG = 'asktam-mcp';
|
||||
|
||||
private array $hook_suffixes = [];
|
||||
|
||||
public function init(): void {
|
||||
add_action('admin_menu', [$this, 'addMenuPages']);
|
||||
add_action('admin_init', [$this, 'registerSettings']);
|
||||
add_action('admin_post_asktam_mcp_rotate_secret', [$this, 'handleRotateSecret']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
add_action('admin_init', [$this, 'handleApiKeyActions']);
|
||||
}
|
||||
|
||||
public function addMenuPages(): void {
|
||||
$this->hook_suffixes[] = add_menu_page(
|
||||
'AskTam MCP',
|
||||
'AskTam MCP',
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
[$this, 'renderToolsPage'],
|
||||
'dashicons-rest-api',
|
||||
58
|
||||
);
|
||||
$submenus = [
|
||||
self::PAGE_SLUG => 'Tools',
|
||||
self::PAGE_SLUG . '-api-keys' => 'API Keys',
|
||||
self::PAGE_SLUG . '-activity' => 'Activity Log',
|
||||
self::PAGE_SLUG . '-settings' => 'Settings',
|
||||
];
|
||||
foreach ($submenus as $slug => $label) {
|
||||
$this->hook_suffixes[] = add_submenu_page(
|
||||
self::PAGE_SLUG, $label, $label, 'manage_options', $slug,
|
||||
[$this, 'renderPage']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function registerSettings(): void {
|
||||
register_setting(self::SETTINGS_GROUP, self::OPTION_DISABLED_TOOLS, [
|
||||
'type' => 'array',
|
||||
'default' => [],
|
||||
'sanitize_callback' => [$this, 'sanitizeDisabledTools'],
|
||||
]);
|
||||
register_setting('asktam_mcp_settings_group', 'asktam_mcp_settings', [
|
||||
'sanitize_callback' => [$this, 'sanitizeSettings'],
|
||||
'type' => 'array',
|
||||
'default' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function sanitizeSettings(mixed $input): array {
|
||||
return [
|
||||
'rate_limit' => max(1, min(300, (int) ($input['rate_limit'] ?? 60))),
|
||||
'log_requests' => !empty($input['log_requests']),
|
||||
'allow_raw_sql' => !empty($input['allow_raw_sql']),
|
||||
'wc_enabled' => !empty($input['wc_enabled']),
|
||||
'require_https' => !empty($input['require_https']),
|
||||
];
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
add_menu_page('AskTam MCP', 'AskTam MCP', 'manage_options', 'asktam-mcp', [$this, 'renderDashboard'], 'dashicons-rest-api', 80);
|
||||
add_submenu_page('asktam-mcp', 'API Keys', 'API Keys', 'manage_options', 'asktam-mcp', [$this, 'renderDashboard']);
|
||||
add_submenu_page('asktam-mcp', 'Activity Log', 'Activity Log', 'manage_options', 'asktam-mcp-log', [$this, 'renderLog']);
|
||||
add_submenu_page('asktam-mcp', 'Settings', 'Settings', 'manage_options', 'asktam-mcp-settings', [$this, 'renderSettings']);
|
||||
public function sanitizeDisabledTools($input): array {
|
||||
$enabled = is_array($input) ? array_map('sanitize_text_field', $input) : [];
|
||||
$all = $this->getAllToolSlugs();
|
||||
return array_values(array_diff($all, $enabled));
|
||||
}
|
||||
|
||||
public function enqueueAssets(string $hook): void {
|
||||
if (!str_contains($hook, 'asktam-mcp')) return;
|
||||
add_action('admin_head', [$this, 'inlineStyles']);
|
||||
if (!in_array($hook, $this->hook_suffixes, true)) return;
|
||||
wp_enqueue_style(
|
||||
'asktam-mcp-admin',
|
||||
plugins_url('assets/css/admin.css', ASKTAM_MCP_FILE),
|
||||
[],
|
||||
ASKTAM_MCP_VERSION
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'asktam-mcp-admin',
|
||||
plugins_url('assets/js/admin.js', ASKTAM_MCP_FILE),
|
||||
[],
|
||||
ASKTAM_MCP_VERSION,
|
||||
true
|
||||
);
|
||||
wp_localize_script('asktam-mcp-admin', 'asktamMcpAdmin', [
|
||||
'copied' => 'Copied!',
|
||||
'sseEndpoint' => rest_url('asktam-mcp/v1/sse'),
|
||||
'siteUrl' => site_url(),
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('asktam_mcp_admin'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function inlineStyles(): void { ?>
|
||||
<style>
|
||||
.mcp-wrap{max-width:960px}.mcp-endpoint-bar{background:#f0f6fc;border:1px solid #c0d8f0;border-radius:6px;padding:14px 18px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:24px}.mcp-endpoint-bar code{background:#fff;border:1px solid #ddd;padding:4px 10px;border-radius:4px;font-size:13px;flex:1;min-width:200px}.mcp-cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:32px}.mcp-card{background:#fff;border:1px solid #ddd;border-radius:8px;padding:20px}.mcp-card h3{margin:0 0 6px;display:flex;align-items:center;gap:8px;font-size:15px}.mcp-card p{color:#555;font-size:13px;margin:0 0 14px}.mcp-card pre{background:#1e1e2e;color:#cdd6f4;border-radius:6px;padding:12px 14px;font-size:12px;overflow-x:auto;line-height:1.6;position:relative;margin:0 0 12px}.mcp-card pre .kp{color:#f9e2af}.mcp-card pre .ku{color:#89dceb}.copy-btn{background:none;border:1px solid #ccc;border-radius:4px;padding:2px 8px;font-size:11px;cursor:pointer;position:absolute;top:8px;right:8px;color:#aaa}.copy-btn:hover{background:#333;color:#fff}.mcp-key-table{width:100%;border-collapse:collapse;font-size:13px}.mcp-key-table th{text-align:left;padding:8px 12px;background:#f6f7f7;border-bottom:2px solid #ddd}.mcp-key-table td{padding:9px 12px;border-bottom:1px solid #eee;vertical-align:middle}.mcp-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}.mcp-ok{background:#d1fae5;color:#065f46}.mcp-no{background:#fee2e2;color:#991b1b}.mcp-scope{display:inline-block;background:#ede9fe;color:#5b21b6;border-radius:3px;padding:1px 6px;font-size:11px;margin:1px}.mcp-generate-form{background:#fff;border:1px solid #ddd;border-radius:8px;padding:20px 24px;margin-bottom:28px}.mcp-form-row{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-bottom:14px}.mcp-form-row label{font-size:13px;font-weight:600;display:block;margin-bottom:4px}.mcp-form-row input[type=text]{border:1px solid #ddd;border-radius:4px;padding:7px 10px;font-size:13px;min-width:200px}.mcp-scope-checks{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.mcp-scope-checks label{font-weight:normal;display:flex;align-items:center;gap:5px;font-size:13px;cursor:pointer}.mcp-warn{background:#fff8ed;border:1px solid #f0b429;border-radius:6px;padding:12px 16px;margin-bottom:20px;font-size:13px}.mcp-danger{background:#fff0f0;border:1px solid #f5c6cb;border-radius:6px;padding:16px 20px;margin-bottom:20px}.mcp-log-table{width:100%;border-collapse:collapse;font-size:12px}.mcp-log-table th{background:#f6f7f7;padding:7px 10px;text-align:left;border-bottom:2px solid #ddd}.mcp-log-table td{padding:6px 10px;border-bottom:1px solid #eee}.mcp-dot-ok{color:#22c55e;font-weight:700}.mcp-dot-fail{color:#ef4444;font-weight:700}
|
||||
</style>
|
||||
<?php }
|
||||
private function getActiveTab(): string {
|
||||
$page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
|
||||
switch ($page) {
|
||||
case self::PAGE_SLUG . '-api-keys': return 'api-keys';
|
||||
case self::PAGE_SLUG . '-activity': return 'activity';
|
||||
case self::PAGE_SLUG . '-settings': return 'settings';
|
||||
default: return 'tools';
|
||||
}
|
||||
}
|
||||
|
||||
public function renderPage(): void {
|
||||
$tab = $this->getActiveTab();
|
||||
if ($tab === 'tools') { $this->renderToolsPage(); return; }
|
||||
if ($tab === 'api-keys') { $this->renderApiKeysPage(); return; }
|
||||
if ($tab === 'activity') { $this->renderActivityPage(); return; }
|
||||
if ($tab === 'settings') { $this->renderSettingsPage(); return; }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dashboard — API Keys
|
||||
// Header + nav
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderDashboard(): void {
|
||||
global $wpdb;
|
||||
$keys = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}asktam_mcp_keys ORDER BY created_at DESC");
|
||||
$sseUrl = rest_url('asktam-mcp/v1/sse');
|
||||
$isHttps = is_ssl();
|
||||
private function renderHeader(string $active): void {
|
||||
$registry = new \AskTamMCP\MCP\Registry();
|
||||
$total = count($registry->getToolDefinitions());
|
||||
$disabled = get_option(self::OPTION_DISABLED_TOOLS, []);
|
||||
if (!is_array($disabled)) $disabled = [];
|
||||
$enabled = $total - count($disabled);
|
||||
?>
|
||||
<div class="wrap mcp-wrap">
|
||||
<h1 style="display:flex;align-items:center;gap:10px;">
|
||||
<span class="dashicons dashicons-rest-api" style="font-size:26px;width:26px;height:26px;color:#2271b1;"></span>
|
||||
AskTam MCP <span style="font-size:12px;font-weight:400;color:#999;background:#f0f0f0;padding:2px 8px;border-radius:10px;">v<?php echo esc_html(ASKTAM_MCP_VERSION); ?></span>
|
||||
</h1>
|
||||
|
||||
<?php if (!$isHttps): ?>
|
||||
<div class="mcp-warn">⚠️ <strong>HTTPS not detected.</strong> The MCP endpoint is accessible over HTTP — tokens can be intercepted in transit. Enable SSL before using this plugin in production. <a href="<?php echo esc_url(admin_url('admin.php?page=asktam-mcp-settings')); ?>">Disable this warning →</a></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mcp-endpoint-bar">
|
||||
<strong>SSE Endpoint:</strong>
|
||||
<code id="mcp-sse-url"><?php echo esc_html($sseUrl); ?></code>
|
||||
<button class="button button-small" onclick="navigator.clipboard.writeText(document.getElementById('mcp-sse-url').textContent);this.textContent='Copied!'">Copy URL</button>
|
||||
<a href="<?php echo esc_url(rest_url('asktam-mcp/v1/info')); ?>" target="_blank" class="button button-small">Info →</a>
|
||||
<div class="wrap asktam-mcp-admin">
|
||||
<div class="asktam-mcp-header">
|
||||
<span class="asktam-mcp-header-icon">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
</span>
|
||||
<div class="asktam-mcp-header-info">
|
||||
<h2 class="asktam-mcp-header-title">
|
||||
AskTam MCP
|
||||
<span class="asktam-mcp-header-version">v<?php echo esc_html(ASKTAM_MCP_VERSION); ?></span>
|
||||
</h2>
|
||||
<p class="asktam-mcp-header-subtitle">Full AI control over WordPress via Model Context Protocol.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate key -->
|
||||
<div class="mcp-generate-form">
|
||||
<h3>Generate API Key</h3>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<?php wp_nonce_field('asktam_mcp_create_key', '_wpnonce_mcp'); ?>
|
||||
<input type="hidden" name="action" value="asktam_mcp_create_key">
|
||||
<div class="mcp-form-row">
|
||||
<div><label for="mcp-key-name">Label</label><input type="text" id="mcp-key-name" name="key_name" placeholder="e.g. Claude Desktop" required></div>
|
||||
<div>
|
||||
<label>Scopes</label>
|
||||
<div class="mcp-scope-checks">
|
||||
<label><input type="checkbox" name="scopes[]" value="read" checked> Read</label>
|
||||
<label><input type="checkbox" name="scopes[]" value="write"> Write</label>
|
||||
<label><input type="checkbox" name="scopes[]" value="admin"> Admin</label>
|
||||
<div class="asktam-mcp-stats">
|
||||
<div class="asktam-mcp-stat">
|
||||
<span class="asktam-mcp-stat-icon asktam-mcp-stat-icon--tools">
|
||||
<svg viewBox="0 0 20 20"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
|
||||
</span>
|
||||
<span class="asktam-mcp-stat-content">
|
||||
<span class="asktam-mcp-stat-value"><?php echo esc_html($total); ?></span>
|
||||
<span class="asktam-mcp-stat-label">Total Tools</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="asktam-mcp-stat">
|
||||
<span class="asktam-mcp-stat-icon asktam-mcp-stat-icon--active">
|
||||
<svg viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/></svg>
|
||||
</span>
|
||||
<span class="asktam-mcp-stat-content">
|
||||
<span class="asktam-mcp-stat-value"><?php echo esc_html($enabled); ?></span>
|
||||
<span class="asktam-mcp-stat-label">Active</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="asktam-mcp-stat">
|
||||
<span class="asktam-mcp-stat-icon asktam-mcp-stat-icon--endpoint">
|
||||
<svg viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</span>
|
||||
<span class="asktam-mcp-stat-content">
|
||||
<span class="asktam-mcp-stat-value" style="font-size:11px;word-break:break-all;"><?php echo esc_html(rest_url('asktam-mcp/v1/sse')); ?></span>
|
||||
<span class="asktam-mcp-stat-label">SSE Endpoint</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="asktam-mcp-nav nav-tab-wrapper">
|
||||
<?php
|
||||
$tabs = [
|
||||
'' => ['Tools', admin_url('admin.php?page=' . self::PAGE_SLUG)],
|
||||
'api-keys' => ['API Keys', admin_url('admin.php?page=' . self::PAGE_SLUG . '-api-keys')],
|
||||
'activity' => ['Activity Log', admin_url('admin.php?page=' . self::PAGE_SLUG . '-activity')],
|
||||
'settings' => ['Settings', admin_url('admin.php?page=' . self::PAGE_SLUG . '-settings')],
|
||||
];
|
||||
$map = ['' => 'tools', 'api-keys' => 'api-keys', 'activity' => 'activity', 'settings' => 'settings'];
|
||||
foreach ($tabs as $key => [$label, $url]) {
|
||||
$cls = ($map[$key] === $active) ? 'nav-tab nav-tab-active' : 'nav-tab';
|
||||
echo '<a href="' . esc_url($url) . '" class="' . $cls . '">' . esc_html($label) . '</a>';
|
||||
}
|
||||
?>
|
||||
</nav>
|
||||
<div class="asktam-mcp-tab-content">
|
||||
<?php
|
||||
}
|
||||
|
||||
private function renderFooter(): void {
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tools page
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderToolsPage(): void {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
$this->renderHeader('tools');
|
||||
|
||||
$all_tools = $this->getAllToolsGrouped();
|
||||
$disabled = get_option(self::OPTION_DISABLED_TOOLS, []);
|
||||
if (!is_array($disabled)) $disabled = [];
|
||||
$total = $this->getAllToolSlugs();
|
||||
$enabled_count = count($total) - count($disabled);
|
||||
?>
|
||||
<form method="post" action="options.php" id="asktam-mcp-tools-form">
|
||||
<?php settings_fields(self::SETTINGS_GROUP); ?>
|
||||
|
||||
<div class="asktam-mcp-bulk-actions">
|
||||
<button type="button" class="button asktam-mcp-enable-all">Enable All</button>
|
||||
<button type="button" class="button asktam-mcp-disable-all">Disable All</button>
|
||||
<button type="submit" class="button button-primary asktam-mcp-bulk-save">Save Changes</button>
|
||||
</div>
|
||||
|
||||
<p class="asktam-mcp-tools-summary">
|
||||
<strong><?php echo esc_html($enabled_count); ?> of <?php echo esc_html(count($total)); ?></strong> tools enabled.
|
||||
</p>
|
||||
|
||||
<?php foreach ($all_tools as $cat_id => $cat): ?>
|
||||
<?php
|
||||
$cat_total = count($cat['tools']);
|
||||
$cat_enabled = 0;
|
||||
foreach ($cat['tools'] as $slug => $tool) {
|
||||
if (!in_array($slug, $disabled, true)) $cat_enabled++;
|
||||
}
|
||||
?>
|
||||
<div class="asktam-mcp-category" data-category="<?php echo esc_attr($cat_id); ?>">
|
||||
<h2 class="asktam-mcp-category-header">
|
||||
<?php echo esc_html($cat['label']); ?>
|
||||
<span class="asktam-mcp-category-count"><?php echo esc_html($cat_enabled . ' / ' . $cat_total); ?></span>
|
||||
<span class="asktam-mcp-category-actions">
|
||||
<button type="button" class="button-link asktam-mcp-cat-enable-all">All</button>
|
||||
<span class="asktam-mcp-separator">·</span>
|
||||
<button type="button" class="button-link asktam-mcp-cat-disable-all">None</button>
|
||||
</span>
|
||||
</h2>
|
||||
<div class="asktam-mcp-tools-grid">
|
||||
<?php foreach ($cat['tools'] as $slug => $tool): ?>
|
||||
<?php $is_enabled = !in_array($slug, $disabled, true); ?>
|
||||
<label class="asktam-mcp-tool-card <?php echo $is_enabled ? 'is-enabled' : 'is-disabled'; ?>">
|
||||
<input type="checkbox"
|
||||
name="<?php echo esc_attr(self::OPTION_DISABLED_TOOLS); ?>[]"
|
||||
value="<?php echo esc_attr($slug); ?>"
|
||||
<?php checked($is_enabled); ?>
|
||||
/>
|
||||
<span class="asktam-mcp-toggle" aria-hidden="true">
|
||||
<span class="asktam-mcp-toggle-track"></span>
|
||||
</span>
|
||||
<span class="asktam-mcp-tool-info">
|
||||
<span class="asktam-mcp-tool-name">
|
||||
<?php echo esc_html($tool['label']); ?>
|
||||
<?php foreach ($tool['badges'] as $badge): ?>
|
||||
<span class="asktam-mcp-badge asktam-mcp-badge--<?php echo esc_attr($badge); ?>"><?php echo esc_html($badge); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<span class="asktam-mcp-tool-desc"><?php echo esc_html($tool['description']); ?></span>
|
||||
<code class="asktam-mcp-tool-slug"><?php echo esc_html($slug); ?></code>
|
||||
</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php submit_button('Save Changes'); ?>
|
||||
</form>
|
||||
<?php
|
||||
$this->renderFooter();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// API Keys page
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderApiKeysPage(): void {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
$this->renderHeader('api-keys');
|
||||
|
||||
global $wpdb;
|
||||
$keys = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}asktam_mcp_keys WHERE is_active=1 ORDER BY created_at DESC");
|
||||
$sse_url = rest_url('asktam-mcp/v1/sse');
|
||||
?>
|
||||
<div class="asktam-mcp-connection">
|
||||
<div class="asktam-mcp-section">
|
||||
<h2>SSE Endpoint</h2>
|
||||
<div class="asktam-mcp-endpoint">
|
||||
<code><?php echo esc_html($sse_url); ?></code>
|
||||
<textarea class="asktam-mcp-copy-source" id="sse-url-copy"><?php echo esc_textarea($sse_url); ?></textarea>
|
||||
<button type="button" class="button asktam-mcp-copy-btn" data-target="sse-url-copy">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="asktam-mcp-section">
|
||||
<h2>Generate API Key</h2>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('asktam_mcp_create_key', 'asktam_mcp_nonce'); ?>
|
||||
<input type="hidden" name="asktam_mcp_action" value="create_key">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="key_label">Label</label></th>
|
||||
<td><input type="text" id="key_label" name="key_label" placeholder="e.g. Claude Desktop" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Scopes</th>
|
||||
<td>
|
||||
<label><input type="checkbox" name="scopes[]" value="read" checked> Read</label>
|
||||
<label><input type="checkbox" name="scopes[]" value="write"> Write</label>
|
||||
<label><input type="checkbox" name="scopes[]" value="admin"> Admin</label>
|
||||
<label><input type="checkbox" name="scopes[]" value="woocommerce"> WooCommerce</label>
|
||||
</div>
|
||||
</div>
|
||||
<div><?php submit_button('Generate', 'primary', 'submit', false, ['style' => 'height:34px;margin:0;']); ?></div>
|
||||
</div>
|
||||
<p style="margin:0;color:#888;font-size:12px;"><strong>Read</strong> = list/get · <strong>Write</strong> = create/update/delete content · <strong>Admin</strong> = full access (supersedes all) · <strong>WooCommerce</strong> = product/order mutations</p>
|
||||
<p class="description">Read = list/get · Write = create/update/delete · Admin = full access · WooCommerce = WC mutations</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button('Generate', 'primary', 'submit', false); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Integration cards -->
|
||||
<div class="mcp-cards">
|
||||
<div class="mcp-card">
|
||||
<h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="12" fill="#D97757"/><path d="M7.5 16.5L12 7.5L16.5 16.5" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 13.5H15" stroke="white" stroke-width="1.8" stroke-linecap="round"/></svg> Claude (claude.ai / Claude Desktop)</h3>
|
||||
<p>Add as custom connector. URL + your API key as OAuth Client ID and secret.</p>
|
||||
<strong style="font-size:12px;color:#555;display:block;margin-bottom:4px;">claude.ai → Settings → Connectors → Add custom connector</strong>
|
||||
<pre style="font-size:12px;background:#f6f7f7;color:#333;border:1px solid #ddd;padding:10px 12px;border-radius:5px;margin-bottom:12px;">URL: <span style="color:#2271b1;"><?php echo esc_html($sseUrl); ?></span>
|
||||
OAuth Client ID: <span style="color:#b45309;">YOUR_API_KEY</span>
|
||||
Client Secret: <span style="color:#b45309;">YOUR_API_KEY</span> (same)</pre>
|
||||
<strong style="font-size:12px;color:#555;display:block;margin-bottom:4px;">Claude Desktop → claude_desktop_config.json</strong>
|
||||
<pre id="claude-desktop" style="position:relative;">{
|
||||
"mcpServers": {
|
||||
"wordpress": {
|
||||
"url": "<span class="ku"><?php echo esc_html($sseUrl); ?></span>",
|
||||
"headers": { "Authorization": "Bearer <span class="kp">YOUR_API_KEY</span>" }
|
||||
}
|
||||
}
|
||||
}
|
||||
<button class="copy-btn" onclick="copyPre('claude-desktop',this)">Copy</button></pre>
|
||||
</div>
|
||||
<div class="mcp-card">
|
||||
<h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="12" fill="#10a37f"/><path d="M12 6.5C9 6.5 7 8.8 7 11.5C7 13.2 7.9 14.7 9.2 15.6L8.5 17.5H15.5L14.8 15.6C16.1 14.7 17 13.2 17 11.5C17 8.8 15 6.5 12 6.5Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/><line x1="10" y1="12" x2="14" y2="12" stroke="white" stroke-width="1.5" stroke-linecap="round"/></svg> OpenAI (GPT Actions)</h3>
|
||||
<p>Import OpenAPI schema. Auth via API Key in <code>X-MCP-API-Key</code> header.</p>
|
||||
<strong style="font-size:12px;color:#555;display:block;margin-bottom:4px;">GPT Builder → Actions → Import from URL</strong>
|
||||
<pre id="openai-url" style="position:relative;"><span class="ku"><?php echo esc_html(rest_url('asktam-mcp/v1/openapi.json')); ?></span>
|
||||
<button class="copy-btn" onclick="copyPre('openai-url',this)">Copy</button></pre>
|
||||
<strong style="font-size:12px;color:#555;display:block;margin-bottom:4px;">Authentication → API Key → Header name: <code>X-MCP-API-Key</code></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keys table -->
|
||||
<h2 style="margin-bottom:12px;">API Keys</h2>
|
||||
<?php if ($keys): ?>
|
||||
<table class="mcp-key-table">
|
||||
<thead><tr><th>Label</th><th>Key (masked)</th><th>Scopes</th><th>Created</th><th>Last Used</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($keys as $k): ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html($k->name); ?></strong></td>
|
||||
<td>
|
||||
<code style="font-size:12px;background:#f6f7f7;padding:2px 8px;border-radius:3px;"><?php echo esc_html(substr($k->api_key,0,8).'•••••••••••••'.substr($k->api_key,-4)); ?></code>
|
||||
<button onclick="navigator.clipboard.writeText('<?php echo esc_js($k->api_key); ?>');this.textContent='✓';setTimeout(()=>this.textContent='Copy',2000)" class="button button-small" style="margin-left:6px;">Copy</button>
|
||||
</td>
|
||||
<td><?php foreach (array_map('trim',explode(',',$k->scopes)) as $s): ?><span class="mcp-scope"><?php echo esc_html($s); ?></span><?php endforeach; ?></td>
|
||||
<td style="color:#666;font-size:12px;"><?php echo esc_html(date('Y-m-d',strtotime($k->created_at))); ?></td>
|
||||
<td style="color:#666;font-size:12px;"><?php echo $k->last_used ? esc_html(date('Y-m-d H:i',strtotime($k->last_used))) : '—'; ?></td>
|
||||
<td><span class="mcp-badge <?php echo $k->is_active ? 'mcp-ok' : 'mcp-no'; ?>"><?php echo $k->is_active ? 'Active' : 'Inactive'; ?></span></td>
|
||||
<td>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
|
||||
<?php wp_nonce_field('asktam_mcp_revoke_'.$k->id,'_wpnonce_mcp'); ?>
|
||||
<input type="hidden" name="action" value="asktam_mcp_revoke_key">
|
||||
<input type="hidden" name="key_id" value="<?php echo (int)$k->id; ?>">
|
||||
<button type="submit" class="button button-small" onclick="return confirm('Revoke key for <?php echo esc_js($k->name); ?>?')" <?php echo !$k->is_active?'disabled':''; ?>>Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?><p style="color:#888;">No keys yet. Generate one above.</p><?php endif; ?>
|
||||
|
||||
<!-- Danger zone: rotate secret -->
|
||||
<h2 style="margin-top:32px;margin-bottom:12px;">Security</h2>
|
||||
<div class="mcp-danger">
|
||||
<strong>⚠ Rotate JWT Secret</strong>
|
||||
<p style="margin:8px 0;font-size:13px;color:#555;">Immediately invalidates <strong>all active JWT tokens</strong> across all connected AI clients. They will need to re-authenticate. Use if you suspect a token leak.</p>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('This will disconnect all active AI clients immediately. Confirm?')">
|
||||
<?php wp_nonce_field('asktam_mcp_rotate_secret','_wpnonce_rotate'); ?>
|
||||
<input type="hidden" name="action" value="asktam_mcp_rotate_secret">
|
||||
<button type="submit" class="button" style="background:#dc3545;color:#fff;border-color:#dc3545;">Rotate JWT Secret Now</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function copyPre(id,btn){const el=document.getElementById(id);const text=el.innerText.replace(/Copy$/,'').trim();navigator.clipboard.writeText(text);btn.textContent='✓ Copied';setTimeout(()=>btn.textContent='Copy',2000);}
|
||||
</script>
|
||||
<div class="asktam-mcp-section">
|
||||
<h2>Connection Instructions</h2>
|
||||
<p class="description">Add as custom connector in claude.ai → Settings → Connectors → Add custom connector</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Activity Log
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderLog(): void {
|
||||
global $wpdb;
|
||||
$page = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$perPage = 50;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}asktam_mcp_log");
|
||||
$logs = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT l.*, k.name as key_name
|
||||
FROM {$wpdb->prefix}asktam_mcp_log l
|
||||
LEFT JOIN {$wpdb->prefix}asktam_mcp_keys k ON l.key_id = k.id
|
||||
ORDER BY l.created_at DESC LIMIT %d OFFSET %d",
|
||||
$perPage, $offset
|
||||
));
|
||||
$pages = ceil($total / $perPage);
|
||||
$first_key = $keys[0];
|
||||
$preview = 'mcp_' . substr(md5($first_key->api_key), 0, 4) . '••••••••••••' . substr($first_key->api_key, -4);
|
||||
$sse_url_escaped = esc_attr($sse_url);
|
||||
$json = json_encode([
|
||||
'mcpServers' => [
|
||||
'wordpress' => [
|
||||
'url' => $sse_url,
|
||||
'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
|
||||
]
|
||||
]
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
?>
|
||||
<div class="wrap mcp-wrap">
|
||||
<h1>AskTam MCP — Activity Log</h1>
|
||||
<p style="color:#888;margin-bottom:16px;"><?php echo number_format($total); ?> total entries · 90-day retention</p>
|
||||
|
||||
<?php if ($logs): ?>
|
||||
<table class="mcp-log-table">
|
||||
<thead><tr><th>Time</th><th>Key</th><th>Tool</th><th>IP</th><th>Duration</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs as $l): ?>
|
||||
<tr>
|
||||
<td style="color:#666;"><?php echo esc_html(date('Y-m-d H:i:s', strtotime($l->created_at))); ?></td>
|
||||
<td><?php echo esc_html($l->key_name ?? "key #{$l->key_id}"); ?></td>
|
||||
<td><code><?php echo esc_html($l->tool); ?></code></td>
|
||||
<td style="color:#888;font-size:11px;"><?php echo esc_html($l->ip); ?></td>
|
||||
<td style="color:#888;"><?php echo (int)$l->duration_ms; ?>ms</td>
|
||||
<td><?php echo $l->status ? '<span class="mcp-dot-ok">✓</span>' : '<span class="mcp-dot-fail">✗</span>'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php if ($pages > 1): ?>
|
||||
<div style="margin-top:16px;">
|
||||
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||
<a href="<?php echo esc_url(add_query_arg('paged', $i)); ?>" class="button button-small" <?php echo $i === $page ? 'style="font-weight:700;"' : ''; ?>><?php echo $i; ?></a>
|
||||
<?php endfor; ?>
|
||||
<div class="asktam-mcp-config-card">
|
||||
<div class="asktam-mcp-config-card-header">
|
||||
<span class="asktam-mcp-config-card-title">claude.ai / Claude Desktop → claude_desktop_config.json</span>
|
||||
<textarea class="asktam-mcp-copy-source" id="desktop-config-copy"><?php echo esc_textarea($json); ?></textarea>
|
||||
<button type="button" class="button asktam-mcp-copy-btn" data-target="desktop-config-copy">Copy</button>
|
||||
</div>
|
||||
<pre><code><?php echo esc_html($json); ?></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?><p style="color:#888;">No activity logged yet.</p><?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Settings
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderSettings(): void {
|
||||
$s = get_option('asktam_mcp_settings', []);
|
||||
<div class="asktam-mcp-section">
|
||||
<h2>API Keys</h2>
|
||||
<?php if ($keys): ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr><th>Label</th><th>Key (masked)</th><th>Scopes</th><th>Created</th><th>Last Used</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($keys as $key):
|
||||
$masked = 'mcp_' . substr(md5($key->api_key), 0, 4) . '••••••••••••' . substr($key->api_key, -4);
|
||||
?>
|
||||
<div class="wrap mcp-wrap">
|
||||
<h1>AskTam MCP — Settings</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php settings_fields('asktam_mcp_settings_group'); do_settings_sections('asktam_mcp_settings_group'); ?>
|
||||
<table class="form-table" style="max-width:600px;">
|
||||
<tr><th>Rate Limit (req/min per key)</th><td><input type="number" name="asktam_mcp_settings[rate_limit]" value="<?php echo (int)($s['rate_limit']??60); ?>" min="1" max="300" class="small-text"></td></tr>
|
||||
<tr><th>Require HTTPS</th><td><input type="checkbox" name="asktam_mcp_settings[require_https]" value="1" <?php checked($s['require_https']??true); ?>><p class="description">Block all MCP requests over HTTP.</p></td></tr>
|
||||
<tr><th>Log Requests</th><td><input type="checkbox" name="asktam_mcp_settings[log_requests]" value="1" <?php checked($s['log_requests']??true); ?>></td></tr>
|
||||
<tr><th>Allow Raw SQL <span style="font-weight:400;color:#888;">(admin scope)</span></th><td><input type="checkbox" name="asktam_mcp_settings[allow_raw_sql]" value="1" <?php checked($s['allow_raw_sql']??false); ?>><p class="description">Allows admin-scoped keys to run SELECT queries.</p></td></tr>
|
||||
<tr><th>WooCommerce Tools</th><td><input type="checkbox" name="asktam_mcp_settings[wc_enabled]" value="1" <?php checked($s['wc_enabled']??class_exists('WooCommerce')); ?> <?php echo !class_exists('WooCommerce')?'disabled':''; ?>><p class="description"><?php echo class_exists('WooCommerce')?'WooCommerce detected ✓':'WooCommerce not active'; ?></p></td></tr>
|
||||
<tr>
|
||||
<td><?php echo esc_html($key->name); ?></td>
|
||||
<td>
|
||||
<code><?php echo esc_html($masked); ?></code>
|
||||
<textarea class="asktam-mcp-copy-source" id="key-copy-<?php echo (int)$key->id; ?>"><?php echo esc_textarea($key->api_key); ?></textarea>
|
||||
<button type="button" class="button-small asktam-mcp-copy-btn" data-target="key-copy-<?php echo (int)$key->id; ?>">Copy</button>
|
||||
</td>
|
||||
<td><?php echo esc_html(implode(', ', array_filter(explode(',', $key->scopes)))); ?></td>
|
||||
<td><?php echo esc_html(date('Y-m-d', strtotime($key->created_at))); ?></td>
|
||||
<td><?php echo $key->last_used ? esc_html(date('Y-m-d', strtotime($key->last_used))) : '—'; ?></td>
|
||||
<td>
|
||||
<form method="post" style="display:inline">
|
||||
<?php wp_nonce_field('asktam_mcp_delete_key_' . $key->id, 'asktam_mcp_nonce'); ?>
|
||||
<input type="hidden" name="asktam_mcp_action" value="delete_key">
|
||||
<input type="hidden" name="key_id" value="<?php echo (int)$key->id; ?>">
|
||||
<button type="submit" class="button-link" onclick="return confirm('Delete this key?')">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php submit_button(); ?>
|
||||
<?php else: ?>
|
||||
<p>No API keys yet. Generate one above.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="asktam-mcp-section">
|
||||
<h2>Security</h2>
|
||||
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:6px;padding:16px;">
|
||||
<strong>⚠ Rotate JWT Secret</strong>
|
||||
<p>Immediately invalidates <strong>all active JWT tokens</strong> across all connected AI clients.</p>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('asktam_mcp_rotate_jwt', 'asktam_mcp_nonce'); ?>
|
||||
<input type="hidden" name="asktam_mcp_action" value="rotate_jwt">
|
||||
<button type="submit" class="button" style="background:#dc3545;border-color:#dc3545;color:#fff" onclick="return confirm('Rotate JWT secret? All clients will need to re-authenticate.')">Rotate JWT Secret Now</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$this->renderFooter();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rotate secret action
|
||||
// Activity Log page
|
||||
// -------------------------------------------------------------------------
|
||||
public function handleRotateSecret(): void {
|
||||
if (!current_user_can('manage_options')) wp_die('Unauthorized');
|
||||
check_admin_referer('asktam_mcp_rotate_secret', '_wpnonce_rotate');
|
||||
TokenManager::rotateSecret();
|
||||
wp_redirect(add_query_arg(['mcp_notice' => 'rotated'], admin_url('admin.php?page=asktam-mcp')));
|
||||
public function renderActivityPage(): void {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
$this->renderHeader('activity');
|
||||
|
||||
global $wpdb;
|
||||
$logs = $wpdb->get_results(
|
||||
"SELECT l.*, k.name as key_name FROM {$wpdb->prefix}asktam_mcp_log l
|
||||
LEFT JOIN {$wpdb->prefix}asktam_mcp_keys k ON l.key_id = k.id
|
||||
ORDER BY l.created_at DESC LIMIT 200"
|
||||
);
|
||||
?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr><th>Time</th><th>Key</th><th>Tool</th><th>Method</th><th>Status</th><th>Duration</th><th>IP</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($logs): foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html($log->created_at); ?></td>
|
||||
<td><?php echo esc_html($log->key_name ?: "key #{$log->key_id}"); ?></td>
|
||||
<td><code><?php echo esc_html($log->tool); ?></code></td>
|
||||
<td><?php echo esc_html($log->method); ?></td>
|
||||
<td>
|
||||
<span style="color:<?php echo $log->status ? '#10b981' : '#ef4444'; ?>">
|
||||
<?php echo $log->status ? '✓' : '✗'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo esc_html($log->duration); ?>ms</td>
|
||||
<td><?php echo esc_html($log->ip); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; else: ?>
|
||||
<tr><td colspan="7">No activity yet.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
$this->renderFooter();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Settings page
|
||||
// -------------------------------------------------------------------------
|
||||
public function renderSettingsPage(): void {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
$this->renderHeader('settings');
|
||||
$settings = get_option('asktam_mcp_settings', []);
|
||||
?>
|
||||
<form method="post" action="options.php" style="max-width:600px">
|
||||
<?php settings_fields('asktam_mcp_settings_group'); ?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th>Rate Limit (req/min per key)</th>
|
||||
<td><input type="number" name="asktam_mcp_settings[rate_limit]" value="<?php echo esc_attr($settings['rate_limit'] ?? 60); ?>" min="1" max="600"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Require HTTPS</th>
|
||||
<td>
|
||||
<input type="checkbox" name="asktam_mcp_settings[require_https]" value="1" <?php checked(!empty($settings['require_https'])); ?>>
|
||||
<p class="description">Block all MCP requests over HTTP.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Log Requests</th>
|
||||
<td><input type="checkbox" name="asktam_mcp_settings[log_requests]" value="1" <?php checked(!empty($settings['log_requests'])); ?>></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Allow Raw SQL <small>(admin scope)</small></th>
|
||||
<td>
|
||||
<input type="checkbox" name="asktam_mcp_settings[allow_raw_sql]" value="1" <?php checked(!empty($settings['allow_raw_sql'])); ?>>
|
||||
<p class="description">Allows admin-scoped keys to run SELECT queries.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>WooCommerce Tools</th>
|
||||
<td>
|
||||
<input type="checkbox" name="asktam_mcp_settings[wc_enabled]" value="1" <?php checked(!empty($settings['wc_enabled'])); ?>>
|
||||
<?php if (class_exists('WooCommerce')): ?><p class="description">WooCommerce detected ✓</p><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button('Save Changes'); ?>
|
||||
</form>
|
||||
<?php
|
||||
$this->renderFooter();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Handle form actions
|
||||
// -------------------------------------------------------------------------
|
||||
public function handleApiKeyActions(): void {
|
||||
if (empty($_POST['asktam_mcp_action'])) return;
|
||||
if (!current_user_can('manage_options')) return;
|
||||
|
||||
$action = sanitize_text_field($_POST['asktam_mcp_action']);
|
||||
|
||||
if ($action === 'create_key') {
|
||||
if (!wp_verify_nonce($_POST['asktam_mcp_nonce'] ?? '', 'asktam_mcp_create_key')) {
|
||||
wp_die('Invalid nonce');
|
||||
}
|
||||
global $wpdb;
|
||||
$label = sanitize_text_field($_POST['key_label'] ?? 'API Key');
|
||||
$scopes = isset($_POST['scopes']) && is_array($_POST['scopes'])
|
||||
? implode(',', array_map('sanitize_text_field', $_POST['scopes']))
|
||||
: 'read';
|
||||
$key = 'mcp_' . bin2hex(random_bytes(20));
|
||||
$wpdb->insert("{$wpdb->prefix}asktam_mcp_keys", [
|
||||
'name' => $label,
|
||||
'api_key' => $key,
|
||||
'scopes' => $scopes,
|
||||
'created_at' => current_time('mysql'),
|
||||
'is_active' => 1,
|
||||
]);
|
||||
wp_redirect(admin_url('admin.php?page=' . self::PAGE_SLUG . '-api-keys&created=1'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_key') {
|
||||
$key_id = (int)($_POST['key_id'] ?? 0);
|
||||
if (!wp_verify_nonce($_POST['asktam_mcp_nonce'] ?? '', 'asktam_mcp_delete_key_' . $key_id)) {
|
||||
wp_die('Invalid nonce');
|
||||
}
|
||||
global $wpdb;
|
||||
$wpdb->update("{$wpdb->prefix}asktam_mcp_keys", ['is_active' => 0], ['id' => $key_id]);
|
||||
wp_redirect(admin_url('admin.php?page=' . self::PAGE_SLUG . '-api-keys&deleted=1'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'rotate_jwt') {
|
||||
if (!wp_verify_nonce($_POST['asktam_mcp_nonce'] ?? '', 'asktam_mcp_rotate_jwt')) {
|
||||
wp_die('Invalid nonce');
|
||||
}
|
||||
update_option('asktam_mcp_jwt_secret', bin2hex(random_bytes(32)));
|
||||
wp_redirect(admin_url('admin.php?page=' . self::PAGE_SLUG . '-api-keys&rotated=1'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tool catalog — all 93 tools grouped by category
|
||||
// -------------------------------------------------------------------------
|
||||
public function getAllToolsGrouped(): array {
|
||||
return [
|
||||
'posts' => [
|
||||
'label' => 'Posts',
|
||||
'tools' => [
|
||||
'list_posts' => ['label'=>'List Posts', 'description'=>'List WordPress posts with filtering by status, author, category, tag, date range, search, and CPT.', 'badges'=>['read']],
|
||||
'get_post' => ['label'=>'Get Post', 'description'=>'Get full details of a post including content, meta, taxonomies, and featured image.', 'badges'=>['read']],
|
||||
'create_post' => ['label'=>'Create Post', 'description'=>'Create a new WordPress post.', 'badges'=>['write']],
|
||||
'update_post' => ['label'=>'Update Post', 'description'=>'Update an existing WordPress post.', 'badges'=>['write']],
|
||||
'delete_post' => ['label'=>'Delete Post', 'description'=>'Delete or trash a WordPress post.', 'badges'=>['write','destructive']],
|
||||
'get_post_meta' => ['label'=>'Get Post Meta', 'description'=>'Get post meta data.', 'badges'=>['read']],
|
||||
'update_post_meta' => ['label'=>'Update Post Meta', 'description'=>'Update post meta data.', 'badges'=>['write']],
|
||||
'set_featured_image'=>['label'=>'Set Featured Image','description'=>'Set or replace the featured image on a post or page.', 'badges'=>['write']],
|
||||
'count_posts' => ['label'=>'Count Posts', 'description'=>'Get post counts by status.', 'badges'=>['read']],
|
||||
],
|
||||
],
|
||||
'pages' => [
|
||||
'label' => 'Pages',
|
||||
'tools' => [
|
||||
'list_pages' => ['label'=>'List Pages', 'description'=>'List WordPress pages.', 'badges'=>['read']],
|
||||
'get_page' => ['label'=>'Get Page', 'description'=>'Get a single WordPress page.', 'badges'=>['read']],
|
||||
'create_page' => ['label'=>'Create Page', 'description'=>'Create a new WordPress page.', 'badges'=>['write']],
|
||||
'update_page' => ['label'=>'Update Page', 'description'=>'Update an existing WordPress page.', 'badges'=>['write']],
|
||||
'delete_page' => ['label'=>'Delete Page', 'description'=>'Delete or trash a WordPress page.', 'badges'=>['write','destructive']],
|
||||
],
|
||||
],
|
||||
'media' => [
|
||||
'label' => 'Media',
|
||||
'tools' => [
|
||||
'list_media' => ['label'=>'List Media', 'description'=>'List WordPress media library items.', 'badges'=>['read']],
|
||||
'get_media' => ['label'=>'Get Media', 'description'=>'Get a single media item by ID.', 'badges'=>['read']],
|
||||
'upload_media' => ['label'=>'Upload Media', 'description'=>'Upload a media file from a public URL.', 'badges'=>['write']],
|
||||
'update_media' => ['label'=>'Update Media', 'description'=>'Update media metadata: alt text, title, caption, description.', 'badges'=>['write']],
|
||||
'delete_media' => ['label'=>'Delete Media', 'description'=>'Delete a media item permanently.', 'badges'=>['write','destructive']],
|
||||
'count_media' => ['label'=>'Count Media', 'description'=>'Get media counts by type.', 'badges'=>['read']],
|
||||
],
|
||||
],
|
||||
'users' => [
|
||||
'label' => 'Users',
|
||||
'tools' => [
|
||||
'list_users' => ['label'=>'List Users', 'description'=>'List WordPress users.', 'badges'=>['read']],
|
||||
'get_user' => ['label'=>'Get User', 'description'=>'Get a single user by ID.', 'badges'=>['read']],
|
||||
'create_user' => ['label'=>'Create User', 'description'=>'Create a new WordPress user.', 'badges'=>['admin']],
|
||||
'update_user' => ['label'=>'Update User', 'description'=>'Update an existing WordPress user.', 'badges'=>['admin']],
|
||||
'delete_user' => ['label'=>'Delete User', 'description'=>'Delete a WordPress user.', 'badges'=>['admin','destructive']],
|
||||
],
|
||||
],
|
||||
'taxonomy' => [
|
||||
'label' => 'Taxonomy & Terms',
|
||||
'tools' => [
|
||||
'list_terms' => ['label'=>'List Terms', 'description'=>'List terms in any taxonomy (category, tag, or custom).', 'badges'=>['read']],
|
||||
'create_term' => ['label'=>'Create Term', 'description'=>'Create a new term in a taxonomy.', 'badges'=>['write']],
|
||||
'update_term' => ['label'=>'Update Term', 'description'=>'Update an existing term.', 'badges'=>['write']],
|
||||
'delete_term' => ['label'=>'Delete Term', 'description'=>'Delete a term from a taxonomy.', 'badges'=>['write','destructive']],
|
||||
'count_terms' => ['label'=>'Count Terms', 'description'=>'Get term counts in a taxonomy.', 'badges'=>['read']],
|
||||
],
|
||||
],
|
||||
'options' => [
|
||||
'label' => 'Options',
|
||||
'tools' => [
|
||||
'get_option' => ['label'=>'Get Option', 'description'=>'Get a WordPress option value by key.', 'badges'=>['read']],
|
||||
'update_option' => ['label'=>'Update Option', 'description'=>'Update a WordPress option.', 'badges'=>['admin']],
|
||||
'list_options' => ['label'=>'List Options', 'description'=>'List WordPress options matching a prefix.', 'badges'=>['read']],
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'label' => 'Plugins',
|
||||
'tools' => [
|
||||
'list_plugins' => ['label'=>'List Plugins', 'description'=>'List all installed WordPress plugins with their status.', 'badges'=>['read']],
|
||||
'activate_plugin' => ['label'=>'Activate Plugin', 'description'=>'Activate a WordPress plugin by its slug/file path.', 'badges'=>['admin']],
|
||||
'deactivate_plugin' => ['label'=>'Deactivate Plugin', 'description'=>'Deactivate a WordPress plugin.', 'badges'=>['admin']],
|
||||
],
|
||||
],
|
||||
'themes' => [
|
||||
'label' => 'Themes',
|
||||
'tools' => [
|
||||
'list_themes' => ['label'=>'List Themes', 'description'=>'List all installed WordPress themes.', 'badges'=>['read']],
|
||||
'get_active_theme'=> ['label'=>'Get Active Theme', 'description'=>'Get the currently active WordPress theme details.', 'badges'=>['read']],
|
||||
'switch_theme' => ['label'=>'Switch Theme', 'description'=>'Switch the active WordPress theme by stylesheet slug.', 'badges'=>['admin']],
|
||||
],
|
||||
],
|
||||
'menus' => [
|
||||
'label' => 'Menus',
|
||||
'tools' => [
|
||||
'list_menus' => ['label'=>'List Menus', 'description'=>'List all WordPress navigation menus.', 'badges'=>['read']],
|
||||
'get_menu' => ['label'=>'Get Menu', 'description'=>'Get all items in a navigation menu by ID or slug.', 'badges'=>['read']],
|
||||
'update_menu_item'=> ['label'=>'Update Menu Item', 'description'=>'Update or create a navigation menu item.', 'badges'=>['write']],
|
||||
],
|
||||
],
|
||||
'comments' => [
|
||||
'label' => 'Comments',
|
||||
'tools' => [
|
||||
'list_comments' => ['label'=>'List Comments', 'description'=>'List WordPress comments with optional post and status filters.', 'badges'=>['read']],
|
||||
'approve_comment'=> ['label'=>'Approve Comment','description'=>'Approve a pending comment.', 'badges'=>['write']],
|
||||
'spam_comment' => ['label'=>'Spam Comment', 'description'=>'Mark a comment as spam.', 'badges'=>['write']],
|
||||
'delete_comment' => ['label'=>'Delete Comment', 'description'=>'Delete or trash a comment.', 'badges'=>['write','destructive']],
|
||||
],
|
||||
],
|
||||
'seo' => [
|
||||
'label' => 'SEO',
|
||||
'tools' => [
|
||||
'get_seo_meta' => ['label'=>'Get SEO Meta', 'description'=>'Get SEO meta for a post/page (Yoast or RankMath).', 'badges'=>['read']],
|
||||
'update_seo_meta' => ['label'=>'Update SEO Meta', 'description'=>'Update SEO meta for a post/page (Yoast or RankMath).', 'badges'=>['write']],
|
||||
],
|
||||
],
|
||||
'site' => [
|
||||
'label' => 'Site',
|
||||
'tools' => [
|
||||
'get_site_info' => ['label'=>'Get Site Info', 'description'=>'Get WordPress site information: name, URL, WP version, PHP version, active theme.', 'badges'=>['read']],
|
||||
'get_health' => ['label'=>'Get Health', 'description'=>'Run WordPress site health checks and return results.', 'badges'=>['read']],
|
||||
'run_cron' => ['label'=>'Run Cron', 'description'=>'Trigger WordPress cron manually and return executed hooks.', 'badges'=>['admin']],
|
||||
'search_content' => ['label'=>'Search Content', 'description'=>'Search across posts, pages, and custom post types.', 'badges'=>['read']],
|
||||
'get_permalink_structure'=>['label'=>'Get Permalink Structure','description'=>'Get the WordPress permalink structure.', 'badges'=>['read']],
|
||||
],
|
||||
],
|
||||
'woocommerce' => [
|
||||
'label' => 'WooCommerce',
|
||||
'tools' => [
|
||||
'wc_list_products' => ['label'=>'List Products', 'description'=>'List WooCommerce products.', 'badges'=>['read','woocommerce']],
|
||||
'wc_get_product' => ['label'=>'Get Product', 'description'=>'Get a single WooCommerce product by ID.', 'badges'=>['read','woocommerce']],
|
||||
'wc_create_product' => ['label'=>'Create Product', 'description'=>'Create a WooCommerce product.', 'badges'=>['write','woocommerce']],
|
||||
'wc_update_product' => ['label'=>'Update Product', 'description'=>'Update a WooCommerce product.', 'badges'=>['write','woocommerce']],
|
||||
'wc_list_orders' => ['label'=>'List Orders', 'description'=>'List WooCommerce orders.', 'badges'=>['read','woocommerce']],
|
||||
'wc_get_order' => ['label'=>'Get Order', 'description'=>'Get a single WooCommerce order by ID.', 'badges'=>['read','woocommerce']],
|
||||
'wc_update_order_status'=>['label'=>'Update Order Status','description'=>'Update a WooCommerce order status.', 'badges'=>['write','woocommerce']],
|
||||
'wc_list_customers' => ['label'=>'List Customers', 'description'=>'List WooCommerce customers.', 'badges'=>['read','woocommerce']],
|
||||
'wc_get_store_stats' => ['label'=>'Get Store Stats', 'description'=>'Get WooCommerce store stats: revenue, orders, products count.', 'badges'=>['read','woocommerce']],
|
||||
],
|
||||
],
|
||||
'elementor_pages' => [
|
||||
'label' => 'Elementor — Page Management',
|
||||
'tools' => [
|
||||
'elementor_create_page' => ['label'=>'Create Page', 'description'=>'Creates a new WordPress page with Elementor enabled.', 'badges'=>['write','elementor']],
|
||||
'elementor_get_page' => ['label'=>'Get Page', 'description'=>'Returns the full Elementor element tree for a page.', 'badges'=>['read','elementor']],
|
||||
'elementor_update_page_settings'=> ['label'=>'Update Page Settings','description'=>'Updates Elementor page-level settings (layout, canvas, etc).', 'badges'=>['write','elementor']],
|
||||
'elementor_delete_page_content'=> ['label'=>'Delete Page Content','description'=>'Removes all Elementor content from a page.', 'badges'=>['write','destructive','elementor']],
|
||||
'elementor_import_template' => ['label'=>'Import Template', 'description'=>'Imports an Elementor template JSON into a page.', 'badges'=>['write','elementor']],
|
||||
'elementor_export_page' => ['label'=>'Export Page', 'description'=>"Exports a page's Elementor data as JSON.", 'badges'=>['read','elementor']],
|
||||
],
|
||||
],
|
||||
'elementor_layout' => [
|
||||
'label' => 'Elementor — Layout & Structure',
|
||||
'tools' => [
|
||||
'elementor_add_container' => ['label'=>'Add Container', 'description'=>'Adds a new flexbox container to a page or inside another container.', 'badges'=>['write','elementor']],
|
||||
'elementor_update_container' => ['label'=>'Update Container', 'description'=>'Updates settings on an existing container element.', 'badges'=>['write','elementor']],
|
||||
'elementor_update_element' => ['label'=>'Update Element', 'description'=>'Updates settings on any element (widget or container) by ID.', 'badges'=>['write','elementor']],
|
||||
'elementor_remove_element' => ['label'=>'Remove Element', 'description'=>'Removes an element and all its children from the page.', 'badges'=>['write','destructive','elementor']],
|
||||
'elementor_duplicate_element'=> ['label'=>'Duplicate Element','description'=>'Creates a deep copy of an element and inserts it after the original.', 'badges'=>['write','elementor']],
|
||||
'elementor_move_element' => ['label'=>'Move Element', 'description'=>'Moves an element to a new parent or position.', 'badges'=>['write','elementor']],
|
||||
],
|
||||
],
|
||||
'elementor_widgets' => [
|
||||
'label' => 'Elementor — Widgets',
|
||||
'tools' => [
|
||||
'elementor_add_widget' => ['label'=>'Add Widget', 'description'=>'Adds any widget type to a container with full settings control.', 'badges'=>['write','elementor']],
|
||||
'elementor_update_widget'=>['label'=>'Update Widget','description'=>'Updates settings on an existing widget element.', 'badges'=>['write','elementor']],
|
||||
'elementor_add_heading' => ['label'=>'Add Heading', 'description'=>'Adds a heading widget with simplified parameters.', 'badges'=>['write','elementor']],
|
||||
'elementor_add_text' => ['label'=>'Add Text', 'description'=>'Adds a text editor widget.', 'badges'=>['write','elementor']],
|
||||
'elementor_add_image' => ['label'=>'Add Image', 'description'=>'Adds an image widget by media library ID or URL.', 'badges'=>['write','elementor']],
|
||||
'elementor_add_button' => ['label'=>'Add Button', 'description'=>'Adds a button widget with text and link.', 'badges'=>['write','elementor']],
|
||||
],
|
||||
],
|
||||
'elementor_templates' => [
|
||||
'label' => 'Elementor — Templates',
|
||||
'tools' => [
|
||||
'elementor_save_as_template' => ['label'=>'Save as Template', 'description'=>'Saves the current page content as a reusable Elementor template.', 'badges'=>['write','elementor']],
|
||||
'elementor_apply_template' => ['label'=>'Apply Template', 'description'=>'Applies a saved template to a target page.', 'badges'=>['write','elementor']],
|
||||
'elementor_list_templates' => ['label'=>'List Templates', 'description'=>'Lists all saved Elementor templates.', 'badges'=>['read','elementor']],
|
||||
],
|
||||
],
|
||||
'elementor_global' => [
|
||||
'label' => 'Elementor — Global Settings',
|
||||
'tools' => [
|
||||
'elementor_get_global_colors' => ['label'=>'Get Global Colors', 'description'=>'Returns the site-wide Elementor color palette.', 'badges'=>['read','elementor']],
|
||||
'elementor_set_global_color' => ['label'=>'Set Global Color', 'description'=>'Updates a specific global color in the Elementor palette.', 'badges'=>['write','elementor']],
|
||||
'elementor_get_global_fonts' => ['label'=>'Get Global Fonts', 'description'=>'Returns the global typography presets.', 'badges'=>['read','elementor']],
|
||||
'elementor_get_kit_settings' => ['label'=>'Get Kit Settings', 'description'=>'Returns the full Elementor kit/site settings.', 'badges'=>['read','elementor']],
|
||||
'elementor_update_kit_settings' => ['label'=>'Update Kit Settings', 'description'=>'Updates Elementor kit/site settings.', 'badges'=>['write','elementor']],
|
||||
],
|
||||
],
|
||||
'elementor_query' => [
|
||||
'label' => 'Elementor — Query & Discovery',
|
||||
'tools' => [
|
||||
'elementor_list_widgets' => ['label'=>'List Widgets', 'description'=>'Lists all widget types and names on a page.', 'badges'=>['read','elementor']],
|
||||
'elementor_get_element' => ['label'=>'Get Element', 'description'=>'Returns settings of a specific element by ID.', 'badges'=>['read','elementor']],
|
||||
'elementor_clone_page' => ['label'=>'Clone Page', 'description'=>'Clones an Elementor page to a new post.', 'badges'=>['write','elementor']],
|
||||
'elementor_replace_text' => ['label'=>'Replace Text', 'description'=>'Bulk-replaces text content across all widgets on a page.', 'badges'=>['write','elementor']],
|
||||
'elementor_get_page_outline'=> ['label'=>'Get Page Outline', 'description'=>'Returns a simplified structural outline of an Elementor page.', 'badges'=>['read','elementor']],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAllToolSlugs(): array {
|
||||
$slugs = [];
|
||||
foreach ($this->getAllToolsGrouped() as $cat) {
|
||||
foreach ($cat['tools'] as $slug => $_) {
|
||||
$slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
return $slugs;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user