feat: activity log tab + rotate secret UI + HTTPS warning + security section

This commit is contained in:
2026-05-08 22:19:15 +00:00
parent 7dd426a38b
commit adf7b569f9
+139 -144
View File
@@ -10,87 +10,62 @@ class AdminMenu {
add_action('admin_menu', [$this, 'register']);
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
add_action('admin_init', [$this, 'registerSettings']);
add_action('admin_post_asktam_mcp_rotate_secret', [$this, 'handleRotateSecret']);
}
public function registerSettings(): void {
register_setting(
'asktam_mcp_settings_group',
'asktam_mcp_settings',
['sanitize_callback' => [$this, 'sanitizeSettings']]
);
register_setting('asktam_mcp_settings_group', 'asktam_mcp_settings', [
'sanitize_callback' => [$this, 'sanitizeSettings'],
]);
}
public function sanitizeSettings(mixed $input): array {
$clean = [];
$clean['rate_limit'] = max(1, min(300, (int) ($input['rate_limit'] ?? 60)));
$clean['log_requests'] = !empty($input['log_requests']);
$clean['allow_raw_sql'] = !empty($input['allow_raw_sql']);
$clean['wc_enabled'] = !empty($input['wc_enabled']);
return $clean;
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 enqueueAssets(string $hook): void {
if (!in_array($hook, ['toplevel_page_asktam-mcp', 'asktam-mcp_page_asktam-mcp-settings'], true)) {
return;
}
if (!str_contains($hook, 'asktam-mcp')) return;
add_action('admin_head', [$this, 'inlineStyles']);
}
public function inlineStyles(): void {
?>
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 .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; }
@media(max-width:700px){ .mcp-cards{grid-template-columns:1fr;} .mcp-form-row{flex-direction:column;} }
.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
}
<?php }
// -------------------------------------------------------------------------
// Dashboard — API Keys
// -------------------------------------------------------------------------
public function renderDashboard(): void {
global $wpdb;
$table = $wpdb->prefix . 'asktam_mcp_keys';
$keys = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
$keys = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}asktam_mcp_keys ORDER BY created_at DESC");
$sseUrl = rest_url('asktam-mcp/v1/sse');
$msgUrl = rest_url('asktam-mcp/v1/message');
$isHttps = is_ssl();
?>
<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>
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>
@@ -98,17 +73,14 @@ class AdminMenu {
<a href="<?php echo esc_url(rest_url('asktam-mcp/v1/info')); ?>" target="_blank" class="button button-small">Info →</a>
</div>
<!-- Generate key form -->
<!-- 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, GPT Action" required>
</div>
<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">
@@ -120,147 +92,170 @@ class AdminMenu {
</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 &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>
<p style="margin:0;color:#888;font-size:12px;"><strong>Read</strong> = list/get &nbsp;·&nbsp; <strong>Write</strong> = create/update/delete content &nbsp;·&nbsp; <strong>Admin</strong> = full access (supersedes all) &nbsp;·&nbsp; <strong>WooCommerce</strong> = product/order mutations</p>
</form>
</div>
<!-- Integration cards -->
<div class="mcp-cards">
<!-- Claude -->
<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 a remote MCP server. In the connector dialog, paste the SSE URL and your API key into the <strong>token field</strong> (leave OAuth Client ID blank).</p>
<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>
Token: <span style="color:#b45309;">YOUR_API_KEY ← paste here</span>
OAuth Client ID: (leave blank)</pre>
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-config" style="position:relative;">{
<pre id="claude-desktop" style="position:relative;">{
"mcpServers": {
"wordpress": {
"url": "<span class="key-url"><?php echo esc_html($sseUrl); ?></span>",
"headers": {
"Authorization": "Bearer <span class="key-placeholder">YOUR_API_KEY</span>"
"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="copyCode('claude-desktop-config',this)">Copy</button></pre>
<button class="copy-btn" onclick="copyPre('claude-desktop',this)">Copy</button></pre>
</div>
<!-- OpenAI -->
<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 / Assistants)
</h3>
<p>Import the OpenAPI schema into a Custom GPT Action. Auth via API Key in the <code>X-MCP-API-Key</code> header.</p>
<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-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:4px;">Authentication → API Key → Header name</strong>
<pre>{
"type": "apiKey",
"in": "header",
"name": "<span class="key-placeholder">X-MCP-API-Key</span>"
}</pre>
<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>
<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 $key): ?>
<?php foreach ($keys as $k): ?>
<tr>
<td><strong><?php echo esc_html($key->name); ?></strong></td>
<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($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>
<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(',', $key->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($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><?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_' . $key->id, '_wpnonce_mcp'); ?>
<?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) $key->id; ?>">
<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>
<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; ?>
</div>
<?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 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);
}
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>
<?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);
?>
<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>
<?php endif; ?>
<?php else: ?><p style="color:#888;">No activity logged yet.</p><?php endif; ?>
</div>
<?php
}
// -------------------------------------------------------------------------
// Settings
// -------------------------------------------------------------------------
public function renderSettings(): void {
$settings = get_option('asktam_mcp_settings', []);
$s = get_option('asktam_mcp_settings', []);
?>
<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');
?>
<?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) ($settings['rate_limit'] ?? 60); ?>" min="1" max="300" class="small-text"></td>
</tr>
<tr>
<th>Log Requests</th>
<td><input type="checkbox" name="asktam_mcp_settings[log_requests]" value="1" <?php checked($settings['log_requests'] ?? true); ?>></td>
</tr>
<tr>
<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); ?>>
<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>
</tr>
<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>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
// -------------------------------------------------------------------------
// Rotate secret action
// -------------------------------------------------------------------------
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')));
exit;
}
}