Files
dobetternorge-tools/mcp.php
T
daveadmin c997f204b5 feat: add Do Better Norge MCP server — token system, runtime API, interactive setup page
- UserMcpTokens: per-user SHA256-hashed token mint/validate/revoke (Plus/Pro only)
- DbnMcpRuntime: 19 MCP tools (search, ask, summarize, timeline, redact, translate,
  legal_analysis, korrespond, barnevernet_analyze, advocate_brief, deep_research,
  discrepancy_find, transcribe_audio, corpus_stats, list_documents, get_document,
  citation_graph, case_workbench_plan, save_to_case)
- api/mcp/user/: session/tools/invoke HTTP endpoints with Bearer token auth
- api/mcp-tokens.php: token create/revoke/list REST API
- mcp.php: interactive setup page with token management, 5-client config tabs,
  auto-fill on token creation, tool catalog grid, privacy notice
- account.php: simplified MCP section with link to mcp.php
- nav.php: MCP nav link
- .htaccess: Authorization header passthrough, MCP route rewrite, CORS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:01:13 +02:00

708 lines
32 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/DbnMcpRuntime.php';
require_once __DIR__ . '/includes/UserMcpTokens.php';
$uiLang = dbnToolsCurrentLanguage();
$isLoggedIn = dbnToolsIsAuthenticated();
$authUser = $isLoggedIn ? dbnToolsAuthenticatedUser() : null;
$isSso = $isLoggedIn && dbnToolsIsFreeTier();
$tier = 'guest';
$detail = null;
if ($isSso) {
$detail = FreeTier::balanceDetail((int)$_SESSION['dbn_tools_sso_uid']);
$tier = (string)($detail['tier'] ?? 'free');
} elseif ($isLoggedIn) {
$tier = 'caveau';
}
$isPlusPro = in_array($tier, ['plus', 'pro'], true);
$toolCatalog = DbnMcpRuntime::tools();
$toolIcons = [
'dbn.search_legal' => '🔍',
'dbn.ask' => '💬',
'dbn.summarize' => '📋',
'dbn.timeline' => '📅',
'dbn.redact' => '🔒',
'dbn.translate' => '🌍',
'dbn.legal_analysis' => '⚖️',
'dbn.korrespond' => '✉️',
'dbn.barnevernet_analyze' => '📄',
'dbn.advocate_brief' => '🏛️',
'dbn.deep_research' => '🔬',
'dbn.discrepancy_find' => '🔄',
'dbn.transcribe_audio' => '🎤',
'dbn.corpus_stats' => '📊',
'dbn.list_documents' => '📚',
'dbn.get_document' => '📖',
'dbn.citation_graph' => '🔗',
'dbn.case_workbench_plan' => '🗂️',
'dbn.save_to_case' => '💾',
];
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MCP — Do Better Norge</title>
<meta name="description" content="Connect Claude, Cursor, and other AI tools to all 19 Do Better Norge legal preparation tools via MCP.">
<meta name="robots" content="noindex, nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="assets/css/tools.css">
<style>
/* ── MCP page styles ───────────────────────────────────────── */
.mcp-hero {
padding: 2.5rem 1.5rem 2rem;
text-align: center;
border-bottom: 1px solid var(--dbn-line, rgba(22,19,15,.16));
margin-bottom: 2rem;
}
.mcp-hero h1 {
font-family: 'Crimson Pro', serif;
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 700;
color: var(--dbn-blue, #00205b);
margin: 0 0 0.6rem;
}
.mcp-hero p {
color: var(--muted, #667085);
font-size: 1rem;
max-width: 560px;
margin: 0 auto;
}
.mcp-hero__badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: #ede9fe;
color: #5b21b6;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
padding: 0.3rem 0.75rem;
border-radius: 999px;
margin-bottom: 1rem;
}
/* ── Tabs ──────────────────────────────────────────────────── */
.mcp-tabs {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
border-bottom: 2px solid var(--dbn-line, rgba(22,19,15,.16));
margin-bottom: 1.25rem;
}
.mcp-tab-btn {
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
padding: 0.55rem 1rem;
font-size: 0.88rem;
font-weight: 500;
color: var(--muted, #667085);
cursor: pointer;
border-radius: 4px 4px 0 0;
transition: color .15s, border-color .15s;
}
.mcp-tab-btn:hover { color: var(--dbn-blue, #00205b); }
.mcp-tab-btn.is-active {
color: var(--dbn-blue, #00205b);
border-bottom-color: var(--dbn-blue, #00205b);
font-weight: 600;
}
.mcp-tab-panel { display: none; }
.mcp-tab-panel.is-active { display: block; }
/* ── Code blocks ────────────────────────────────────────────── */
.mcp-code-wrap {
position: relative;
}
.mcp-code-wrap pre {
background: #101828;
color: #f9fafb;
padding: 1rem 1rem 1rem 1rem;
border-radius: 8px;
font-size: 0.8rem;
overflow-x: auto;
margin: 0;
line-height: 1.6;
}
.mcp-copy-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(249,250,251,.12);
border: 1px solid rgba(249,250,251,.2);
color: #f9fafb;
font-size: 0.75rem;
padding: 0.3rem 0.6rem;
border-radius: 5px;
cursor: pointer;
transition: background .15s;
}
.mcp-copy-btn:hover { background: rgba(249,250,251,.22); }
/* ── Tool catalog ───────────────────────────────────────────── */
.mcp-tool-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
margin-top: 1rem;
}
.mcp-tool-card {
border: 1px solid var(--dbn-line, rgba(22,19,15,.16));
border-radius: 10px;
padding: 0.9rem 1rem;
background: #fff;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
}
.mcp-tool-card:hover {
border-color: var(--dbn-blue, #00205b);
box-shadow: 0 4px 12px rgba(0,32,91,.08);
}
.mcp-tool-card__icon { font-size: 1.3rem; margin-bottom: 0.35rem; }
.mcp-tool-card__name {
font-weight: 600;
font-size: 0.88rem;
color: var(--dbn-blue, #00205b);
margin-bottom: 0.2rem;
}
.mcp-tool-card__slug {
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-size: 0.72rem;
color: var(--muted, #667085);
margin-bottom: 0.4rem;
}
.mcp-tool-card__desc {
font-size: 0.82rem;
color: var(--dbn-ink, #16130f);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.mcp-tool-card[open] .mcp-tool-card__desc { -webkit-line-clamp: unset; overflow: visible; }
/* ── Gated states ───────────────────────────────────────────── */
.mcp-gate {
text-align: center;
padding: 2rem 1rem;
}
.mcp-gate p { color: var(--muted, #667085); margin: 0.5rem 0 1.25rem; }
/* ── Token revealed ─────────────────────────────────────────── */
.mcp-token-reveal {
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
padding: 0.9rem 1rem;
margin-top: 0.85rem;
display: none;
}
.mcp-token-reveal code {
display: block;
overflow-x: auto;
white-space: nowrap;
margin-top: 0.4rem;
font-size: 0.85rem;
word-break: break-all;
}
/* ── Token list ─────────────────────────────────────────────── */
.mcp-token-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
border: 1px solid var(--dbn-line, #e5e7eb);
border-radius: 8px;
padding: 0.7rem 0.8rem;
}
.mcp-token-row + .mcp-token-row { margin-top: 0.5rem; }
.mcp-token-row__revoke {
flex-shrink: 0;
border: 1px solid #fecaca;
color: #991b1b;
background: #fff;
border-radius: 6px;
padding: 0.35rem 0.55rem;
cursor: pointer;
font-size: 0.82rem;
}
/* ── Privacy notice ─────────────────────────────────────────── */
.mcp-privacy {
background: var(--dbn-paper, #f6f2ea);
border-radius: 8px;
padding: 1rem 1.25rem;
font-size: 0.85rem;
color: var(--dbn-ink, #16130f);
margin-top: 0.5rem;
}
.mcp-privacy strong { color: var(--dbn-blue, #00205b); }
</style>
</head>
<body data-authenticated="<?= $isLoggedIn ? 'true' : 'false' ?>" class="lt-app">
<script>
window.DBN_TOOLS_AUTHENTICATED = <?= $isLoggedIn ? 'true' : 'false' ?>;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<div class="account-shell">
<!-- ── Hero ─────────────────────────────────────────────────── -->
<div class="mcp-hero">
<div class="mcp-hero__badge">✦ Plus &amp; Pro</div>
<h1>Use DBN tools from Claude, Cursor &amp; Copilot</h1>
<p>Connect any MCP client to all 19 Do Better Norge tools — transcription, legal analysis, timelines, redaction, and more.</p>
</div>
<!-- ── Token management ─────────────────────────────────────── -->
<section class="account-section" id="mcpTokenSection">
<p class="account-section__title">Your MCP token</p>
<?php if (!$isLoggedIn): ?>
<div class="mcp-gate">
<p>Sign in to create your personal MCP token for Plus or Pro members.</p>
<a href="/?return=<?= urlencode('/mcp.php') ?>" class="account-upgrade-btn" style="display:inline-block; text-decoration:none;">Sign in</a>
</div>
<?php elseif (!$isPlusPro): ?>
<div class="mcp-gate">
<p>MCP access is available on Plus and Pro plans. Upgrade to connect your AI tools.</p>
<a href="/account.php" class="account-upgrade-btn" style="display:inline-block; text-decoration:none;">Upgrade plan</a>
</div>
<?php else: ?>
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
Tokens are shown once at creation. Create one per client (Claude, Cursor, VS Code…).
</p>
<div id="mcpTokenList" style="margin:0.85rem 0 0.25rem;"></div>
<form id="mcpTokenForm" style="display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; margin-top:0.85rem;">
<input id="mcpTokenName" type="text" maxlength="100" value="DBN MCP" aria-label="Token name"
style="min-width:220px; padding:0.65rem 0.75rem; border:1px solid var(--line,#d0d5dd); border-radius:8px; font-size:0.9rem;">
<button type="submit" class="account-upgrade-btn" style="border:0; cursor:pointer;">Create token</button>
</form>
<div id="mcpTokenReveal" class="mcp-token-reveal">
<strong>Copy this token now — it won't be shown again:</strong>
<code id="mcpTokenPlain"></code>
<button id="mcpTokenCopyBtn" type="button" style="margin-top:0.6rem; border:1px solid #bbf7d0; background:#fff; border-radius:6px; padding:0.35rem 0.7rem; cursor:pointer; font-size:0.82rem;">Copy token</button>
</div>
<?php endif; ?>
</section>
<!-- ── Client config ─────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title">Client configuration</p>
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0; margin-bottom:1rem;">
Paste your token into the config below after creating it above.
<span id="tokenHint" style="display:none;color:#065f46;font-weight:600;"> Token auto-filled.</span>
</p>
<div class="mcp-tabs" role="tablist" aria-label="MCP client">
<button class="mcp-tab-btn is-active" role="tab" aria-selected="true" aria-controls="tab-claude-desktop" data-tab="claude-desktop">Claude Desktop</button>
<button class="mcp-tab-btn" role="tab" aria-selected="false" aria-controls="tab-claude-code" data-tab="claude-code">Claude Code</button>
<button class="mcp-tab-btn" role="tab" aria-selected="false" aria-controls="tab-cursor" data-tab="cursor">Cursor / Windsurf</button>
<button class="mcp-tab-btn" role="tab" aria-selected="false" aria-controls="tab-vscode" data-tab="vscode">VS Code</button>
<button class="mcp-tab-btn" role="tab" aria-selected="false" aria-controls="tab-remote" data-tab="remote">Remote HTTP</button>
</div>
<!-- Claude Desktop -->
<div class="mcp-tab-panel is-active" id="tab-claude-desktop" role="tabpanel">
<p style="font-size:0.85rem; color:var(--muted,#667085); margin:0 0 0.75rem;">
Edit <code>claude_desktop_config.json</code>
(<code>~/Library/Application Support/Claude/</code> on Mac,
<code>%APPDATA%\Claude\</code> on Windows):
</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" data-copy="claude-desktop">Copy</button>
<pre><code id="code-claude-desktop">{
"mcpServers": {
"dobetternorge": {
"command": "npx",
"args": ["-y", "@bluenotelogic/mcp", "dobetternorge-mcp", "--stdio"],
"env": {
"DBN_MCP_TOKEN": "dbn_user_mcp_..."
}
}
}
}</code></pre>
</div>
</div>
<!-- Claude Code -->
<div class="mcp-tab-panel" id="tab-claude-code" role="tabpanel">
<p style="font-size:0.85rem; color:var(--muted,#667085); margin:0 0 0.75rem;">
Run in your terminal:
</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" data-copy="claude-code">Copy</button>
<pre><code id="code-claude-code">claude mcp add dobetternorge -- npx -y @bluenotelogic/mcp dobetternorge-mcp --stdio
# Set your token (add to ~/.bashrc or ~/.zshrc to persist):
export DBN_MCP_TOKEN=dbn_user_mcp_...</code></pre>
</div>
</div>
<!-- Cursor / Windsurf -->
<div class="mcp-tab-panel" id="tab-cursor" role="tabpanel">
<p style="font-size:0.85rem; color:var(--muted,#667085); margin:0 0 0.75rem;">
Edit <code>~/.cursor/mcp.json</code> (or Windsurf's equivalent):
</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" data-copy="cursor">Copy</button>
<pre><code id="code-cursor">{
"mcpServers": {
"dobetternorge": {
"command": "npx",
"args": ["-y", "@bluenotelogic/mcp", "dobetternorge-mcp", "--stdio"],
"env": {
"DBN_MCP_TOKEN": "dbn_user_mcp_..."
}
}
}
}</code></pre>
</div>
<p style="font-size:0.82rem; color:var(--muted,#667085); margin:0.75rem 0 0;">
Cursor also supports the remote HTTP endpoint — use the <strong>Remote HTTP</strong> tab if you prefer not to run npx.
</p>
</div>
<!-- VS Code -->
<div class="mcp-tab-panel" id="tab-vscode" role="tabpanel">
<p style="font-size:0.85rem; color:var(--muted,#667085); margin:0 0 0.75rem;">
Create <code>.vscode/mcp.json</code> in your project (VS Code will prompt for the token on first use):
</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" data-copy="vscode">Copy</button>
<pre><code id="code-vscode">{
"inputs": [
{
"type": "promptString",
"id": "dbn-token",
"description": "Do Better Norge MCP token",
"password": true
}
],
"servers": {
"dobetternorge": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@bluenotelogic/mcp", "dobetternorge-mcp", "--stdio"],
"env": {
"DBN_MCP_TOKEN": "${input:dbn-token}"
}
}
}
}</code></pre>
</div>
</div>
<!-- Remote HTTP -->
<div class="mcp-tab-panel" id="tab-remote" role="tabpanel">
<p style="font-size:0.85rem; color:var(--muted,#667085); margin:0 0 0.75rem;">
For clients that support remote MCP (Cursor, Zed, Windsurf with remote connector) — no npx needed:
</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" data-copy="remote">Copy</button>
<pre><code id="code-remote">URL: https://mcp.dobetternorge.no/mcp
Authorization: Bearer dbn_user_mcp_...</code></pre>
</div>
<p style="font-size:0.82rem; color:var(--muted,#667085); margin:0.75rem 0 0;">
Paste the URL into your client's "Remote MCP server" field, then set the <code>Authorization</code> header.
</p>
</div>
<?php if ($isPlusPro): ?>
<div style="margin-top:1.25rem; display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap;">
<button id="mcpTestBtn" type="button"
style="border:1px solid var(--dbn-blue,#00205b); color:var(--dbn-blue,#00205b); background:#fff; border-radius:8px; padding:0.55rem 1rem; cursor:pointer; font-size:0.88rem;">
Test connection
</button>
<span id="mcpTestResult" style="font-size:0.88rem;"></span>
</div>
<?php endif; ?>
</section>
<!-- ── Tool catalog ───────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title">Available tools (<?= count($toolCatalog) ?>)</p>
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
All tools run on your Plus or Pro plan credits. Click a card for full details.
</p>
<div class="mcp-tool-grid" id="mcpToolGrid">
<?php foreach ($toolCatalog as $tool):
$slug = htmlspecialchars((string)($tool['slug'] ?? ''));
$name = htmlspecialchars((string)($tool['display_name'] ?? ''));
$desc = htmlspecialchars((string)($tool['description'] ?? ''));
$icon = $toolIcons[$tool['slug'] ?? ''] ?? '🔧';
$props = (array)($tool['config']['input_schema']['properties'] ?? []);
$req = (array)($tool['config']['input_schema']['required'] ?? []);
$paramNames = array_keys($props);
?>
<div class="mcp-tool-card" data-slug="<?= $slug ?>">
<div class="mcp-tool-card__icon"><?= $icon ?></div>
<div class="mcp-tool-card__name"><?= $name ?></div>
<div class="mcp-tool-card__slug"><?= $slug ?></div>
<div class="mcp-tool-card__desc"><?= $desc ?></div>
<?php if (!empty($paramNames)): ?>
<div class="mcp-tool-card__params" style="margin-top:0.5rem; display:none; flex-wrap:wrap; gap:0.3rem;">
<?php foreach ($paramNames as $param): ?>
<span style="background:<?= in_array($param, $req, true) ? '#ddd6fe' : '#f3f4f6' ?>;
color:<?= in_array($param, $req, true) ? '#5b21b6' : '#374151' ?>;
font-size:0.71rem; padding:0.15rem 0.45rem; border-radius:4px;
font-family: monospace;"><?= htmlspecialchars($param) ?><?= in_array($param, $req, true) ? '*' : '' ?></span>
<?php endforeach; ?>
</div>
<p style="margin:0.5rem 0 0; font-size:0.75rem; color:var(--muted,#667085); display:none;" class="mcp-tool-card__hint">
<em>Purple = required</em>
</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- ── Privacy notice ─────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title">Privacy</p>
<div class="mcp-privacy">
<strong>Process-and-forget by default.</strong> All tool calls process your text in memory and return results to your AI client.
Nothing is saved to My Case unless you explicitly call <code>dbn.save_to_case</code>.
DBN MCP tokens do not retain input text or tool results after the request completes.
</div>
<p style="margin-top:1rem; font-size:0.82rem; color:var(--muted,#667085);">
Tools provide legal preparation support, not final legal advice.
Results are for informational purposes and should be reviewed by a qualified legal professional.
</p>
</section>
</div><!-- /.account-shell -->
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<script>
(function () {
'use strict';
/* ── Helpers ──────────────────────────────────────────────────── */
function esc(s) {
return String(s || '').replace(/[&<>"']/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[c];
});
}
var TOKEN_PLACEHOLDER = 'dbn_user_mcp_...';
var activeToken = null;
/* ── Token-fill helper ────────────────────────────────────────── */
function fillToken(token) {
activeToken = token;
var ids = ['code-claude-desktop', 'code-claude-code', 'code-cursor', 'code-remote'];
ids.forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = el.textContent.replace(TOKEN_PLACEHOLDER, token);
});
var hint = document.getElementById('tokenHint');
if (hint) hint.style.display = 'inline';
}
/* ── Copy button ─────────────────────────────────────────────── */
function copyText(text, btn) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function () {
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = orig; }, 1500);
});
} else {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
/* ── Copy buttons on code blocks ─────────────────────────────── */
document.querySelectorAll('.mcp-copy-btn[data-copy]').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = 'code-' + btn.getAttribute('data-copy');
var el = document.getElementById(id);
if (el) copyText(el.textContent, btn);
});
});
/* ── Tab switcher ─────────────────────────────────────────────── */
var tabBtns = document.querySelectorAll('.mcp-tab-btn[data-tab]');
var tabPanels = document.querySelectorAll('.mcp-tab-panel');
tabBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
tabBtns.forEach(function (b) { b.classList.remove('is-active'); b.setAttribute('aria-selected', 'false'); });
tabPanels.forEach(function (p) { p.classList.remove('is-active'); });
btn.classList.add('is-active');
btn.setAttribute('aria-selected', 'true');
var panel = document.getElementById('tab-' + btn.getAttribute('data-tab'));
if (panel) panel.classList.add('is-active');
});
});
/* ── Tool cards expand/collapse ──────────────────────────────── */
document.querySelectorAll('.mcp-tool-card').forEach(function (card) {
card.addEventListener('click', function () {
var expanded = card.getAttribute('data-expanded') === '1';
var params = card.querySelector('.mcp-tool-card__params');
var hint = card.querySelector('.mcp-tool-card__hint');
var desc = card.querySelector('.mcp-tool-card__desc');
if (expanded) {
card.setAttribute('data-expanded', '0');
if (params) params.style.display = 'none';
if (hint) hint.style.display = 'none';
if (desc) { desc.style.webkitLineClamp = '2'; desc.style.overflow = 'hidden'; }
} else {
card.setAttribute('data-expanded', '1');
if (params) params.style.display = 'flex';
if (hint) hint.style.display = 'block';
if (desc) { desc.style.webkitLineClamp = 'unset'; desc.style.overflow = 'visible'; }
}
});
});
<?php if ($isPlusPro): ?>
/* ── Token list ──────────────────────────────────────────────── */
var tokenList = document.getElementById('mcpTokenList');
function renderTokens(tokens) {
if (!tokenList) return;
if (!tokens.length) {
tokenList.innerHTML = '<p style="color:var(--muted,#667085); font-size:0.88rem;">No MCP tokens yet.</p>';
return;
}
tokenList.innerHTML = tokens.map(function (t) {
var status = t.is_active ? 'Active' : 'Revoked';
var last = t.last_used_at ? 'Last used ' + esc(t.last_used_at) : 'Never used';
var revoke = t.is_active
? '<button type="button" class="mcp-token-row__revoke" data-revoke="' + t.id + '">Revoke</button>'
: '';
return '<div class="mcp-token-row">'
+ '<div>'
+ '<strong>' + esc(t.name) + '</strong><br>'
+ '<code style="font-size:0.8rem;">' + esc(t.token_prefix) + '…</code><br>'
+ '<span style="color:var(--muted,#667085); font-size:0.78rem;">' + esc(status) + ' · ' + esc(last) + '</span>'
+ '</div>'
+ revoke
+ '</div>';
}).join('');
}
function loadTokens() {
fetch('/api/mcp-tokens.php', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) { renderTokens(data.tokens || []); })
.catch(function () {
if (tokenList) tokenList.innerHTML = '<p style="color:#991b1b; font-size:0.88rem;">Could not load tokens.</p>';
});
}
if (tokenList) {
loadTokens();
tokenList.addEventListener('click', function (e) {
var id = e.target && e.target.getAttribute('data-revoke');
if (!id) return;
fetch('/api/mcp-tokens.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'revoke', id: Number(id) })
}).then(function () { loadTokens(); });
});
}
/* ── Token create form ────────────────────────────────────────── */
var form = document.getElementById('mcpTokenForm');
var nameEl = document.getElementById('mcpTokenName');
var reveal = document.getElementById('mcpTokenReveal');
var plainEl = document.getElementById('mcpTokenPlain');
var copyBtn = document.getElementById('mcpTokenCopyBtn');
if (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
fetch('/api/mcp-tokens.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create', name: (nameEl ? nameEl.value : '') || 'DBN MCP' })
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.ok) throw new Error((data.error && data.error.message) || 'Token creation failed');
var tok = data.token.token;
if (plainEl) plainEl.textContent = tok;
if (reveal) reveal.style.display = 'block';
fillToken(tok);
loadTokens();
// Scroll to config
var configSection = document.querySelector('.mcp-tabs');
if (configSection) configSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
})
.catch(function (err) { alert(err.message); });
});
}
if (copyBtn && plainEl) {
copyBtn.addEventListener('click', function () {
copyText(plainEl.textContent, copyBtn);
});
}
/* ── Test connection ─────────────────────────────────────────── */
var testBtn = document.getElementById('mcpTestBtn');
var testResult = document.getElementById('mcpTestResult');
if (testBtn) {
testBtn.addEventListener('click', function () {
if (!activeToken) {
if (testResult) { testResult.textContent = 'Create a token first.'; testResult.style.color = '#b45309'; }
return;
}
testBtn.disabled = true;
if (testResult) { testResult.textContent = 'Testing…'; testResult.style.color = 'var(--muted,#667085)'; }
fetch('/api/mcp/user/session', {
headers: { 'Authorization': 'Bearer ' + activeToken }
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.ok) {
if (testResult) { testResult.textContent = '✓ Connected — ' + (data.user && data.user.email ? data.user.email : 'OK'); testResult.style.color = '#065f46'; }
} else {
throw new Error((data.error && data.error.message) || 'Error');
}
})
.catch(function (err) {
if (testResult) { testResult.textContent = '✗ ' + err.message; testResult.style.color = '#991b1b'; }
})
.finally(function () { testBtn.disabled = false; });
});
}
<?php endif; ?>
}());
</script>
</body>
</html>