1bfafa9908
- Replace all hardcoded English strings in mcp.php with dbnToolsT() calls - Add 44 MCP UI chrome translation keys to includes/i18n.php (en/no/uk/pl) - Generate includes/mcp-tool-translations.php with tool names, descriptions, and parameter docs translated into Norwegian, Ukrainian, and Polish via Azure OpenAI - Create mcp-tool.php: parameterized detail page (?tool=dbn.slug) with parameter table, example request/response JSON, and privacy section, all localized - Add "View details →" links on tool cards in mcp.php (shown on expand) - Add translations/mcp-chrome.php and scripts/generate-mcp-translations.php Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
741 lines
34 KiB
PHP
741 lines
34 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><?= htmlspecialchars(dbnToolsT('mcp_page_title', $uiLang)) ?></title>
|
|
<meta name="description" content="<?= htmlspecialchars(dbnToolsT('mcp_meta_desc', $uiLang)) ?>">
|
|
<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">
|
|
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.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[data-expanded="1"] .mcp-tool-card__desc { -webkit-line-clamp: unset; overflow: visible; }
|
|
.mcp-tool-card__details-link {
|
|
display: inline-block;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.78rem;
|
|
color: var(--dbn-blue, #00205b);
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
.mcp-tool-card__details-link:hover { text-decoration: underline; }
|
|
/* ── 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"><?= htmlspecialchars(dbnToolsT('mcp_hero_badge', $uiLang)) ?></div>
|
|
<h1><?= htmlspecialchars(dbnToolsT('mcp_hero_h1', $uiLang)) ?></h1>
|
|
<p><?= htmlspecialchars(dbnToolsT('mcp_hero_sub', $uiLang)) ?></p>
|
|
</div>
|
|
|
|
<!-- ── Token management ─────────────────────────────────────── -->
|
|
<section class="account-section" id="mcpTokenSection">
|
|
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_token_section_title', $uiLang)) ?></p>
|
|
|
|
<?php if (!$isLoggedIn): ?>
|
|
<div class="mcp-gate">
|
|
<p><?= htmlspecialchars(dbnToolsT('mcp_gate_guest_p', $uiLang)) ?></p>
|
|
<a href="/?return=<?= urlencode('/mcp.php') ?>" class="account-upgrade-btn" style="display:inline-block; text-decoration:none;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_gate_guest_btn', $uiLang)) ?>
|
|
</a>
|
|
</div>
|
|
|
|
<?php elseif (!$isPlusPro): ?>
|
|
<div class="mcp-gate">
|
|
<p><?= htmlspecialchars(dbnToolsT('mcp_gate_free_p', $uiLang)) ?></p>
|
|
<a href="/account.php" class="account-upgrade-btn" style="display:inline-block; text-decoration:none;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_gate_free_btn', $uiLang)) ?>
|
|
</a>
|
|
</div>
|
|
|
|
<?php else: ?>
|
|
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_token_hint', $uiLang)) ?>
|
|
</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;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_token_create_btn', $uiLang)) ?>
|
|
</button>
|
|
</form>
|
|
|
|
<div id="mcpTokenReveal" class="mcp-token-reveal">
|
|
<strong><?= htmlspecialchars(dbnToolsT('mcp_token_reveal_label', $uiLang)) ?></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;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_token_copy_btn', $uiLang)) ?>
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<!-- ── Client config ─────────────────────────────────────────── -->
|
|
<section class="account-section">
|
|
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_config_title', $uiLang)) ?></p>
|
|
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0; margin-bottom:1rem;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_config_hint', $uiLang)) ?>
|
|
<span id="tokenHint" style="display:none;color:#065f46;font-weight:600;"> <?= htmlspecialchars(dbnToolsT('mcp_config_token_filled', $uiLang)) ?></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;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_config_run_terminal', $uiLang)) ?>
|
|
</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;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_test_btn', $uiLang)) ?>
|
|
</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"><?= htmlspecialchars(dbnToolsT('mcp_tools_title', $uiLang)) ?> (<?= count($toolCatalog) ?>)</p>
|
|
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_tools_sub', $uiLang)) ?>
|
|
</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);
|
|
$detailUrl = '/mcp-tool.php?tool=' . urlencode((string)($tool['slug'] ?? '')) . '&lang=' . urlencode($uiLang);
|
|
?>
|
|
<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.3rem 0 0; font-size:0.75rem; color:var(--muted,#667085); display:none;" class="mcp-tool-card__hint">
|
|
<em><?= htmlspecialchars(dbnToolsT('mcp_tools_param_req_hint', $uiLang)) ?></em>
|
|
</p>
|
|
<?php endif; ?>
|
|
<a href="<?= htmlspecialchars($detailUrl) ?>" class="mcp-tool-card__details-link" style="display:none;"
|
|
onclick="event.stopPropagation();">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_tools_view_details', $uiLang)) ?>
|
|
</a>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Privacy notice ─────────────────────────────────────────── -->
|
|
<section class="account-section">
|
|
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_privacy_title', $uiLang)) ?></p>
|
|
<div class="mcp-privacy">
|
|
<strong><?= htmlspecialchars(dbnToolsT('mcp_privacy_text', $uiLang)) ?></strong>
|
|
</div>
|
|
<p style="margin-top:1rem; font-size:0.82rem; color:var(--muted,#667085);">
|
|
<?= htmlspecialchars(dbnToolsT('mcp_privacy_legal', $uiLang)) ?>
|
|
</p>
|
|
</section>
|
|
|
|
</div><!-- /.account-shell -->
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
/* ── i18n from PHP ─────────────────────────────────────────── */
|
|
var I = {
|
|
tokenActive: <?= json_encode(dbnToolsT('mcp_token_active', $uiLang)) ?>,
|
|
tokenRevoked: <?= json_encode(dbnToolsT('mcp_token_revoked', $uiLang)) ?>,
|
|
tokenNeverUsed: <?= json_encode(dbnToolsT('mcp_token_never_used', $uiLang)) ?>,
|
|
tokenLastUsed: <?= json_encode(dbnToolsT('mcp_token_last_used', $uiLang)) ?>,
|
|
tokenRevokeBtn: <?= json_encode(dbnToolsT('mcp_token_revoke_btn', $uiLang)) ?>,
|
|
testNoToken: <?= json_encode(dbnToolsT('mcp_test_no_token', $uiLang)) ?>,
|
|
testTesting: <?= json_encode(dbnToolsT('mcp_test_testing', $uiLang)) ?>,
|
|
};
|
|
|
|
/* ── Helpers ──────────────────────────────────────────────────── */
|
|
function esc(s) {
|
|
return String(s || '').replace(/[&<>"']/g, function (c) {
|
|
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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');
|
|
var details = card.querySelector('.mcp-tool-card__details-link');
|
|
if (expanded) {
|
|
card.setAttribute('data-expanded', '0');
|
|
if (params) params.style.display = 'none';
|
|
if (hint) hint.style.display = 'none';
|
|
if (details) details.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 (details) details.style.display = 'inline-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;"><?= addslashes(dbnToolsT('mcp_token_no_tokens', $uiLang)) ?></p>';
|
|
return;
|
|
}
|
|
tokenList.innerHTML = tokens.map(function (t) {
|
|
var status = t.is_active ? esc(I.tokenActive) : esc(I.tokenRevoked);
|
|
var last = t.last_used_at ? esc(I.tokenLastUsed) + ' ' + esc(t.last_used_at) : esc(I.tokenNeverUsed);
|
|
var revoke = t.is_active
|
|
? '<button type="button" class="mcp-token-row__revoke" data-revoke="' + t.id + '">' + esc(I.tokenRevokeBtn) + '</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;">' + status + ' · ' + 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();
|
|
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 = I.testNoToken; testResult.style.color = '#b45309'; }
|
|
return;
|
|
}
|
|
testBtn.disabled = true;
|
|
if (testResult) { testResult.textContent = I.testTesting; 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>
|