feat(mcp): add official Anthropic docs tracker — v1.1.0

New tools (4):
- init_official_docs()    — fetch + store baseline + current snapshots locally
- refresh_official_docs() — update current without touching baseline
- diff_official_docs()    — compare baseline vs current, 0 network, section-level
- search_official_docs(query) — search official docs, loads only matching sections

Architecture:
- 4 local cache files in ~/.cache/claude-code-guide/ (index + content, baseline + current)
- Diff reads index files only (~50KB each), never the full 1.2MB content
- search loads only matched sections from content file (not entire doc)
- Atomic writes (.tmp + rename) prevent snapshot corruption
- schemaVersion: 1 for future-proof migrations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Florian BRUNIAUX 2026-03-11 17:28:25 +01:00
parent 4dd479161d
commit 5b713db6fd
6 changed files with 750 additions and 3 deletions

View file

@ -1,12 +1,12 @@
{
"name": "claude-code-ultimate-guide-mcp",
"version": "1.0.3",
"version": "1.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-code-ultimate-guide-mcp",
"version": "1.0.3",
"version": "1.0.4",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",

View file

@ -1,6 +1,6 @@
{
"name": "claude-code-ultimate-guide-mcp",
"version": "1.0.4",
"version": "1.1.0",
"description": "MCP server for the Claude Code Ultimate Guide — search, read, and explore 20K+ lines of documentation directly from Claude Code",
"keywords": [
"mcp",

View file

@ -0,0 +1,221 @@
import { createHash } from 'crypto';
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { resolve } from 'path';
const ANTHROPIC_DOCS_URL = 'https://code.claude.com/docs/llms-full.txt';
// Stable path — NOT versioned, survives package upgrades
const SNAPSHOT_DIR = resolve(homedir(), '.cache', 'claude-code-guide');
// ── Types ──────────────────────────────────────────────────────────────────
export interface SectionMeta {
slug: string;
title: string;
sourceUrl: string;
hash: string;
lineCount: number;
charCount: number;
}
export interface IndexFile {
schemaVersion: 1;
fetchedAt: string;
contentHash: string;
sectionCount: number;
sections: Record<string, SectionMeta>;
}
export interface ContentFile {
schemaVersion: 1;
sections: Record<string, { content: string }>;
}
export type SnapshotType = 'baseline' | 'current';
// ── Paths ──────────────────────────────────────────────────────────────────
export function getSnapshotDir(): string {
return SNAPSHOT_DIR;
}
function indexPath(type: SnapshotType): string {
return resolve(SNAPSHOT_DIR, `anthropic-docs-${type}-index.json`);
}
function contentPath(type: SnapshotType): string {
return resolve(SNAPSHOT_DIR, `anthropic-docs-${type}-content.json`);
}
// ── Fetch ──────────────────────────────────────────────────────────────────
export async function fetchOfficialDocs(): Promise<string> {
const response = await fetch(ANTHROPIC_DOCS_URL, {
headers: { 'User-Agent': 'claude-code-ultimate-guide-mcp/1.1.0' },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} fetching ${ANTHROPIC_DOCS_URL}`);
}
const text = await response.text();
if (text.length < 500_000) {
throw new Error(
`Response looks malformed (got ${(text.length / 1024).toFixed(0)}KB, expected >500KB). The URL format may have changed.`,
);
}
return text;
}
// ── Parsing ────────────────────────────────────────────────────────────────
export function parseIntoSections(raw: string): { index: IndexFile; content: ContentFile } {
const lines = raw.split('\n');
const sections: Record<string, SectionMeta> = {};
const contents: Record<string, { content: string }> = {};
// Tracking state for the current section being built
let currentSlug: string | null = null;
let currentTitle: string | null = null;
let currentSourceUrl: string | null = null;
let currentStartLine = 0;
// Title candidate (# H1) waiting to be confirmed by a Source: line
let titleCandidate: string | null = null;
let titleCandidateLine = -1;
const flushSection = (endLine: number) => {
if (!currentSlug || !currentTitle || !currentSourceUrl) return;
const sectionLines = lines.slice(currentStartLine, endLine);
const content = sectionLines.join('\n').trimEnd();
if (content.length === 0) return;
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
sections[currentSlug] = {
slug: currentSlug,
title: currentTitle,
sourceUrl: currentSourceUrl,
hash,
lineCount: sectionLines.length,
charCount: content.length,
};
contents[currentSlug] = { content };
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Detect H1 title (# Title, but not ##+ headings)
if (line.startsWith('# ') && !line.startsWith('## ')) {
titleCandidate = line;
titleCandidateLine = i;
continue;
}
// Detect Source: URL immediately after H1 (line N+1 rule)
const sourceMatch = line.match(
/^Source:\s+(https:\/\/code\.claude\.com\/docs\/(?:en\/)?([a-zA-Z0-9-]+))\s*$/,
);
if (sourceMatch && titleCandidate !== null && i === titleCandidateLine + 1) {
const sourceUrl = sourceMatch[1];
const slug = sourceMatch[2];
// Flush the previous section up to the start of this new one
flushSection(titleCandidateLine);
currentSlug = slug;
currentTitle = titleCandidate;
currentSourceUrl = sourceUrl;
currentStartLine = titleCandidateLine;
titleCandidate = null;
titleCandidateLine = -1;
continue;
}
// Reset title candidate if not immediately followed by Source:
if (titleCandidate !== null && i > titleCandidateLine + 1) {
titleCandidate = null;
titleCandidateLine = -1;
}
}
// Flush the last section
flushSection(lines.length);
if (Object.keys(sections).length === 0) {
throw new Error(
'Section parser found 0 sections — llms-full.txt format may have changed. ' +
'Please open an issue at https://github.com/FlorianBruniaux/claude-code-ultimate-guide',
);
}
const contentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
const index: IndexFile = {
schemaVersion: 1,
fetchedAt: new Date().toISOString(),
contentHash,
sectionCount: Object.keys(sections).length,
sections,
};
const content: ContentFile = {
schemaVersion: 1,
sections: contents,
};
return { index, content };
}
// ── Save / Load ────────────────────────────────────────────────────────────
export function saveSnapshot(type: SnapshotType, index: IndexFile, content: ContentFile): void {
mkdirSync(SNAPSHOT_DIR, { recursive: true });
// Atomic write: write to .tmp then rename to final path
const iPath = indexPath(type);
writeFileSync(`${iPath}.tmp`, JSON.stringify(index, null, 2), 'utf8');
renameSync(`${iPath}.tmp`, iPath);
const cPath = contentPath(type);
writeFileSync(`${cPath}.tmp`, JSON.stringify(content, null, 2), 'utf8');
renameSync(`${cPath}.tmp`, cPath);
}
export function loadIndex(type: SnapshotType): IndexFile | null {
const path = indexPath(type);
if (!existsSync(path)) return null;
try {
const data = JSON.parse(readFileSync(path, 'utf8')) as IndexFile;
if (data.schemaVersion !== 1) return null;
return data;
} catch {
return null;
}
}
/** Load only the specified sections from the content file — never loads everything into memory */
export function loadSections(
type: SnapshotType,
slugs: string[],
): Record<string, string> {
const path = contentPath(type);
if (!existsSync(path)) return {};
try {
const data = JSON.parse(readFileSync(path, 'utf8')) as ContentFile;
const result: Record<string, string> = {};
for (const slug of slugs) {
const section = data.sections[slug];
if (section) result[slug] = section.content;
}
return result;
} catch {
return {};
}
}
export function snapshotAgeDays(index: IndexFile): number {
return Math.floor((Date.now() - new Date(index.fetchedAt).getTime()) / (1000 * 60 * 60 * 24));
}

View file

@ -0,0 +1,105 @@
import type { IndexFile } from './docs-cache.js';
// ── Types ──────────────────────────────────────────────────────────────────
export interface SectionDiff {
slug: string;
title: string;
sourceUrl: string;
kind: 'added' | 'removed' | 'modified';
// Only for 'modified'
linesBefore?: number;
linesAfter?: number;
charsBefore?: number;
charsAfter?: number;
lineDelta?: number;
firstChangedLine?: string;
}
export interface DiffResult {
unchanged: number;
added: SectionDiff[];
removed: SectionDiff[];
modified: SectionDiff[];
baselineDate: string;
currentDate: string;
}
// ── Diff engine (pure — no I/O, no network) ────────────────────────────────
export function diffSnapshots(baseline: IndexFile, current: IndexFile): DiffResult {
const baselineSlugs = new Set(Object.keys(baseline.sections));
const currentSlugs = new Set(Object.keys(current.sections));
const added: SectionDiff[] = [];
const removed: SectionDiff[] = [];
const modified: SectionDiff[] = [];
let unchanged = 0;
for (const slug of currentSlugs) {
if (!baselineSlugs.has(slug)) {
const s = current.sections[slug];
added.push({ slug, title: s.title, sourceUrl: s.sourceUrl, kind: 'added' });
}
}
for (const slug of baselineSlugs) {
if (!currentSlugs.has(slug)) {
const s = baseline.sections[slug];
removed.push({ slug, title: s.title, sourceUrl: s.sourceUrl, kind: 'removed' });
}
}
for (const slug of baselineSlugs) {
if (!currentSlugs.has(slug)) continue;
const base = baseline.sections[slug];
const curr = current.sections[slug];
if (base.hash === curr.hash) {
unchanged++;
continue;
}
modified.push({
slug,
title: curr.title,
sourceUrl: curr.sourceUrl,
kind: 'modified',
linesBefore: base.lineCount,
linesAfter: curr.lineCount,
charsBefore: base.charCount,
charsAfter: curr.charCount,
lineDelta: curr.lineCount - base.lineCount,
// firstChangedLine is enriched by the tool layer after loading content
});
}
return {
unchanged,
added,
removed,
modified,
baselineDate: baseline.fetchedAt,
currentDate: current.fetchedAt,
};
}
/** Find the first line that differs between two content strings. Returns truncated to 120 chars. */
export function findFirstChangedLine(baseContent: string, currentContent: string): string {
const baseLines = baseContent.split('\n');
const currLines = currentContent.split('\n');
const len = Math.min(baseLines.length, currLines.length);
for (let i = 0; i < len; i++) {
if (baseLines[i] !== currLines[i]) {
return currLines[i].slice(0, 120);
}
}
// One side is longer — the first extra line is the change
if (currLines.length > baseLines.length) {
return (currLines[baseLines.length] ?? '').slice(0, 120);
}
return '';
}

View file

@ -10,6 +10,7 @@ import { registerReleases } from './tools/releases.js';
import { registerCompareVersions } from './tools/compare-versions.js';
import { registerGetThreat, registerListThreats } from './tools/threats.js';
import { registerSearchExamples } from './tools/search-examples.js';
import { registerOfficialDocs } from './tools/official-docs.js';
import { registerResources } from './resources/index.js';
import { registerPrompts } from './prompts/index.js';
import { loadReference, loadReleases, isDevMode } from './lib/content.js';
@ -45,6 +46,7 @@ export function createServer(): McpServer {
registerGetThreat(server);
registerListThreats(server);
registerSearchExamples(server);
registerOfficialDocs(server);
// Register resources
registerResources(server);

View file

@ -0,0 +1,419 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import {
fetchOfficialDocs,
parseIntoSections,
saveSnapshot,
loadIndex,
loadSections,
snapshotAgeDays,
getSnapshotDir,
} from '../lib/docs-cache.js';
import { diffSnapshots, findFirstChangedLine } from '../lib/docs-diff.js';
const STALENESS_WARN_DAYS = 30;
// ── Helpers ────────────────────────────────────────────────────────────────
function formatDate(iso: string): string {
return iso.slice(0, 10); // YYYY-MM-DD
}
function formatSize(bytes: number): string {
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(2)} MB`;
return `${(bytes / 1024).toFixed(0)} KB`;
}
/** Extract a ~300-char excerpt from content around the first match of query. */
function extractExcerpt(content: string, query: string, maxLen = 300): string {
const lowerContent = content.toLowerCase();
const lowerQuery = query.toLowerCase();
const idx = lowerContent.indexOf(lowerQuery);
if (idx === -1) {
// No match in body — return opening
const excerpt = content.slice(0, maxLen);
return excerpt + (content.length > maxLen ? '…' : '');
}
const start = Math.max(0, idx - 100);
const end = Math.min(content.length, idx + query.length + 200);
const prefix = start > 0 ? '…' : '';
const suffix = end < content.length ? '…' : '';
return prefix + content.slice(start, end) + suffix;
}
/** Score a section for a query. Title matches count ×3, body match ×1. */
function scoreSection(title: string, content: string, query: string): number {
const q = query.toLowerCase();
const titleScore = title.toLowerCase().includes(q) ? 3 : 0;
const bodyScore = content.toLowerCase().includes(q) ? 1 : 0;
return titleScore + bodyScore;
}
// ── Tool registration ──────────────────────────────────────────────────────
export function registerOfficialDocs(server: McpServer): void {
// ── init_official_docs ──────────────────────────────────────────────────
server.tool(
'init_official_docs',
'Fetch the official Anthropic Claude Code docs (llms-full.txt) and store a local snapshot as the diff baseline. Run this first. Safe to re-run — overwrites previous baseline AND current. Takes ~5s (fetches ~1.2MB from Anthropic).',
{},
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
async () => {
let raw: string;
try {
raw = await fetchOfficialDocs();
} catch (err) {
return {
content: [{
type: 'text',
text: `Failed to fetch official docs: ${err instanceof Error ? err.message : String(err)}\n\nCheck network connectivity and retry.`,
}],
isError: true,
};
}
let parsed: ReturnType<typeof parseIntoSections>;
try {
parsed = parseIntoSections(raw);
} catch (err) {
return {
content: [{
type: 'text',
text: `Parsing failed: ${err instanceof Error ? err.message : String(err)}`,
}],
isError: true,
};
}
try {
// init sets BOTH baseline and current
saveSnapshot('baseline', parsed.index, parsed.content);
saveSnapshot('current', parsed.index, parsed.content);
} catch (err) {
return {
content: [{
type: 'text',
text: `Failed to save snapshot to disk: ${err instanceof Error ? err.message : String(err)}\n\nCheck that ${getSnapshotDir()} is writable.`,
}],
isError: true,
};
}
const { index } = parsed;
const totalChars = Object.values(index.sections).reduce((s, sec) => s + sec.charCount, 0);
const lines = [
'# Official Docs Snapshot Created',
'',
`Fetched: ${index.fetchedAt}`,
`Sections: ${index.sectionCount}`,
`Total size: ${formatSize(totalChars)}`,
`Snapshot dir: ${getSnapshotDir()}`,
'',
'Both baseline and current snapshots have been set.',
'',
'Next steps:',
' - Run refresh_official_docs() to update current without touching the baseline',
' - Run diff_official_docs() to see changes between baseline and current',
' - Run search_official_docs(query) to search the official docs',
];
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
// ── refresh_official_docs ───────────────────────────────────────────────
server.tool(
'refresh_official_docs',
'Re-fetch the official Anthropic Claude Code docs and update the "current" snapshot without touching the baseline. Run this to update the comparison target before diffing. Takes ~5s (fetches ~1.2MB from Anthropic).',
{},
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
async () => {
const baseline = loadIndex('baseline');
if (!baseline) {
return {
content: [{
type: 'text',
text: 'No baseline snapshot found. Run init_official_docs() first to create both the baseline and the initial current snapshot.',
}],
isError: true,
};
}
let raw: string;
try {
raw = await fetchOfficialDocs();
} catch (err) {
return {
content: [{
type: 'text',
text: `Failed to fetch official docs: ${err instanceof Error ? err.message : String(err)}\n\nYour existing snapshots are intact.`,
}],
isError: true,
};
}
let parsed: ReturnType<typeof parseIntoSections>;
try {
parsed = parseIntoSections(raw);
} catch (err) {
return {
content: [{
type: 'text',
text: `Parsing failed: ${err instanceof Error ? err.message : String(err)}\n\nYour existing snapshots are intact.`,
}],
isError: true,
};
}
try {
saveSnapshot('current', parsed.index, parsed.content);
} catch (err) {
return {
content: [{
type: 'text',
text: `Failed to save current snapshot: ${err instanceof Error ? err.message : String(err)}`,
}],
isError: true,
};
}
// Quick preview: run the diff to show a summary
const diff = diffSnapshots(baseline, parsed.index);
const total = diff.added.length + diff.removed.length + diff.modified.length;
const lines = [
'# Official Docs — Current Snapshot Updated',
'',
`Fetched: ${parsed.index.fetchedAt}`,
`Sections: ${parsed.index.sectionCount}`,
'',
];
if (total === 0) {
lines.push(`No changes vs baseline (${formatDate(baseline.fetchedAt)}). All ${diff.unchanged} sections unchanged.`);
} else {
lines.push(`vs baseline (${formatDate(baseline.fetchedAt)}): ${diff.added.length} added, ${diff.removed.length} removed, ${diff.modified.length} modified, ${diff.unchanged} unchanged`);
lines.push('');
lines.push('Run diff_official_docs() for the full diff.');
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
// ── diff_official_docs ──────────────────────────────────────────────────
server.tool(
'diff_official_docs',
'Compare the baseline and current official Anthropic Claude Code docs snapshots. Shows added, removed, and modified pages. No network call — reads local files only. Run init_official_docs() first, then refresh_official_docs() when you want to update the current snapshot.',
{},
{ readOnlyHint: true, destructiveHint: false, openWorldHint: false },
async () => {
const baseline = loadIndex('baseline');
if (!baseline) {
return {
content: [{
type: 'text',
text: 'No baseline found. Run init_official_docs() first to create a baseline snapshot.',
}],
isError: true,
};
}
const current = loadIndex('current');
if (!current) {
return {
content: [{
type: 'text',
text: 'No current snapshot found. Run refresh_official_docs() to fetch the latest docs.',
}],
isError: true,
};
}
// Fast path: identical content hashes
if (baseline.contentHash === current.contentHash) {
const ageDays = snapshotAgeDays(current);
const staleWarn = ageDays > STALENESS_WARN_DAYS
? `\n⚠ Current snapshot is ${ageDays} days old — consider running refresh_official_docs()`
: '';
return {
content: [{
type: 'text',
text: [
'# Official Docs Diff',
'',
`Baseline: ${formatDate(baseline.fetchedAt)} | Current: ${formatDate(current.fetchedAt)}`,
'',
`No changes detected. All ${baseline.sectionCount} sections unchanged.${staleWarn}`,
'',
'Run refresh_official_docs() to fetch latest docs and update current.',
].join('\n'),
}],
};
}
const diff = diffSnapshots(baseline, current);
// Enrich modified sections with firstChangedLine (load only modified slugs)
if (diff.modified.length > 0) {
const modifiedSlugs = diff.modified.map((m) => m.slug);
const baselineContents = loadSections('baseline', modifiedSlugs);
const currentContents = loadSections('current', modifiedSlugs);
for (const entry of diff.modified) {
const baseContent = baselineContents[entry.slug];
const currContent = currentContents[entry.slug];
if (baseContent && currContent) {
const line = findFirstChangedLine(baseContent, currContent);
if (line) entry.firstChangedLine = line;
}
}
}
const lines: string[] = [
'# Official Docs Diff',
'',
`Baseline: ${formatDate(baseline.fetchedAt)} | Current: ${formatDate(current.fetchedAt)}`,
'',
];
const ageDays = snapshotAgeDays(current);
if (ageDays > STALENESS_WARN_DAYS) {
lines.push(`⚠️ Current snapshot is ${ageDays} days old — consider running refresh_official_docs()`);
lines.push('');
}
if (diff.added.length > 0) {
lines.push(`## Added (${diff.added.length})`);
for (const s of diff.added) {
lines.push(`+ ${s.slug} — "${s.title}"`);
lines.push(` ${s.sourceUrl}`);
}
lines.push('');
}
if (diff.removed.length > 0) {
lines.push(`## Removed (${diff.removed.length})`);
for (const s of diff.removed) {
lines.push(`- ${s.slug} — "${s.title}"`);
lines.push(` ${s.sourceUrl}`);
}
lines.push('');
}
if (diff.modified.length > 0) {
lines.push(`## Modified (${diff.modified.length})`);
for (const s of diff.modified) {
const delta = s.lineDelta !== undefined
? (s.lineDelta >= 0 ? `+${s.lineDelta}` : `${s.lineDelta}`)
: '';
const charDelta = s.charsAfter !== undefined && s.charsBefore !== undefined
? (s.charsAfter - s.charsBefore >= 0
? `+${s.charsAfter - s.charsBefore}`
: `${s.charsAfter - s.charsBefore}`)
: '';
lines.push(`~ ${s.slug} (L${s.linesBefore} → L${s.linesAfter}, ${delta} lines, ${charDelta} chars)`);
if (s.firstChangedLine) {
lines.push(` First change: "${s.firstChangedLine}"`);
}
lines.push(` ${s.sourceUrl}`);
}
lines.push('');
}
const total = diff.added.length + diff.removed.length + diff.modified.length;
lines.push('---');
lines.push(`${diff.added.length} added, ${diff.removed.length} removed, ${diff.modified.length} modified, ${diff.unchanged} unchanged (${total} total changes)`);
lines.push('Run refresh_official_docs() to update current to latest.');
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
// ── search_official_docs ────────────────────────────────────────────────
server.tool(
'search_official_docs',
'Search the official Anthropic Claude Code documentation by keyword or topic. Uses the local current snapshot — no network call. Run init_official_docs() first.',
{
query: z.string().describe('Search term or topic (e.g. "hooks", "MCP authentication", "cost limits")'),
limit: z.number().min(1).max(10).optional().default(5).describe('Max sections to return (default 5)'),
},
{ readOnlyHint: true, destructiveHint: false, openWorldHint: false },
async ({ query, limit }) => {
const current = loadIndex('current');
if (!current) {
return {
content: [{
type: 'text',
text: 'No snapshot found. Run init_official_docs() first to create a local cache of the official docs.',
}],
isError: true,
};
}
// Score all sections against the query (index only — no content loaded yet)
const scored = Object.values(current.sections)
.map((sec) => ({ meta: sec, titleScore: sec.title.toLowerCase().includes(query.toLowerCase()) ? 3 : 0 }))
.filter((item) => item.titleScore > 0); // title-only pass first for speed
// If fewer than limit title matches, also check content (load lazily)
let finalSlugs: string[];
if (scored.length >= (limit ?? 5)) {
finalSlugs = scored
.sort((a, b) => b.titleScore - a.titleScore)
.slice(0, limit ?? 5)
.map((item) => item.meta.slug);
} else {
// Load all content and score body matches too
const allSlugs = Object.keys(current.sections);
const allContents = loadSections('current', allSlugs);
const fullScored = Object.values(current.sections)
.map((sec) => ({
meta: sec,
score: scoreSection(sec.title, allContents[sec.slug] ?? '', query),
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit ?? 5);
finalSlugs = fullScored.map((item) => item.meta.slug);
}
if (finalSlugs.length === 0) {
return {
content: [{
type: 'text',
text: `No results for "${query}" in ${current.sectionCount} sections (snapshot: ${formatDate(current.fetchedAt)}).\n\nTry broader terms or run refresh_official_docs() to update the cache.`,
}],
};
}
// Load only the matching sections
const matchingContents = loadSections('current', finalSlugs);
const lines: string[] = [
`# Search: "${query}" — ${finalSlugs.length} result${finalSlugs.length !== 1 ? 's' : ''} (snapshot: ${formatDate(current.fetchedAt)})`,
'',
];
for (const slug of finalSlugs) {
const meta = current.sections[slug];
const content = matchingContents[slug] ?? '';
const excerpt = extractExcerpt(content, query);
lines.push(`## ${meta.title}`);
lines.push(meta.sourceUrl);
lines.push('');
lines.push(excerpt);
lines.push('');
}
lines.push('---');
lines.push(`Showing ${finalSlugs.length} of ${current.sectionCount} sections. Run refresh_official_docs() to update the cache.`);
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
}