Localize mcp.php + add mcp-tool.php detail pages for all 19 MCP tools

- 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>
This commit is contained in:
2026-05-24 12:05:07 +02:00
parent e09ee62c62
commit 1bfafa9908
6 changed files with 2754 additions and 47 deletions
+79 -47
View File
@@ -50,8 +50,8 @@ $toolIcons = [
<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.">
<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>
@@ -190,7 +190,16 @@ $toolIcons = [
-webkit-box-orient: vertical;
overflow: hidden;
}
.mcp-tool-card[open] .mcp-tool-card__desc { -webkit-line-clamp: unset; overflow: visible; }
.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;
@@ -259,30 +268,34 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
<!-- ── 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 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">Your MCP token</p>
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_token_section_title', $uiLang)) ?></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>
<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>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>
<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;">
Tokens are shown once at creation. Create one per client (Claude, Cursor, VS Code…).
<?= htmlspecialchars(dbnToolsT('mcp_token_hint', $uiLang)) ?>
</p>
<div id="mcpTokenList" style="margin:0.85rem 0 0.25rem;"></div>
@@ -290,23 +303,27 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
<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>
<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>Copy this token now — it won't be shown again:</strong>
<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;">Copy token</button>
<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">Client configuration</p>
<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;">
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>
<?= 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">
@@ -343,7 +360,7 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
<!-- 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:
<?= 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>
@@ -427,7 +444,7 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
<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
<?= htmlspecialchars(dbnToolsT('mcp_test_btn', $uiLang)) ?>
</button>
<span id="mcpTestResult" style="font-size:0.88rem;"></span>
</div>
@@ -436,9 +453,9 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
<!-- ── Tool catalog ───────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title">Available tools (<?= count($toolCatalog) ?>)</p>
<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;">
All tools run on your Plus or Pro plan credits. Click a card for full details.
<?= htmlspecialchars(dbnToolsT('mcp_tools_sub', $uiLang)) ?>
</p>
<div class="mcp-tool-grid" id="mcpToolGrid">
@@ -450,6 +467,7 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
$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>
@@ -465,10 +483,14 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
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 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>
@@ -476,15 +498,12 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
<!-- ── Privacy notice ─────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title">Privacy</p>
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_privacy_title', $uiLang)) ?></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.
<strong><?= htmlspecialchars(dbnToolsT('mcp_privacy_text', $uiLang)) ?></strong>
</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.
<?= htmlspecialchars(dbnToolsT('mcp_privacy_legal', $uiLang)) ?>
</p>
</section>
@@ -496,6 +515,17 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
(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) {
@@ -566,19 +596,22 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
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 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 (desc) { desc.style.webkitLineClamp = '2'; desc.style.overflow = 'hidden'; }
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 (desc) { desc.style.webkitLineClamp = 'unset'; desc.style.overflow = 'visible'; }
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'; }
}
});
});
@@ -590,20 +623,20 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
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>';
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 ? 'Active' : 'Revoked';
var last = t.last_used_at ? 'Last used ' + esc(t.last_used_at) : 'Never used';
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 + '">Revoke</button>'
? '<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;">' + esc(status) + ' · ' + esc(last) + '</span>'
+ '<span style="color:var(--muted,#667085); font-size:0.78rem;">' + status + ' · ' + last + '</span>'
+ '</div>'
+ revoke
+ '</div>';
@@ -657,7 +690,6 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
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' });
})
@@ -678,11 +710,11 @@ Authorization: Bearer dbn_user_mcp_...</code></pre>
if (testBtn) {
testBtn.addEventListener('click', function () {
if (!activeToken) {
if (testResult) { testResult.textContent = 'Create a token first.'; testResult.style.color = '#b45309'; }
if (testResult) { testResult.textContent = I.testNoToken; testResult.style.color = '#b45309'; }
return;
}
testBtn.disabled = true;
if (testResult) { testResult.textContent = 'Testing…'; testResult.style.color = 'var(--muted,#667085)'; }
if (testResult) { testResult.textContent = I.testTesting; testResult.style.color = 'var(--muted,#667085)'; }
fetch('/api/mcp/user/session', {
headers: { 'Authorization': 'Bearer ' + activeToken }
})