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
+463
View File
@@ -0,0 +1,463 @@
<?php
/**
* generate-mcp-translations.php
* One-shot CLI script: php scripts/generate-mcp-translations.php
*
* Produces two files:
* 1. translations/mcp-chrome.php — UI chrome strings for mcp.php + mcp-tool.php
* 2. includes/mcp-tool-translations.php — tool names, descriptions, param docs in all 4 languages
*/
declare(strict_types=1);
define('SCRIPT_MODE', true);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
// ─────────────────────────────────────────────────────────────────────────────
// Azure config (same pattern as generate-page-translations.php)
// ─────────────────────────────────────────────────────────────────────────────
$azConfig = [
'endpoint' => rtrim((string)dbnToolsEnv('DBN_AZURE_OPENAI_ENDPOINT', ''), '/'),
'api_key' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_API_KEY', ''),
'api_version' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_API_VERSION', '2024-02-01'),
'chat_deployment' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_CHAT_DEPLOYMENT', 'gpt-4o-mini'),
];
function azureChat(array $config, array $messages, array $options = []): array
{
$url = $config['endpoint']
. '/openai/deployments/'
. rawurlencode($config['chat_deployment'])
. '/chat/completions?api-version='
. rawurlencode($config['api_version']);
$payload = json_encode(array_filter([
'messages' => $messages,
'temperature' => $options['temperature'] ?? 0.1,
'max_tokens' => $options['max_tokens'] ?? 4096,
'response_format' => isset($options['json']) && $options['json']
? ['type' => 'json_object'] : null,
], fn($v) => $v !== null), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'api-key: ' . $config['api_key'],
],
CURLOPT_TIMEOUT => (int)($options['timeout'] ?? 120),
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$body = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false) throw new RuntimeException("Azure curl failed: $err");
$decoded = json_decode($body, true);
if (!is_array($decoded)) throw new RuntimeException("Azure returned non-JSON (HTTP $code)");
if ($code < 200 || $code >= 300) {
throw new RuntimeException('Azure error: ' . ($decoded['error']['message'] ?? "HTTP $code"));
}
return $decoded;
}
$languages = [
'no' => 'Norwegian (Norsk bokmål)',
'uk' => 'Ukrainian',
'pl' => 'Polish',
];
$systemPrompt = <<<'PROMPT'
You are a professional legal translator specializing in Norwegian family law and child welfare.
Translate all JSON string values to {LANG}.
PRESERVE AS-IS:
- Norwegian institution names: Barnevernet, Statsforvalteren, Bufdir, Fylkesnemnda, NAV, Tingretten
- Norwegian legal act names: forvaltningsloven, barnevernsloven, opplæringslova, EMK, fvl
- Norwegian legal terms: klage, vedtak, saksnummer, akutt, omsorgsovertakelse, tiltaksplan
- Technical terms: MCP, DBN, dbn.tool_name, JSON, Bearer token, npx, stdio
- Brand names: Do Better Norge, Claude, Cursor, VS Code, Windsurf, Copilot
- Code snippets, URLs, env var names like DBN_MCP_TOKEN
- HTML entities and tags exactly as they appear
- The symbol ← and →
Return a JSON object with EXACTLY the same keys as the input. Translate only the string values.
Use formal/professional register appropriate for a legal tools platform.
PROMPT;
function translateBatch(array $azConfig, string $langName, string $systemPrompt, array $batch): array
{
$prompt = str_replace('{LANG}', $langName, $systemPrompt);
$json = json_encode($batch, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$response = azureChat($azConfig, [
['role' => 'system', 'content' => $prompt],
['role' => 'user', 'content' => $json],
], ['json' => true, 'max_tokens' => 4096, 'timeout' => 120]);
$content = trim((string)($response['choices'][0]['message']['content'] ?? ''));
$decoded = json_decode($content, true);
if (!is_array($decoded)) {
echo " [WARN] JSON parse failed, trying extraction...\n";
if (preg_match('/\{.*\}/s', $content, $m)) {
$decoded = json_decode($m[0], true);
}
}
if (!is_array($decoded)) {
echo " [ERROR] Could not parse response, using English fallback\n";
return $batch;
}
foreach ($batch as $k => $v) {
if (!isset($decoded[$k])) $decoded[$k] = $v;
}
return $decoded;
}
function writePhpFile(string $path, array $data, string $comment): void
{
$php = "<?php\n// $comment\n// DO NOT EDIT MANUALLY — regenerate with scripts/generate-mcp-translations.php\nreturn ";
$php .= var_export($data, true);
$php .= ";\n";
file_put_contents($path, $php);
}
// ─────────────────────────────────────────────────────────────────────────────
// Part 1 — MCP UI chrome strings
// ─────────────────────────────────────────────────────────────────────────────
$chromeEn = [
// mcp.php
'mcp_page_title' => 'MCP — Do Better Norge',
'mcp_meta_desc' => 'Connect Claude, Cursor, and other AI tools to all 19 DBN legal preparation tools via MCP.',
'mcp_hero_badge' => '✦ Plus & Pro',
'mcp_hero_h1' => 'Use DBN tools from Claude, Cursor & Copilot',
'mcp_hero_sub' => 'Connect any MCP client to all 19 Do Better Norge tools — transcription, legal analysis, timelines, redaction, and more.',
'mcp_token_section_title' => 'Your MCP token',
'mcp_gate_guest_p' => 'Sign in to create your personal MCP token. Available to Plus and Pro members.',
'mcp_gate_guest_btn' => 'Sign in',
'mcp_gate_free_p' => 'MCP access is available on Plus and Pro plans. Upgrade to connect your AI tools.',
'mcp_gate_free_btn' => 'Upgrade plan',
'mcp_token_hint' => 'Tokens are shown once at creation. Create one per client (Claude, Cursor, VS Code…).',
'mcp_token_create_btn' => 'Create token',
'mcp_token_reveal_label' => 'Copy this token now — it will not be shown again:',
'mcp_token_copy_btn' => 'Copy token',
'mcp_token_no_tokens' => 'No MCP tokens yet.',
'mcp_token_active' => 'Active',
'mcp_token_revoked' => 'Revoked',
'mcp_token_never_used' => 'Never used',
'mcp_token_last_used' => 'Last used',
'mcp_token_revoke_btn' => 'Revoke',
'mcp_config_title' => 'Client configuration',
'mcp_config_hint' => 'Paste your token into the config below after creating it above.',
'mcp_config_token_filled' => 'Token auto-filled.',
'mcp_config_run_terminal' => 'Run in your terminal:',
'mcp_test_btn' => 'Test connection',
'mcp_test_no_token' => 'Create a token first.',
'mcp_test_testing' => 'Testing…',
'mcp_tools_title' => 'Available tools',
'mcp_tools_sub' => 'All tools run on your Plus or Pro plan credits. Click a card for full technical details.',
'mcp_tools_param_req_hint' => 'Purple = required',
'mcp_tools_view_details' => 'View details →',
// privacy section
'mcp_privacy_title' => 'Privacy',
'mcp_privacy_text' => 'Process-and-forget by default. 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 dbn.save_to_case.',
'mcp_privacy_legal' => 'Tools provide legal preparation support, not final legal advice. Results are for informational purposes and should be reviewed by a qualified legal professional.',
// mcp-tool.php
'mcp_tool_back' => '← Back to MCP setup',
'mcp_tool_params_title' => 'Parameters',
'mcp_tool_no_params' => 'This tool takes no input parameters.',
'mcp_tool_col_param' => 'Parameter',
'mcp_tool_col_type' => 'Type',
'mcp_tool_col_required' => 'Required',
'mcp_tool_col_desc' => 'Description',
'mcp_tool_example_req' => 'Example request',
'mcp_tool_example_resp' => 'Example response',
'mcp_tool_connect_title' => 'Connect',
'mcp_tool_connect_text' => 'Create your MCP token on the setup page and use it with any supported client.',
'mcp_tool_setup_link' => 'Set up MCP →',
'mcp_tool_yes' => 'Yes',
'mcp_tool_no' => 'No',
];
echo "\n=== Part 1: Translating MCP UI chrome strings ===\n";
$chromeTranslations = ['en' => $chromeEn];
$batchSize = 20;
$keys = array_keys($chromeEn);
$batches = array_chunk($keys, $batchSize);
foreach ($languages as $langCode => $langName) {
echo " Language: $langName ($langCode)\n";
$langResult = [];
foreach ($batches as $idx => $batchKeys) {
$batchNum = $idx + 1;
echo " Batch $batchNum/" . count($batches) . " (" . count($batchKeys) . " strings)...\n";
$batchInput = [];
foreach ($batchKeys as $k) $batchInput[$k] = $chromeEn[$k];
$langResult = array_merge($langResult, translateBatch($azConfig, $langName, $systemPrompt, $batchInput));
}
$chromeTranslations[$langCode] = $langResult;
}
$chromeOutPath = dirname(__DIR__) . '/translations/mcp-chrome.php';
writePhpFile($chromeOutPath, $chromeTranslations, 'MCP UI chrome translations — mcp.php + mcp-tool.php');
echo " Written: $chromeOutPath\n";
// ─────────────────────────────────────────────────────────────────────────────
// Part 2 — MCP tool content (display_name, description, param descriptions)
// ─────────────────────────────────────────────────────────────────────────────
// English source for all tool content — param descriptions are authored here
// since DbnMcpRuntime.php params mostly lack description fields.
$toolsEn = [
'dbn.search_legal' => [
'display_name' => 'Search DBN legal corpus',
'description' => 'Search the DBN Norwegian family-law corpus.',
'params' => [
'query' => 'The search query (minimum 3 characters). Enter a legal topic, keyword, or question.',
'language' => 'Response language: en, no, uk, pl, or auto (detect from input).',
'limit' => 'Maximum number of results to return (110).',
'corpus_scope' => 'Which corpus to search: shared (public legal corpus), private (your uploaded documents), or both.',
],
],
'dbn.ask' => [
'display_name' => 'Ask a legal question',
'description' => 'Answer a legal preparation question with source-grounded DBN context.',
'params' => [
'question' => 'The legal question to answer (minimum 5 characters).',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.summarize' => [
'display_name' => 'Summarize document',
'description' => 'Summarize pasted case text with optional legal-corpus enrichment.',
'params' => [
'text' => 'The text to summarize.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_legal_corpus' => 'Enrich the summary with relevant legal corpus passages (true/false).',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.timeline' => [
'display_name' => 'Extract timeline',
'description' => 'Extract dates, hearings, milestones, and deadlines from case text.',
'params' => [
'text' => 'The case text to extract dates from.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'focus' => 'What to extract: all (every date), deadlines (appeal windows and filing deadlines), hearings (tribunal and court dates), cps (Barnevernet milestones).',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.redact' => [
'display_name' => 'Redact private data',
'description' => 'Remove or pseudonymize names, IDs, phone numbers, addresses, and places.',
'params' => [
'text' => 'The text to redact.',
'language' => 'Language of the input text: en, no, uk, pl, or auto.',
'mode' => 'Redaction scope: standard (names, IDs, phones) or strict (also addresses, locations, institutions).',
'output_format' => 'Replacement style: contextual ([PERSON A]), generic (█████), or pseudonym (consistent invented names).',
],
],
'dbn.translate' => [
'display_name' => 'Translate legal document',
'description' => 'Translate Norwegian family-law text with legal terminology annotations.',
'params' => [
'text' => 'The text to translate.',
'source_lang' => 'Language of the input text. Use auto to detect automatically.',
'target_lang' => 'Language to translate into (required): en, no, uk, or pl.',
'doc_type' => 'Document type hint for legal terminology: auto, barnevernet, adopsjon, emergency, samvaer, fylkesnemnd, or other.',
],
],
'dbn.legal_analysis' => [
'display_name' => 'Legal analysis',
'description' => 'Extract legal issues from a document and answer each with DBN legal context.',
'params' => [
'text' => 'The document text to analyze.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'doc_type' => 'Document type hint: auto, barnevernet, adopsjon, emergency, samvaer, fylkesnemnd, or other.',
],
],
'dbn.korrespond' => [
'display_name' => 'Draft authority correspondence',
'description' => 'Draft a reply or new letter to Norwegian authorities.',
'params' => [
'narrative' => 'Description of your situation — what happened and what you need.',
'received_text' => 'Text of the letter or decision you received (for reply mode).',
'recipient_body' => 'The authority you are writing to (e.g. Barnevernet, NAV, Skole, Kommune).',
'goal' => 'Your legal goal — what you want the letter to achieve.',
'mode' => 'reply (responding to a received letter) or initiate (starting new correspondence).',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
'force_draft' => 'Skip the clarify gate and draft immediately (true/false).',
],
],
'dbn.barnevernet_analyze' => [
'display_name' => 'Analyze Barnevernet document',
'description' => 'Analyze child-welfare documents for red flags and legal issues.',
'params' => [
'document_text' => 'Full text of the child welfare document to analyze (required).',
'filename' => 'Original filename for context (helps identify document type).',
'advocate_role' => 'Your role in the case: parent, lawyer, guardian ad litem, etc.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.advocate_brief' => [
'display_name' => 'Create advocate brief',
'description' => 'Generate a source-grounded brief for a chosen party or role.',
'params' => [
'query' => 'Legal question or topic for the brief.',
'paste_text' => 'Document text to use as the basis for the brief.',
'advocate_role' => 'The party or role to advocate for (required). E.g. parent, child, Barnevernet.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.deep_research' => [
'display_name' => 'Deep research',
'description' => 'Expand a legal question into research angles and synthesize a cited brief.',
'params' => [
'query' => 'Legal question to research in depth.',
'paste_text' => 'Supporting document text to research from.',
'language' => 'Response language: en, no, uk, pl, or auto.',
'use_case_context' => 'Include context from your Case Workbench session (true/false).',
],
],
'dbn.discrepancy_find' => [
'display_name' => 'Find document discrepancies',
'description' => 'Compare two document versions for contradictions, deletions, and added claims.',
'params' => [
'document_a_text' => 'Text of the first document (required).',
'document_b_text' => 'Text of the second document to compare against the first (required).',
'filename_a' => 'Filename of the first document (for reference in the report).',
'filename_b' => 'Filename of the second document (for reference in the report).',
'language' => 'Response language: en, no, uk, pl, or auto.',
],
],
'dbn.transcribe_audio' => [
'display_name' => 'Transcribe audio',
'description' => 'Transcribe an audio file from base64-encoded content or a URL.',
'params' => [
'audio_base64' => 'Base64-encoded audio file content. Use either this or audio_url.',
'audio_url' => 'URL to a publicly accessible audio file. Use either this or audio_base64.',
'filename' => 'Original filename with extension (e.g. recording.mp3) — helps set the correct audio format.',
'language' => 'Language spoken in the audio (e.g. no, en, uk). Leave blank for auto-detection.',
'diarize' => 'Enable speaker diarization — label each segment with a speaker identifier (true/false).',
],
],
'dbn.corpus_stats' => [
'display_name' => 'Corpus statistics',
'description' => 'Return document and chunk counts and active legal sources in the DBN corpus.',
'params' => [],
],
'dbn.list_documents' => [
'display_name' => 'List corpus documents',
'description' => 'List DBN legal corpus documents with optional filters.',
'params' => [
'category' => 'Filter by document category (e.g. barnevernet, statsforvalter, echr).',
'title' => 'Filter by title — partial match.',
'limit' => 'Maximum number of documents to return (150, default 20).',
'offset' => 'Number of documents to skip (for pagination).',
],
],
'dbn.get_document' => [
'display_name' => 'Get document chunks',
'description' => 'Fetch a document and its text chunks by numeric document ID.',
'params' => [
'document_id' => 'The numeric ID of the document to retrieve (required).',
],
],
'dbn.citation_graph' => [
'display_name' => 'Explore citation graph',
'description' => 'Explore cites, cited-by, implementation, or chain relationships.',
'params' => [
'doc_id' => 'The numeric ID of the document to explore (required).',
'action' => 'Relationship to traverse: cites, cited_by, implements, or chain (full citation path).',
'depth' => 'How many levels of relationships to explore (13, default 1).',
],
],
'dbn.case_workbench_plan' => [
'display_name' => 'Plan next case step',
'description' => 'Create a stateless legal preparation plan based on your situation.',
'params' => [
'situation' => 'Describe your current legal situation and what you need help with (required).',
'goal' => 'Your desired outcome or legal goal.',
'deadline' => 'Any relevant deadline (date or description, e.g. "3 weeks").',
'language' => 'Response language: en, no, uk, pl, or auto.',
],
],
'dbn.save_to_case' => [
'display_name' => 'Save result to My Case',
'description' => 'Explicitly save a prior MCP tool result to the user case record.',
'params' => [
'tool' => 'The MCP tool slug whose result you are saving (required).',
'title' => 'A descriptive title for this saved result.',
'input_payload' => 'The input parameters used to generate the result (required).',
'output_payload' => 'The tool result to save (required).',
'meta' => 'Optional metadata (e.g. notes, tags, source reference).',
],
],
];
echo "\n=== Part 2: Translating MCP tool content ===\n";
// Flatten all tool strings for translation (display_name, description, params)
// Key format: {short_slug}__name, {short_slug}__desc, {short_slug}__p__{param}
$flatSource = [];
$slugMap = []; // short_slug => full_slug
foreach ($toolsEn as $fullSlug => $toolData) {
$shortSlug = str_replace('dbn.', '', $fullSlug);
$slugMap[$shortSlug] = $fullSlug;
$flatSource[$shortSlug . '__name'] = $toolData['display_name'];
$flatSource[$shortSlug . '__desc'] = $toolData['description'];
foreach ($toolData['params'] as $param => $desc) {
$flatSource[$shortSlug . '__p__' . $param] = $desc;
}
}
// Translate in batches of 20
$flatKeys = array_keys($flatSource);
$batches = array_chunk($flatKeys, 20);
$flatTranslations = ['en' => $flatSource];
foreach ($languages as $langCode => $langName) {
echo " Language: $langName ($langCode)\n";
$langResult = [];
foreach ($batches as $idx => $batchKeys) {
$batchNum = $idx + 1;
echo " Batch $batchNum/" . count($batches) . " (" . count($batchKeys) . " strings)...\n";
$batchInput = [];
foreach ($batchKeys as $k) $batchInput[$k] = $flatSource[$k];
$langResult = array_merge($langResult, translateBatch($azConfig, $langName, $systemPrompt, $batchInput));
}
$flatTranslations[$langCode] = $langResult;
}
// Reconstruct nested structure: [full_slug][lang] => ['display_name', 'description', 'params']
$toolTranslations = [];
foreach ($toolsEn as $fullSlug => $toolData) {
$shortSlug = str_replace('dbn.', '', $fullSlug);
foreach (['en', 'no', 'uk', 'pl'] as $lc) {
$toolTranslations[$fullSlug][$lc] = [
'display_name' => $flatTranslations[$lc][$shortSlug . '__name'] ?? $toolData['display_name'],
'description' => $flatTranslations[$lc][$shortSlug . '__desc'] ?? $toolData['description'],
'params' => [],
];
foreach (array_keys($toolData['params']) as $param) {
$toolTranslations[$fullSlug][$lc]['params'][$param] =
$flatTranslations[$lc][$shortSlug . '__p__' . $param] ?? $toolData['params'][$param];
}
}
}
$toolOutPath = dirname(__DIR__) . '/includes/mcp-tool-translations.php';
writePhpFile($toolOutPath, $toolTranslations, 'MCP tool translations — display_name, description, param descriptions');
echo " Written: $toolOutPath\n";
echo "\n✓ Done. Next steps:\n";
echo " 1. Copy translations from translations/mcp-chrome.php into includes/i18n.php\n";
echo " 2. The tool translations are ready in includes/mcp-tool-translations.php\n";