feat: admin UI with API key gen + Claude + OpenAI integration cards

This commit is contained in:
2026-05-08 03:41:23 +00:00
parent 714e078160
commit 81f9f592ff
+225 -66
View File
@@ -4,36 +4,64 @@ namespace AskTamMCP\Core;
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
/**
* WP Admin menu + settings page for AskTam MCP.
*/
class AdminMenu { class AdminMenu {
public function __construct() { public function __construct() {
add_action('admin_menu', [$this, 'register']); add_action('admin_menu', [$this, 'register']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
} }
public function register(): void { public function register(): void {
add_menu_page( add_menu_page('AskTam MCP', 'AskTam MCP', 'manage_options', 'asktam-mcp', [$this, 'renderDashboard'], 'dashicons-rest-api', 80);
'AskTam MCP', add_submenu_page('asktam-mcp', 'API Keys', 'API Keys', 'manage_options', 'asktam-mcp', [$this, 'renderDashboard']);
'AskTam MCP', add_submenu_page('asktam-mcp', 'Settings', 'Settings', 'manage_options', 'asktam-mcp-settings', [$this, 'renderSettings']);
'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', 'Settings', 'Settings', 'manage_options', 'asktam-mcp-settings', [$this, 'renderSettings']);
} }
public function enqueueAssets(string $hook): void { public function enqueueAssets(string $hook): void {
if (!in_array($hook, ['toplevel_page_asktam-mcp', 'asktam-mcp_page_asktam-mcp-settings'], true)) { if (!in_array($hook, ['toplevel_page_asktam-mcp', 'asktam-mcp_page_asktam-mcp-settings'], true)) {
return; return;
} }
wp_enqueue_style('asktam-mcp-admin', ASKTAM_MCP_URL . 'assets/admin.css', [], ASKTAM_MCP_VERSION); // Inline all styles — no external file dependency needed
add_action('admin_head', [$this, 'inlineStyles']);
}
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 h3 img { width:20px; height:20px; }
.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 .key-placeholder { color:#f9e2af; }
.mcp-card pre .key-url { 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-key-table tr:hover td { background:#fafafa; }
.mcp-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }
.mcp-badge-active { background:#d1fae5; color:#065f46; }
.mcp-badge-inactive { 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-generate-form h3 { margin:0 0 16px; font-size:15px; }
.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-new-key-banner { background:#ecfdf5; border:1px solid #6ee7b7; border-radius:6px; padding:14px 18px; margin-bottom:20px; display:none; }
.mcp-new-key-banner strong { color:#065f46; }
.mcp-new-key-banner code { background:#fff; border:1px solid #a7f3d0; padding:4px 10px; border-radius:4px; font-family:monospace; font-size:13px; word-break:break-all; }
@media(max-width:700px){ .mcp-cards{grid-template-columns:1fr;} .mcp-form-row{flex-direction:column;} }
</style>
<?php
} }
public function renderDashboard(): void { public function renderDashboard(): void {
@@ -41,101 +69,232 @@ class AdminMenu {
$table = $wpdb->prefix . 'asktam_mcp_keys'; $table = $wpdb->prefix . 'asktam_mcp_keys';
$keys = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC"); $keys = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
$sseUrl = rest_url('asktam-mcp/v1/sse'); $sseUrl = rest_url('asktam-mcp/v1/sse');
$infoUrl = rest_url('asktam-mcp/v1/info'); $msgUrl = rest_url('asktam-mcp/v1/message');
?> ?>
<div class="wrap"> <div class="wrap mcp-wrap">
<h1>AskTam MCP <span style="font-size:13px;font-weight:normal;color:#666;">v<?php echo esc_html(ASKTAM_MCP_VERSION); ?></span></h1> <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>
<div class="notice notice-info" style="padding:12px 16px;margin:12px 0;"> <!-- Endpoint bar -->
<strong>MCP Endpoint:</strong> <div class="mcp-endpoint-bar">
<code style="background:#f0f0f0;padding:3px 8px;border-radius:3px;"><?php echo esc_html($sseUrl); ?></code> <strong>SSE Endpoint:</strong>
&nbsp; <code id="mcp-sse-url"><?php echo esc_html($sseUrl); ?></code>
<a href="<?php echo esc_url($infoUrl); ?>" target="_blank">Info →</a> <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> </div>
<h2 style="margin-top:24px;">API Keys</h2> <!-- NEW KEY BANNER (shown via JS after form submit) -->
<p>Add the <code>X-MCP-API-Key</code> header to your MCP client with one of the keys below.</p> <div class="mcp-new-key-banner" id="mcp-new-key-banner">
<strong>✓ New API key created:</strong><br><br>
<code id="mcp-new-key-value"></code><br><br>
<span style="color:#065f46;font-size:12px;">Copy it now — it won't be shown again in full.</span>
</div>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> <!-- Generate key form -->
<?php wp_nonce_field('asktam_mcp_create_key', '_wpnonce_mcp'); ?> <div class="mcp-generate-form">
<input type="hidden" name="action" value="asktam_mcp_create_key"> <h3>Generate API Key</h3>
<table class="form-table" style="max-width:600px;"> <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<tr> <?php wp_nonce_field('asktam_mcp_create_key', '_wpnonce_mcp'); ?>
<th>Name</th> <input type="hidden" name="action" value="asktam_mcp_create_key">
<td><input type="text" name="key_name" class="regular-text" placeholder="e.g. Claude Desktop" required></td> <div class="mcp-form-row">
</tr> <div>
<tr> <label for="mcp-key-name">Label</label>
<th>Scopes</th> <input type="text" id="mcp-key-name" name="key_name" placeholder="e.g. Claude Desktop, GPT Action" required>
<td> </div>
<label><input type="checkbox" name="scopes[]" value="read" checked> Read</label> &nbsp; <div>
<label><input type="checkbox" name="scopes[]" value="write"> Write</label> &nbsp; <label>Scopes</label>
<label><input type="checkbox" name="scopes[]" value="admin"> Admin</label> &nbsp; <div class="mcp-scope-checks">
<label><input type="checkbox" name="scopes[]" value="woocommerce"> WooCommerce</label> <label><input type="checkbox" name="scopes[]" value="read" checked> Read</label>
</td> <label><input type="checkbox" name="scopes[]" value="write"> Write</label>
</tr> <label><input type="checkbox" name="scopes[]" value="admin"> Admin</label>
</table> <label><input type="checkbox" name="scopes[]" value="woocommerce"> WooCommerce</label>
<?php submit_button('Generate API Key', 'primary', 'submit', false); ?> </div>
</form> </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;">Scope guide: <strong>Read</strong> = list/get only &nbsp;·&nbsp; <strong>Write</strong> = create/update/delete content &nbsp;·&nbsp; <strong>Admin</strong> = plugins, themes, users, options (supersedes all) &nbsp;·&nbsp; <strong>WooCommerce</strong> = product/order mutations</p>
</form>
</div>
<!-- Integration cards -->
<div class="mcp-cards">
<!-- Claude card -->
<div class="mcp-card">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><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 a remote MCP server via SSE. Works with Claude.ai connectors and Claude Desktop config.</p>
<strong style="font-size:12px;color:#555;display:block;margin-bottom:6px;">claude.ai → Settings → Connectors → Add MCP Server</strong>
<pre id="claude-config">{
"url": "<span class="key-url"><?php echo esc_html($sseUrl); ?></span>",
"headers": {
"X-MCP-API-Key": "<span class="key-placeholder">YOUR_API_KEY</span>"
}
}</pre>
<strong style="font-size:12px;color:#555;display:block;margin-bottom:6px;">Claude Desktop → claude_desktop_config.json</strong>
<pre id="claude-desktop-config" style="position:relative;">{
"mcpServers": {
"wordpress": {
"url": "<span class="key-url"><?php echo esc_html($sseUrl); ?></span>",
"headers": {
"X-MCP-API-Key": "<span class="key-placeholder">YOUR_API_KEY</span>"
}
}
}
}
<button class="copy-btn" onclick="copyCode('claude-desktop-config',this)">Copy</button></pre>
</div>
<!-- OpenAI card -->
<div class="mcp-card">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><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 / Assistants)
</h3>
<p>Use as a Custom GPT Action or Assistants API tool via the OpenAPI schema endpoint below.</p>
<strong style="font-size:12px;color:#555;display:block;margin-bottom:6px;">GPT Builder → Actions → Add Action → Import URL</strong>
<pre id="openai-schema-url" style="position:relative;"><span class="key-url"><?php echo esc_html(rest_url('asktam-mcp/v1/openapi.json')); ?></span>
<button class="copy-btn" onclick="copyCode('openai-schema-url',this)">Copy</button></pre>
<strong style="font-size:12px;color:#555;display:block;margin-bottom:6px;">Authentication → API Key → Header</strong>
<pre>{
"type": "apiKey",
"in": "header",
"name": "X-MCP-API-Key",
"value": "<span class="key-placeholder">YOUR_API_KEY</span>"
}</pre>
<strong style="font-size:12px;color:#555;display:block;margin-bottom:6px;">Assistants API (function calling)</strong>
<pre style="position:relative;"><span style="color:#89b4fa;">POST</span> https://api.openai.com/v1/assistants
{
"tools": [{ "type": "function", ... }],
"instructions": "You control a WordPress site..."
}
<button class="copy-btn" onclick="navigator.clipboard.writeText('<?php echo esc_js($msgUrl); ?>');this.textContent='Copied!'">Copy msg URL</button></pre>
</div>
</div>
<!-- Keys table -->
<h2 style="margin-bottom:12px;">API Keys</h2>
<?php if ($keys): ?> <?php if ($keys): ?>
<table class="wp-list-table widefat fixed striped" style="max-width:900px;margin-top:24px;"> <table class="mcp-key-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Label</th>
<th>API Key</th> <th>Key (masked)</th>
<th>Scopes</th> <th>Scopes</th>
<th>Created</th>
<th>Last Used</th> <th>Last Used</th>
<th>Status</th> <th>Status</th>
<th>Actions</th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($keys as $key): ?> <?php foreach ($keys as $key): ?>
<tr> <tr>
<td><?php echo esc_html($key->name); ?></td> <td><strong><?php echo esc_html($key->name); ?></strong></td>
<td><code style="font-size:11px;"><?php echo esc_html(substr($key->api_key, 0, 12) . '...'); ?></code> <td>
<button onclick="navigator.clipboard.writeText('<?php echo esc_js($key->api_key); ?>')" class="button button-small">Copy</button> <code style="font-size:12px;background:#f6f7f7;padding:2px 8px;border-radius:3px;">
<?php echo esc_html(substr($key->api_key, 0, 8) . '•••••••••••••' . substr($key->api_key, -4)); ?>
</code>
<button onclick="navigator.clipboard.writeText('<?php echo esc_js($key->api_key); ?>');this.textContent='✓ Copied';setTimeout(()=>this.textContent='Copy',2000)" class="button button-small" style="margin-left:6px;">Copy</button>
</td>
<td>
<?php foreach (array_map('trim', explode(',', $key->scopes)) as $scope): ?>
<span class="mcp-scope"><?php echo esc_html($scope); ?></span>
<?php endforeach; ?>
</td>
<td style="color:#666;font-size:12px;"><?php echo esc_html(date('Y-m-d', strtotime($key->created_at))); ?></td>
<td style="color:#666;font-size:12px;"><?php echo $key->last_used ? esc_html(date('Y-m-d H:i', strtotime($key->last_used))) : '—'; ?></td>
<td>
<span class="mcp-badge <?php echo $key->is_active ? 'mcp-badge-active' : 'mcp-badge-inactive'; ?>">
<?php echo $key->is_active ? 'Active' : 'Inactive'; ?>
</span>
</td> </td>
<td><?php echo esc_html($key->scopes); ?></td>
<td><?php echo $key->last_used ? esc_html($key->last_used) : '—'; ?></td>
<td><?php echo $key->is_active ? '<span style="color:green">Active</span>' : '<span style="color:red">Inactive</span>'; ?></td>
<td> <td>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;"> <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline;">
<?php wp_nonce_field('asktam_mcp_revoke_' . $key->id, '_wpnonce_mcp'); ?> <?php wp_nonce_field('asktam_mcp_revoke_' . $key->id, '_wpnonce_mcp'); ?>
<input type="hidden" name="action" value="asktam_mcp_revoke_key"> <input type="hidden" name="action" value="asktam_mcp_revoke_key">
<input type="hidden" name="key_id" value="<?php echo (int) $key->id; ?>"> <input type="hidden" name="key_id" value="<?php echo (int) $key->id; ?>">
<button type="submit" class="button button-small" onclick="return confirm('Revoke this key?')">Revoke</button> <button type="submit" class="button button-small" onclick="return confirm('Revoke key for <?php echo esc_js($key->name); ?>?')" <?php echo !$key->is_active ? 'disabled' : ''; ?>>Revoke</button>
</form> </form>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<?php else: ?>
<p style="color:#888;">No keys yet. Generate one above.</p>
<?php endif; ?> <?php endif; ?>
</div> </div>
<script>
function copyCode(id, btn) {
const el = document.getElementById(id);
const text = el.innerText.replace(/\bCopy\b.*$/, '').trim();
navigator.clipboard.writeText(text);
btn.textContent = '✓ Copied';
setTimeout(() => btn.textContent = 'Copy', 2000);
}
// Show new key banner if redirected after creation
(function() {
const params = new URLSearchParams(window.location.search);
if (params.get('mcp_created') === '1') {
// Key was just created — pull from session storage if we stored it
const stored = sessionStorage.getItem('mcp_new_key');
if (stored) {
const banner = document.getElementById('mcp-new-key-banner');
const val = document.getElementById('mcp-new-key-value');
banner.style.display = 'block';
val.textContent = stored;
sessionStorage.removeItem('mcp_new_key');
}
}
})();
</script>
<?php <?php
} }
public function renderSettings(): void { public function renderSettings(): void {
$settings = get_option('asktam_mcp_settings', []); $settings = get_option('asktam_mcp_settings', []);
?> ?>
<div class="wrap"> <div class="wrap mcp-wrap">
<h1>AskTam MCP Settings</h1> <h1>AskTam MCP — Settings</h1>
<form method="post" action="options.php"> <form method="post" action="options.php">
<?php settings_fields('asktam_mcp_settings_group'); ?> <?php settings_fields('asktam_mcp_settings_group'); ?>
<table class="form-table" style="max-width:600px;"> <table class="form-table" style="max-width:600px;">
<tr> <tr>
<th>Rate Limit (req/min)</th> <th>Rate Limit (req/min per key)</th>
<td><input type="number" name="asktam_mcp_settings[rate_limit]" value="<?php echo (int) ($settings['rate_limit'] ?? 60); ?>" min="1" max="300"></td> <td><input type="number" name="asktam_mcp_settings[rate_limit]" value="<?php echo (int) ($settings['rate_limit'] ?? 60); ?>" min="1" max="300" class="small-text"></td>
</tr> </tr>
<tr> <tr>
<th>Log Requests</th> <th>Log Requests</th>
<td><input type="checkbox" name="asktam_mcp_settings[log_requests]" value="1" <?php checked($settings['log_requests'] ?? true); ?>></td> <td><input type="checkbox" name="asktam_mcp_settings[log_requests]" value="1" <?php checked($settings['log_requests'] ?? true); ?>></td>
</tr> </tr>
<tr> <tr>
<th>Allow Raw SQL</th> <th>Allow Raw SQL <span style="font-weight:400;color:#888;">(admin scope only)</span></th>
<td><input type="checkbox" name="asktam_mcp_settings[allow_raw_sql]" value="1" <?php checked($settings['allow_raw_sql'] ?? false); ?>> <td>
<p class="description">Allows admin-scoped keys to run SELECT queries directly.</p> <input type="checkbox" name="asktam_mcp_settings[allow_raw_sql]" value="1" <?php checked($settings['allow_raw_sql'] ?? false); ?>>
<p class="description">Allows admin-scoped keys to execute SELECT queries directly.</p>
</td>
</tr>
<tr>
<th>WooCommerce Tools</th>
<td>
<input type="checkbox" name="asktam_mcp_settings[wc_enabled]" value="1" <?php checked($settings['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> </td>
</tr> </tr>
</table> </table>