Files
daveadmin 1bfafa9908 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>
2026-05-24 12:05:07 +02:00

834 lines
29 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/DbnMcpRuntime.php';
$uiLang = dbnToolsCurrentLanguage();
// ── Load tool from ?tool= param ──────────────────────────────────────────────
$requestedSlug = (string)($_GET['tool'] ?? '');
$toolCatalog = DbnMcpRuntime::tools();
$toolDef = null;
foreach ($toolCatalog as $t) {
if (($t['slug'] ?? '') === $requestedSlug) {
$toolDef = $t;
break;
}
}
if ($toolDef === null) {
http_response_code(404);
echo '<!doctype html><html><head><title>Tool not found</title></head><body><p>MCP tool not found. <a href="/mcp.php">Back to MCP setup</a></p></body></html>';
exit;
}
// ── Load translations ─────────────────────────────────────────────────────────
$toolTranslations = [];
$toolTransFile = __DIR__ . '/includes/mcp-tool-translations.php';
if (file_exists($toolTransFile)) {
$toolTranslations = (array)include $toolTransFile;
}
function mcpToolT(string $slug, string $field, string $lang, array $trans): string
{
$fallback = $trans[$slug]['en'][$field] ?? '';
return (string)($trans[$slug][$lang][$field] ?? $fallback);
}
function mcpToolParamT(string $slug, string $param, string $lang, array $trans): string
{
$fallback = $trans[$slug]['en']['params'][$param] ?? '';
return (string)($trans[$slug][$lang]['params'][$param] ?? $fallback);
}
// ── Resolved display data ────────────────────────────────────────────────────
$slug = (string)($toolDef['slug'] ?? '');
$displayName = mcpToolT($slug, 'display_name', $uiLang, $toolTranslations) ?: (string)($toolDef['display_name'] ?? $slug);
$description = mcpToolT($slug, 'description', $uiLang, $toolTranslations) ?: (string)($toolDef['description'] ?? '');
$props = (array)($toolDef['config']['input_schema']['properties'] ?? []);
$required = (array)($toolDef['config']['input_schema']['required'] ?? []);
$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' => '💾',
];
$toolIcon = $toolIcons[$slug] ?? '🔧';
// ── Example requests (per tool) ───────────────────────────────────────────────
$exampleRequests = [
'dbn.search_legal' => <<<'JSON'
{
"query": "emergency care order requirements barnevernet",
"language": "en",
"limit": 5,
"corpus_scope": "shared"
}
JSON,
'dbn.ask' => <<<'JSON'
{
"question": "Can Barnevernet remove a child without a court hearing?",
"language": "en",
"use_case_context": false
}
JSON,
'dbn.summarize' => <<<'JSON'
{
"text": "Paste the case document text here...",
"language": "en",
"use_legal_corpus": true,
"use_case_context": false
}
JSON,
'dbn.timeline' => <<<'JSON'
{
"text": "Paste case notes or decision letter here...",
"language": "en",
"focus": "all",
"use_case_context": false
}
JSON,
'dbn.redact' => <<<'JSON'
{
"text": "On 15.03.2024 caseworker Anna Hansen met with...",
"language": "no",
"mode": "standard",
"output_format": "contextual"
}
JSON,
'dbn.translate' => <<<'JSON'
{
"text": "Barnevernet fattet vedtak om omsorgsovertakelse...",
"source_lang": "no",
"target_lang": "en",
"doc_type": "barnevernet"
}
JSON,
'dbn.legal_analysis' => <<<'JSON'
{
"text": "Paste the document text to analyze here...",
"language": "en",
"doc_type": "auto"
}
JSON,
'dbn.korrespond' => <<<'JSON'
{
"narrative": "Barnevernet issued an emergency placement order without notification.",
"recipient_body": "Barnevernet",
"goal": "Appeal the decision and request access to case documents.",
"mode": "initiate",
"language": "en"
}
JSON,
'dbn.barnevernet_analyze' => <<<'JSON'
{
"document_text": "Paste the Barnevernet document text here...",
"filename": "vedtak-omsorgsovertakelse.pdf",
"advocate_role": "parent",
"language": "en"
}
JSON,
'dbn.advocate_brief' => <<<'JSON'
{
"query": "Grounds for challenging a care order under BVL §4-12",
"advocate_role": "parent",
"language": "en",
"use_case_context": false
}
JSON,
'dbn.deep_research' => <<<'JSON'
{
"query": "ECHR Article 8 violations in Norwegian child welfare cases",
"language": "en",
"use_case_context": false
}
JSON,
'dbn.discrepancy_find' => <<<'JSON'
{
"document_a_text": "Text of the first document version...",
"document_b_text": "Text of the second document version...",
"filename_a": "report-2023.pdf",
"filename_b": "report-2024.pdf",
"language": "en"
}
JSON,
'dbn.transcribe_audio' => <<<'JSON'
{
"audio_url": "https://example.com/meeting-recording.mp3",
"language": "no",
"diarize": true
}
JSON,
'dbn.corpus_stats' => '{}',
'dbn.list_documents' => <<<'JSON'
{
"category": "barnevernet",
"limit": 20,
"offset": 0
}
JSON,
'dbn.get_document' => <<<'JSON'
{
"document_id": 42
}
JSON,
'dbn.citation_graph' => <<<'JSON'
{
"doc_id": 42,
"action": "cites",
"depth": 2
}
JSON,
'dbn.case_workbench_plan' => <<<'JSON'
{
"situation": "Barnevernet has scheduled a Fylkesnemnda hearing in 3 weeks. I need to prepare a response.",
"goal": "Challenge the care order and request return of the child.",
"deadline": "2024-06-15",
"language": "en"
}
JSON,
'dbn.save_to_case' => <<<'JSON'
{
"tool": "dbn.legal_analysis",
"title": "Legal analysis of care order decision",
"input_payload": { "text": "...", "language": "en" },
"output_payload": { "issues": [...], "summary": "..." }
}
JSON,
];
// ── Example responses (per tool) ──────────────────────────────────────────────
$exampleResponses = [
'dbn.search_legal' => <<<'JSON'
{
"ok": true,
"results": [
{
"title": "BVL §4-12 — Vedtak om omsorgsovertakelse",
"chunk_text": "§ 4-12. Vedtak om å overta omsorgen for et barn kan treffes dersom...",
"score": 0.91,
"source": "barnevernsloven"
},
{
"title": "NOU 2012:5 — Bedre beskyttelse av barns utvikling",
"chunk_text": "Utvalget vurderer at terskelen for akuttvedtak bør klargjøres...",
"score": 0.84,
"source": "nou"
}
],
"total": 2
}
JSON,
'dbn.ask' => <<<'JSON'
{
"ok": true,
"answer": "Under Norwegian law, Barnevernet may issue an emergency care order (akuttvedtak) under BVL §4-6 without a prior court hearing if there is immediate risk of serious harm. However, the case must be reviewed by Fylkesnemnda within six weeks...",
"sources": [
{ "title": "BVL §4-6 — Midlertidige vedtak", "url": "/corpus/bvl-4-6" },
{ "title": "Strand Lobben m.fl. mot Norge (EMD)", "url": "/corpus/echr-strand-lobben" }
],
"credits_used": 3
}
JSON,
'dbn.summarize' => <<<'JSON'
{
"ok": true,
"summary": "The document is a Barnevernet investigation report concerning a family with two children. Key findings include...",
"key_points": [
"Home visit conducted 12.03.2024 — no immediate danger found",
"Tiltaksplan proposed with three support measures",
"Next review scheduled for 15.06.2024"
],
"credits_used": 5
}
JSON,
'dbn.timeline' => <<<'JSON'
{
"ok": true,
"events": [
{
"date": "2024-03-12",
"date_type": "ABSOLUTE",
"confidence": "HIGH",
"actor": "Barnevernet",
"description": "Home visit conducted following anonymous report",
"source_excerpt": "12.03.2024: hjemmebesøk ble gjennomført..."
},
{
"date": "2024-06-15",
"date_type": "ABSOLUTE",
"confidence": "HIGH",
"actor": "Fylkesnemnda",
"description": "Scheduled hearing date",
"source_excerpt": "Nemnda berammer møte til 15. juni 2024"
}
],
"next_practical_step": "File your written submission before the 06.06.2024 deadline.",
"credits_used": 4
}
JSON,
'dbn.redact' => <<<'JSON'
{
"ok": true,
"redacted_text": "On [DATE A] caseworker [PERSON A] met with [PERSON B] at [ADDRESS A]...",
"replacements": [
{ "original": "15.03.2024", "replacement": "[DATE A]" },
{ "original": "Anna Hansen", "replacement": "[PERSON A]" },
{ "original": "Ola Nordmann", "replacement": "[PERSON B]" }
],
"credits_used": 2
}
JSON,
'dbn.translate' => <<<'JSON'
{
"ok": true,
"source_lang": "no",
"target_lang": "en",
"translated_text": "Barnevernet issued a care order decision (vedtak om omsorgsovertakelse)...",
"annotations": [
{
"term": "omsorgsovertakelse",
"note": "Care order — formal transfer of parental responsibility to the state under BVL §4-12"
},
{
"term": "Fylkesnemnda",
"note": "County Social Welfare Board — the quasi-judicial body that reviews Barnevernet decisions"
}
],
"credits_used": 6
}
JSON,
'dbn.legal_analysis' => <<<'JSON'
{
"ok": true,
"issues": [
{
"issue": "Procedural rights under forvaltningsloven §17",
"analysis": "The parent was not given an opportunity to be heard before the decision was made, potentially violating fvl §17...",
"legal_basis": ["forvaltningsloven §17", "BVL §6-3"],
"sources": [{ "title": "fvl §17 — Plikten til å påse at saken er opplyst" }]
}
],
"overall_assessment": "The document shows potential procedural violations that may form grounds for an appeal.",
"credits_used": 8
}
JSON,
'dbn.korrespond' => <<<'JSON'
{
"ok": true,
"letter_no": "Til Barnevernet\nDato: 24.05.2024\n\nAnke på vedtak om omsorgsovertakelse...",
"letter_en": "To Barnevernet\nDate: 24.05.2024\n\nAppeal against care order decision...",
"cited_sources": ["BVL §4-12", "forvaltningsloven §28", "EMK artikkel 8"],
"credits_used": 10
}
JSON,
'dbn.barnevernet_analyze' => <<<'JSON'
{
"ok": true,
"red_flags": [
"Decision issued without notification to parent (fvl §16 violation)",
"No tiltaksplan offered before escalating to care order (BVL §4-4)"
],
"legal_issues": [
"Potential violation of proportionality principle — less intrusive measures not exhausted",
"ECHR Art. 8 family life rights not addressed in reasoning"
],
"recommendations": [
"File an appeal within 3 weeks citing fvl §28",
"Request full case file under fvl §18",
"Consult Fylkesnemnda procedural timeline"
],
"credits_used": 7
}
JSON,
'dbn.advocate_brief' => <<<'JSON'
{
"ok": true,
"brief": "On behalf of the parent, the following legal arguments support challenging the care order under BVL §4-12...",
"legal_arguments": [
"The threshold for care order under §4-12(a) requires 'serious deficiencies' — the report does not meet this standard",
"Proportionality requires exhausting support measures (§4-4) before removal"
],
"sources": [
{ "title": "BVL §4-12", "type": "statute" },
{ "title": "Rt-2015-110", "type": "case_law" }
],
"credits_used": 9
}
JSON,
'dbn.deep_research' => <<<'JSON'
{
"ok": true,
"research_angles": [
"Procedural safeguards under ECHR Art. 6 in child welfare proceedings",
"Substantive requirements for care order threshold (BVL §4-12)",
"Norwegian Supreme Court interpretation of best interests standard"
],
"brief": "Research synthesis: The European Court of Human Rights has found Norway in violation of Art. 8 in 14 cases since 2015, with the core issue being...",
"citations": [
"Strand Lobben m.fl. mot Norge, EMD-37283/13 (10.09.2019)",
"Johansen mot Norge, EMD-17383/90 (07.08.1996)"
],
"credits_used": 15
}
JSON,
'dbn.discrepancy_find' => <<<'JSON'
{
"ok": true,
"discrepancies": [
{
"type": "deletion",
"location": "Section 3, paragraph 2",
"original": "The child expressed a wish to return home.",
"modified": "[removed]",
"significance": "HIGH — child's stated preference is a legally relevant factor under BVL §6-3"
},
{
"type": "added_claim",
"location": "Section 5",
"original": "[absent]",
"modified": "The parent failed to attend the scheduled meeting.",
"significance": "MEDIUM — unverified claim added in the later version"
}
],
"credits_used": 6
}
JSON,
'dbn.transcribe_audio' => <<<'JSON'
{
"ok": true,
"transcript": "Full transcript of the audio recording...",
"segments": [
{ "speaker": "A", "start": 0.0, "end": 4.2, "text": "God morgen, vi er samlet for å diskutere..." },
{ "speaker": "B", "start": 4.5, "end": 9.1, "text": "Takk. Jeg ønsker å starte med å si at..." }
],
"language_detected": "no",
"credits_used": 8
}
JSON,
'dbn.corpus_stats' => <<<'JSON'
{
"ok": true,
"documents": 2847,
"chunks": 94231,
"sources": {
"statutes": 42,
"tribunal_decisions": 1204,
"echr_cases": 187,
"government_reports": 1414
},
"last_updated": "2024-05-01"
}
JSON,
'dbn.list_documents' => <<<'JSON'
{
"ok": true,
"documents": [
{ "id": 42, "title": "BVL §4-12 — Vedtak om omsorgsovertakelse", "category": "statute" },
{ "id": 87, "title": "FNV-2023-142 — Fylkesnemnda for barnevern og sosiale saker", "category": "tribunal" },
{ "id": 203, "title": "Strand Lobben m.fl. mot Norge (EMD-37283/13)", "category": "echr" }
],
"total": 847,
"offset": 0
}
JSON,
'dbn.get_document' => <<<'JSON'
{
"ok": true,
"document": {
"id": 42,
"title": "BVL §4-12 — Vedtak om omsorgsovertakelse",
"category": "statute",
"chunks": [
{ "chunk_id": 1, "text": "§ 4-12. Vedtak om å overta omsorgen for et barn kan treffes dersom..." },
{ "chunk_id": 2, "text": "(a) det er alvorlige mangler ved den daglige omsorg som barnet får..." }
]
}
}
JSON,
'dbn.citation_graph' => <<<'JSON'
{
"ok": true,
"action": "cites",
"root": { "id": 42, "title": "BVL §4-12" },
"nodes": [
{ "id": 42, "title": "BVL §4-12", "type": "statute" },
{ "id": 15, "title": "BVL §4-4 — Hjelpetiltak", "type": "statute" },
{ "id": 203, "title": "Strand Lobben m.fl. mot Norge", "type": "echr" }
],
"edges": [
{ "from": 42, "to": 15, "type": "cites" },
{ "from": 42, "to": 203, "type": "cites" }
],
"depth": 1
}
JSON,
'dbn.case_workbench_plan' => <<<'JSON'
{
"ok": true,
"plan": {
"situation_summary": "Fylkesnemnda hearing scheduled in 3 weeks — care order challenge.",
"steps": [
"1. Request full case file under forvaltningsloven §18 — submit by 01.06.2024",
"2. Run Legal Analysis tool on the original care order decision",
"3. Create Advocate Brief (parent role) with the grounds for challenge",
"4. Draft formal response letter using Korrespond (Fylkesnemnda preset)",
"5. Compile timeline of all procedural dates"
],
"recommended_tools": ["dbn.legal_analysis", "dbn.advocate_brief", "dbn.korrespond", "dbn.timeline"],
"deadline": "2024-06-15"
},
"credits_used": 3
}
JSON,
'dbn.save_to_case' => <<<'JSON'
{
"ok": true,
"saved": true,
"case_entry_id": "entry_abc123",
"title": "Legal analysis of care order decision",
"saved_at": "2024-05-24T09:15:00Z"
}
JSON,
];
$exReq = trim($exampleRequests[$slug] ?? '{}');
$exResp = trim($exampleResponses[$slug] ?? '{"ok":true}');
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($displayName) ?> — <?= htmlspecialchars(dbnToolsT('mcp_page_title', $uiLang)) ?></title>
<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-tool-breadcrumb {
font-size: 0.85rem;
color: var(--muted, #667085);
margin-bottom: 1.5rem;
}
.mcp-tool-breadcrumb a {
color: var(--dbn-blue, #00205b);
text-decoration: none;
}
.mcp-tool-breadcrumb a:hover { text-decoration: underline; }
.mcp-tool-hero {
margin-bottom: 0.5rem;
}
.mcp-tool-hero__icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.mcp-tool-hero__name {
font-family: 'Crimson Pro', serif;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
color: var(--dbn-blue, #00205b);
margin: 0 0 0.2rem;
}
.mcp-tool-hero__slug {
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-size: 0.82rem;
color: var(--muted, #667085);
margin-bottom: 0.75rem;
}
.mcp-tool-hero__desc {
font-size: 1rem;
color: var(--dbn-ink, #16130f);
max-width: 640px;
margin: 0;
}
/* Param table */
.mcp-param-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
margin-top: 0.75rem;
}
.mcp-param-table th {
text-align: left;
padding: 0.5rem 0.75rem;
background: var(--dbn-paper, #f6f2ea);
font-weight: 600;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--muted, #667085);
border-bottom: 2px solid var(--dbn-line, #e5e7eb);
}
.mcp-param-table td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--dbn-line, #e5e7eb);
vertical-align: top;
}
.mcp-param-table tr:last-child td { border-bottom: 0; }
.mcp-param-name {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
}
.mcp-param-name--required {
background: #ddd6fe;
color: #5b21b6;
padding: 0.15rem 0.4rem;
border-radius: 4px;
}
.mcp-param-name--optional {
background: #f3f4f6;
color: #374151;
padding: 0.15rem 0.4rem;
border-radius: 4px;
}
.mcp-param-type {
color: var(--muted, #667085);
font-size: 0.8rem;
}
.mcp-param-enum {
font-size: 0.75rem;
color: var(--muted, #667085);
font-family: monospace;
}
.mcp-req-yes {
color: #065f46;
font-weight: 600;
}
.mcp-req-no {
color: var(--muted, #667085);
}
/* Code blocks (reuse mcp.php style) */
.mcp-code-wrap {
position: relative;
}
.mcp-code-wrap pre {
background: #101828;
color: #f9fafb;
padding: 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); }
.mcp-code-label {
font-size: 0.78rem;
font-weight: 600;
color: var(--muted, #667085);
text-transform: uppercase;
letter-spacing: .04em;
margin: 0 0 0.4rem;
}
.mcp-privacy {
background: var(--dbn-paper, #f6f2ea);
border-radius: 8px;
padding: 1rem 1.25rem;
font-size: 0.85rem;
color: var(--dbn-ink, #16130f);
}
.mcp-privacy strong { color: var(--dbn-blue, #00205b); }
.mcp-connect-link {
display: inline-block;
margin-top: 0.75rem;
color: var(--dbn-blue, #00205b);
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
}
.mcp-connect-link:hover { text-decoration: underline; }
</style>
</head>
<body data-authenticated="<?= dbnToolsIsAuthenticated() ? 'true' : 'false' ?>" class="lt-app">
<script>
window.DBN_TOOLS_AUTHENTICATED = <?= dbnToolsIsAuthenticated() ? 'true' : 'false' ?>;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<div class="account-shell">
<!-- ── Breadcrumb ────────────────────────────────────────────── -->
<div class="mcp-tool-breadcrumb">
<a href="/mcp.php"><?= htmlspecialchars(dbnToolsT('mcp_tool_back', $uiLang)) ?></a>
</div>
<!-- ── Hero ──────────────────────────────────────────────────── -->
<section class="account-section">
<div class="mcp-tool-hero">
<div class="mcp-tool-hero__icon"><?= $toolIcon ?></div>
<h1 class="mcp-tool-hero__name"><?= htmlspecialchars($displayName) ?></h1>
<div class="mcp-tool-hero__slug"><?= htmlspecialchars($slug) ?></div>
<p class="mcp-tool-hero__desc"><?= htmlspecialchars($description) ?></p>
</div>
</section>
<!-- ── Parameters ────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_tool_params_title', $uiLang)) ?></p>
<?php if (empty($props)): ?>
<p style="color:var(--muted,#667085); font-size:0.88rem;">
<?= htmlspecialchars(dbnToolsT('mcp_tool_no_params', $uiLang)) ?>
</p>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="mcp-param-table">
<thead>
<tr>
<th><?= htmlspecialchars(dbnToolsT('mcp_tool_col_param', $uiLang)) ?></th>
<th><?= htmlspecialchars(dbnToolsT('mcp_tool_col_type', $uiLang)) ?></th>
<th><?= htmlspecialchars(dbnToolsT('mcp_tool_col_required', $uiLang)) ?></th>
<th><?= htmlspecialchars(dbnToolsT('mcp_tool_col_desc', $uiLang)) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($props as $paramName => $paramDef):
$isReq = in_array($paramName, $required, true);
$paramType = (string)($paramDef['type'] ?? 'string');
$enumVals = (array)($paramDef['enum'] ?? []);
$paramDesc = mcpToolParamT($slug, $paramName, $uiLang, $toolTranslations);
if ($paramDesc === '') {
$paramDesc = (string)($paramDef['description'] ?? '');
}
?>
<tr>
<td>
<span class="mcp-param-name <?= $isReq ? 'mcp-param-name--required' : 'mcp-param-name--optional' ?>">
<?= htmlspecialchars($paramName) ?><?= $isReq ? '*' : '' ?>
</span>
</td>
<td>
<span class="mcp-param-type"><?= htmlspecialchars($paramType) ?></span>
<?php if (!empty($enumVals)): ?>
<br><span class="mcp-param-enum"><?= htmlspecialchars(implode(' | ', $enumVals)) ?></span>
<?php endif; ?>
</td>
<td>
<?php if ($isReq): ?>
<span class="mcp-req-yes"><?= htmlspecialchars(dbnToolsT('mcp_tool_yes', $uiLang)) ?></span>
<?php else: ?>
<span class="mcp-req-no">—</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($paramDesc) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<p style="font-size:0.75rem; color:var(--muted,#667085); margin-top:0.5rem;">
* <?= htmlspecialchars(dbnToolsT('mcp_tools_param_req_hint', $uiLang)) ?>
</p>
<?php endif; ?>
</section>
<!-- ── Example request ───────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_tool_example_req', $uiLang)) ?></p>
<p class="mcp-code-label">POST /api/mcp/user/tools/<?= htmlspecialchars($slug) ?>/invoke</p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" id="copyReqBtn">Copy</button>
<pre><code id="exReqCode"><?= htmlspecialchars($exReq) ?></code></pre>
</div>
</section>
<!-- ── Example response ──────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_tool_example_resp', $uiLang)) ?></p>
<div class="mcp-code-wrap">
<button class="mcp-copy-btn" type="button" id="copyRespBtn">Copy</button>
<pre><code id="exRespCode"><?= htmlspecialchars($exResp) ?></code></pre>
</div>
</section>
<!-- ── Connect ───────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('mcp_tool_connect_title', $uiLang)) ?></p>
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
<?= htmlspecialchars(dbnToolsT('mcp_tool_connect_text', $uiLang)) ?>
</p>
<a href="/mcp.php" class="mcp-connect-link"><?= htmlspecialchars(dbnToolsT('mcp_tool_setup_link', $uiLang)) ?></a>
</section>
<!-- ── Privacy ───────────────────────────────────────────────── -->
<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:0.75rem; 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';
function copyText(text, btn) {
var orig = btn.textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function () {
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = orig; }, 1500);
});
} else {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.textContent = 'Copied!';
setTimeout(function () { btn.textContent = orig; }, 1500);
}
}
var reqBtn = document.getElementById('copyReqBtn');
var respBtn = document.getElementById('copyRespBtn');
var reqCode = document.getElementById('exReqCode');
var respCode = document.getElementById('exRespCode');
if (reqBtn && reqCode) reqBtn.addEventListener('click', function () { copyText(reqCode.textContent, reqBtn); });
if (respBtn && respCode) respBtn.addEventListener('click', function () { copyText(respCode.textContent, respBtn); });
}());
</script>
</body>
</html>