claude-code-ultimate-guide-mcp 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13986,14 +13986,37 @@ function searchGuide(query, limit = 10) {
13986
13986
 
13987
13987
  // src/lib/urls.ts
13988
13988
  var GITHUB_BASE = "https://github.com/FlorianBruniaux/claude-code-ultimate-guide/blob/main";
13989
- var GUIDE_SITE_BASE = "https://cc.bruniaux.com/guide";
13989
+ var GUIDE_SITE_BASE = "https://cc.bruniaux.com/guide/ultimate-guide";
13990
+ var CHAPTER_RANGES = [
13991
+ { from: 1, to: 220, slug: "00-introduction" },
13992
+ { from: 221, to: 1378, slug: "01-quick-start" },
13993
+ { from: 1379, to: 4231, slug: "02-core-workflow" },
13994
+ { from: 4232, to: 5648, slug: "03-memory-files" },
13995
+ { from: 5649, to: 6361, slug: "04-agents" },
13996
+ { from: 6362, to: 7470, slug: "05-skills" },
13997
+ { from: 7471, to: 8106, slug: "06-commands" },
13998
+ { from: 8107, to: 9482, slug: "07-hooks" },
13999
+ { from: 9483, to: 12118, slug: "08-mcp" },
14000
+ { from: 12119, to: 19831, slug: "09-advanced-patterns" },
14001
+ { from: 19832, to: 20760, slug: "10-reference" },
14002
+ { from: 20761, to: 21384, slug: "11-ai-ecosystem" },
14003
+ { from: 21385, to: Infinity, slug: "12-appendices" }
14004
+ ];
14005
+ function lineToChapterSlug(line) {
14006
+ for (const range of CHAPTER_RANGES) {
14007
+ if (line >= range.from && line <= range.to) return range.slug;
14008
+ }
14009
+ return "12-appendices";
14010
+ }
13990
14011
  function githubUrl(filePath, line) {
13991
14012
  const base = `${GITHUB_BASE}/${filePath}`;
13992
14013
  return line ? `${base}#L${line}` : base;
13993
14014
  }
13994
14015
  function guideSiteUrl(filePath, line) {
13995
14016
  if (filePath !== "guide/ultimate-guide.md") return null;
13996
- return line ? `${GUIDE_SITE_BASE}/#L${line}` : GUIDE_SITE_BASE;
14017
+ if (!line) return `${GUIDE_SITE_BASE}/`;
14018
+ const chapterSlug = lineToChapterSlug(line);
14019
+ return `${GUIDE_SITE_BASE}/${chapterSlug}/`;
13997
14020
  }
13998
14021
  function formatLinks(filePath, line) {
13999
14022
  const gh = githubUrl(filePath, line);
@@ -14512,7 +14535,7 @@ function registerChangelog(server) {
14512
14535
  for (const r of ccReleases) {
14513
14536
  lines.push(`### v${r.version} (${r.date})`);
14514
14537
  for (const h of r.highlights ?? []) lines.push(`- ${h}`);
14515
- lines.push(`GitHub: ${githubUrl("guide/claude-code-releases.md")}`);
14538
+ lines.push(`GitHub: ${githubUrl("guide/core/claude-code-releases.md")}`);
14516
14539
  lines.push("");
14517
14540
  }
14518
14541
  }
@@ -14630,7 +14653,7 @@ function registerListExamples(server) {
14630
14653
  }
14631
14654
 
14632
14655
  // src/tools/releases.ts
14633
- var RELEASES_GITHUB = githubUrl("guide/claude-code-releases.md");
14656
+ var RELEASES_GITHUB = githubUrl("guide/core/claude-code-releases.md");
14634
14657
  function registerReleases(server) {
14635
14658
  server.tool(
14636
14659
  "get_release",
@@ -14696,7 +14719,7 @@ Full history: ${RELEASES_GITHUB}`
14696
14719
  }
14697
14720
 
14698
14721
  // src/tools/compare-versions.ts
14699
- var RELEASES_GITHUB2 = githubUrl("guide/claude-code-releases.md");
14722
+ var RELEASES_GITHUB2 = githubUrl("guide/core/claude-code-releases.md");
14700
14723
  function parseSemver(v) {
14701
14724
  const parts = v.replace(/^v/, "").split(".").map(Number);
14702
14725
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
@@ -15102,6 +15125,555 @@ function registerSearchExamples(server) {
15102
15125
  );
15103
15126
  }
15104
15127
 
15128
+ // src/lib/docs-cache.ts
15129
+ import { createHash } from "crypto";
15130
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
15131
+ import { homedir } from "os";
15132
+ import { resolve } from "path";
15133
+ var ANTHROPIC_DOCS_URL = "https://code.claude.com/docs/llms-full.txt";
15134
+ var SNAPSHOT_DIR = resolve(homedir(), ".cache", "claude-code-guide");
15135
+ function getSnapshotDir() {
15136
+ return SNAPSHOT_DIR;
15137
+ }
15138
+ function indexPath(type) {
15139
+ return resolve(SNAPSHOT_DIR, `anthropic-docs-${type}-index.json`);
15140
+ }
15141
+ function contentPath(type) {
15142
+ return resolve(SNAPSHOT_DIR, `anthropic-docs-${type}-content.json`);
15143
+ }
15144
+ async function fetchOfficialDocs() {
15145
+ const response = await fetch(ANTHROPIC_DOCS_URL, {
15146
+ headers: { "User-Agent": "claude-code-ultimate-guide-mcp/1.1.0" },
15147
+ signal: AbortSignal.timeout(15e3)
15148
+ });
15149
+ if (!response.ok) {
15150
+ throw new Error(`HTTP ${response.status} fetching ${ANTHROPIC_DOCS_URL}`);
15151
+ }
15152
+ const text = await response.text();
15153
+ if (text.length < 5e5) {
15154
+ throw new Error(
15155
+ `Response looks malformed (got ${(text.length / 1024).toFixed(0)}KB, expected >500KB). The URL format may have changed.`
15156
+ );
15157
+ }
15158
+ return text;
15159
+ }
15160
+ function parseIntoSections(raw) {
15161
+ const lines = raw.split("\n");
15162
+ const sections = {};
15163
+ const contents = {};
15164
+ let currentSlug = null;
15165
+ let currentTitle = null;
15166
+ let currentSourceUrl = null;
15167
+ let currentStartLine = 0;
15168
+ let titleCandidate = null;
15169
+ let titleCandidateLine = -1;
15170
+ const flushSection = (endLine) => {
15171
+ if (!currentSlug || !currentTitle || !currentSourceUrl) return;
15172
+ const sectionLines = lines.slice(currentStartLine, endLine);
15173
+ const content2 = sectionLines.join("\n").trimEnd();
15174
+ if (content2.length === 0) return;
15175
+ const hash2 = createHash("sha256").update(content2).digest("hex").slice(0, 16);
15176
+ sections[currentSlug] = {
15177
+ slug: currentSlug,
15178
+ title: currentTitle,
15179
+ sourceUrl: currentSourceUrl,
15180
+ hash: hash2,
15181
+ lineCount: sectionLines.length,
15182
+ charCount: content2.length
15183
+ };
15184
+ contents[currentSlug] = { content: content2 };
15185
+ };
15186
+ for (let i = 0; i < lines.length; i++) {
15187
+ const line = lines[i];
15188
+ if (line.startsWith("# ") && !line.startsWith("## ")) {
15189
+ titleCandidate = line;
15190
+ titleCandidateLine = i;
15191
+ continue;
15192
+ }
15193
+ const sourceMatch = line.match(
15194
+ /^Source:\s+(https:\/\/code\.claude\.com\/docs\/(?:en\/)?([a-zA-Z0-9-]+))\s*$/
15195
+ );
15196
+ if (sourceMatch && titleCandidate !== null && i === titleCandidateLine + 1) {
15197
+ const sourceUrl = sourceMatch[1];
15198
+ const slug = sourceMatch[2];
15199
+ flushSection(titleCandidateLine);
15200
+ currentSlug = slug;
15201
+ currentTitle = titleCandidate;
15202
+ currentSourceUrl = sourceUrl;
15203
+ currentStartLine = titleCandidateLine;
15204
+ titleCandidate = null;
15205
+ titleCandidateLine = -1;
15206
+ continue;
15207
+ }
15208
+ if (titleCandidate !== null && i > titleCandidateLine + 1) {
15209
+ titleCandidate = null;
15210
+ titleCandidateLine = -1;
15211
+ }
15212
+ }
15213
+ flushSection(lines.length);
15214
+ if (Object.keys(sections).length === 0) {
15215
+ throw new Error(
15216
+ "Section parser found 0 sections \u2014 llms-full.txt format may have changed. Please open an issue at https://github.com/FlorianBruniaux/claude-code-ultimate-guide"
15217
+ );
15218
+ }
15219
+ const contentHash = createHash("sha256").update(raw).digest("hex").slice(0, 16);
15220
+ const index = {
15221
+ schemaVersion: 1,
15222
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
15223
+ contentHash,
15224
+ sectionCount: Object.keys(sections).length,
15225
+ sections
15226
+ };
15227
+ const content = {
15228
+ schemaVersion: 1,
15229
+ sections: contents
15230
+ };
15231
+ return { index, content };
15232
+ }
15233
+ function saveSnapshot(type, index, content) {
15234
+ mkdirSync(SNAPSHOT_DIR, { recursive: true });
15235
+ const iPath = indexPath(type);
15236
+ writeFileSync(`${iPath}.tmp`, JSON.stringify(index, null, 2), "utf8");
15237
+ renameSync(`${iPath}.tmp`, iPath);
15238
+ const cPath = contentPath(type);
15239
+ writeFileSync(`${cPath}.tmp`, JSON.stringify(content, null, 2), "utf8");
15240
+ renameSync(`${cPath}.tmp`, cPath);
15241
+ }
15242
+ function loadIndex(type) {
15243
+ const path = indexPath(type);
15244
+ if (!existsSync2(path)) return null;
15245
+ try {
15246
+ const data = JSON.parse(readFileSync2(path, "utf8"));
15247
+ if (data.schemaVersion !== 1) return null;
15248
+ return data;
15249
+ } catch {
15250
+ return null;
15251
+ }
15252
+ }
15253
+ function loadSections(type, slugs) {
15254
+ const path = contentPath(type);
15255
+ if (!existsSync2(path)) return {};
15256
+ try {
15257
+ const data = JSON.parse(readFileSync2(path, "utf8"));
15258
+ const result = {};
15259
+ for (const slug of slugs) {
15260
+ const section = data.sections[slug];
15261
+ if (section) result[slug] = section.content;
15262
+ }
15263
+ return result;
15264
+ } catch {
15265
+ return {};
15266
+ }
15267
+ }
15268
+ function snapshotAgeDays(index) {
15269
+ return Math.floor((Date.now() - new Date(index.fetchedAt).getTime()) / (1e3 * 60 * 60 * 24));
15270
+ }
15271
+
15272
+ // src/lib/docs-diff.ts
15273
+ function diffSnapshots(baseline, current) {
15274
+ const baselineSlugs = new Set(Object.keys(baseline.sections));
15275
+ const currentSlugs = new Set(Object.keys(current.sections));
15276
+ const added = [];
15277
+ const removed = [];
15278
+ const modified = [];
15279
+ let unchanged = 0;
15280
+ for (const slug of currentSlugs) {
15281
+ if (!baselineSlugs.has(slug)) {
15282
+ const s = current.sections[slug];
15283
+ added.push({ slug, title: s.title, sourceUrl: s.sourceUrl, kind: "added" });
15284
+ }
15285
+ }
15286
+ for (const slug of baselineSlugs) {
15287
+ if (!currentSlugs.has(slug)) {
15288
+ const s = baseline.sections[slug];
15289
+ removed.push({ slug, title: s.title, sourceUrl: s.sourceUrl, kind: "removed" });
15290
+ }
15291
+ }
15292
+ for (const slug of baselineSlugs) {
15293
+ if (!currentSlugs.has(slug)) continue;
15294
+ const base = baseline.sections[slug];
15295
+ const curr = current.sections[slug];
15296
+ if (base.hash === curr.hash) {
15297
+ unchanged++;
15298
+ continue;
15299
+ }
15300
+ modified.push({
15301
+ slug,
15302
+ title: curr.title,
15303
+ sourceUrl: curr.sourceUrl,
15304
+ kind: "modified",
15305
+ linesBefore: base.lineCount,
15306
+ linesAfter: curr.lineCount,
15307
+ charsBefore: base.charCount,
15308
+ charsAfter: curr.charCount,
15309
+ lineDelta: curr.lineCount - base.lineCount
15310
+ // firstChangedLine is enriched by the tool layer after loading content
15311
+ });
15312
+ }
15313
+ return {
15314
+ unchanged,
15315
+ added,
15316
+ removed,
15317
+ modified,
15318
+ baselineDate: baseline.fetchedAt,
15319
+ currentDate: current.fetchedAt
15320
+ };
15321
+ }
15322
+ function findFirstChangedLine(baseContent, currentContent) {
15323
+ const baseLines = baseContent.split("\n");
15324
+ const currLines = currentContent.split("\n");
15325
+ const len = Math.min(baseLines.length, currLines.length);
15326
+ for (let i = 0; i < len; i++) {
15327
+ if (baseLines[i] !== currLines[i]) {
15328
+ return currLines[i].slice(0, 120);
15329
+ }
15330
+ }
15331
+ if (currLines.length > baseLines.length) {
15332
+ return (currLines[baseLines.length] ?? "").slice(0, 120);
15333
+ }
15334
+ return "";
15335
+ }
15336
+
15337
+ // src/tools/official-docs.ts
15338
+ var STALENESS_WARN_DAYS = 30;
15339
+ function formatDate(iso) {
15340
+ return iso.slice(0, 10);
15341
+ }
15342
+ function formatSize(bytes) {
15343
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
15344
+ return `${(bytes / 1024).toFixed(0)} KB`;
15345
+ }
15346
+ function extractExcerpt(content, query, maxLen = 300) {
15347
+ const lowerContent = content.toLowerCase();
15348
+ const lowerQuery = query.toLowerCase();
15349
+ const idx = lowerContent.indexOf(lowerQuery);
15350
+ if (idx === -1) {
15351
+ const excerpt = content.slice(0, maxLen);
15352
+ return excerpt + (content.length > maxLen ? "\u2026" : "");
15353
+ }
15354
+ const start = Math.max(0, idx - 100);
15355
+ const end = Math.min(content.length, idx + query.length + 200);
15356
+ const prefix = start > 0 ? "\u2026" : "";
15357
+ const suffix = end < content.length ? "\u2026" : "";
15358
+ return prefix + content.slice(start, end) + suffix;
15359
+ }
15360
+ function scoreSection(title, content, query) {
15361
+ const q = query.toLowerCase();
15362
+ const titleScore = title.toLowerCase().includes(q) ? 3 : 0;
15363
+ const bodyScore = content.toLowerCase().includes(q) ? 1 : 0;
15364
+ return titleScore + bodyScore;
15365
+ }
15366
+ function registerOfficialDocs(server) {
15367
+ server.tool(
15368
+ "init_official_docs",
15369
+ "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 \u2014 overwrites previous baseline AND current. Takes ~5s (fetches ~1.2MB from Anthropic).",
15370
+ {},
15371
+ { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
15372
+ async () => {
15373
+ let raw;
15374
+ try {
15375
+ raw = await fetchOfficialDocs();
15376
+ } catch (err) {
15377
+ return {
15378
+ content: [{
15379
+ type: "text",
15380
+ text: `Failed to fetch official docs: ${err instanceof Error ? err.message : String(err)}
15381
+
15382
+ Check network connectivity and retry.`
15383
+ }],
15384
+ isError: true
15385
+ };
15386
+ }
15387
+ let parsed;
15388
+ try {
15389
+ parsed = parseIntoSections(raw);
15390
+ } catch (err) {
15391
+ return {
15392
+ content: [{
15393
+ type: "text",
15394
+ text: `Parsing failed: ${err instanceof Error ? err.message : String(err)}`
15395
+ }],
15396
+ isError: true
15397
+ };
15398
+ }
15399
+ try {
15400
+ saveSnapshot("baseline", parsed.index, parsed.content);
15401
+ saveSnapshot("current", parsed.index, parsed.content);
15402
+ } catch (err) {
15403
+ return {
15404
+ content: [{
15405
+ type: "text",
15406
+ text: `Failed to save snapshot to disk: ${err instanceof Error ? err.message : String(err)}
15407
+
15408
+ Check that ${getSnapshotDir()} is writable.`
15409
+ }],
15410
+ isError: true
15411
+ };
15412
+ }
15413
+ const { index } = parsed;
15414
+ const totalChars = Object.values(index.sections).reduce((s, sec) => s + sec.charCount, 0);
15415
+ const lines = [
15416
+ "# Official Docs Snapshot Created",
15417
+ "",
15418
+ `Fetched: ${index.fetchedAt}`,
15419
+ `Sections: ${index.sectionCount}`,
15420
+ `Total size: ${formatSize(totalChars)}`,
15421
+ `Snapshot dir: ${getSnapshotDir()}`,
15422
+ "",
15423
+ "Both baseline and current snapshots have been set.",
15424
+ "",
15425
+ "Next steps:",
15426
+ " - Run refresh_official_docs() to update current without touching the baseline",
15427
+ " - Run diff_official_docs() to see changes between baseline and current",
15428
+ " - Run search_official_docs(query) to search the official docs"
15429
+ ];
15430
+ return { content: [{ type: "text", text: lines.join("\n") }] };
15431
+ }
15432
+ );
15433
+ server.tool(
15434
+ "refresh_official_docs",
15435
+ '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).',
15436
+ {},
15437
+ { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
15438
+ async () => {
15439
+ const baseline = loadIndex("baseline");
15440
+ if (!baseline) {
15441
+ return {
15442
+ content: [{
15443
+ type: "text",
15444
+ text: "No baseline snapshot found. Run init_official_docs() first to create both the baseline and the initial current snapshot."
15445
+ }],
15446
+ isError: true
15447
+ };
15448
+ }
15449
+ let raw;
15450
+ try {
15451
+ raw = await fetchOfficialDocs();
15452
+ } catch (err) {
15453
+ return {
15454
+ content: [{
15455
+ type: "text",
15456
+ text: `Failed to fetch official docs: ${err instanceof Error ? err.message : String(err)}
15457
+
15458
+ Your existing snapshots are intact.`
15459
+ }],
15460
+ isError: true
15461
+ };
15462
+ }
15463
+ let parsed;
15464
+ try {
15465
+ parsed = parseIntoSections(raw);
15466
+ } catch (err) {
15467
+ return {
15468
+ content: [{
15469
+ type: "text",
15470
+ text: `Parsing failed: ${err instanceof Error ? err.message : String(err)}
15471
+
15472
+ Your existing snapshots are intact.`
15473
+ }],
15474
+ isError: true
15475
+ };
15476
+ }
15477
+ try {
15478
+ saveSnapshot("current", parsed.index, parsed.content);
15479
+ } catch (err) {
15480
+ return {
15481
+ content: [{
15482
+ type: "text",
15483
+ text: `Failed to save current snapshot: ${err instanceof Error ? err.message : String(err)}`
15484
+ }],
15485
+ isError: true
15486
+ };
15487
+ }
15488
+ const diff = diffSnapshots(baseline, parsed.index);
15489
+ const total = diff.added.length + diff.removed.length + diff.modified.length;
15490
+ const lines = [
15491
+ "# Official Docs \u2014 Current Snapshot Updated",
15492
+ "",
15493
+ `Fetched: ${parsed.index.fetchedAt}`,
15494
+ `Sections: ${parsed.index.sectionCount}`,
15495
+ ""
15496
+ ];
15497
+ if (total === 0) {
15498
+ lines.push(`No changes vs baseline (${formatDate(baseline.fetchedAt)}). All ${diff.unchanged} sections unchanged.`);
15499
+ } else {
15500
+ lines.push(`vs baseline (${formatDate(baseline.fetchedAt)}): ${diff.added.length} added, ${diff.removed.length} removed, ${diff.modified.length} modified, ${diff.unchanged} unchanged`);
15501
+ lines.push("");
15502
+ lines.push("Run diff_official_docs() for the full diff.");
15503
+ }
15504
+ return { content: [{ type: "text", text: lines.join("\n") }] };
15505
+ }
15506
+ );
15507
+ server.tool(
15508
+ "diff_official_docs",
15509
+ "Compare the baseline and current official Anthropic Claude Code docs snapshots. Shows added, removed, and modified pages. No network call \u2014 reads local files only. Run init_official_docs() first, then refresh_official_docs() when you want to update the current snapshot.",
15510
+ {},
15511
+ { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
15512
+ async () => {
15513
+ const baseline = loadIndex("baseline");
15514
+ if (!baseline) {
15515
+ return {
15516
+ content: [{
15517
+ type: "text",
15518
+ text: "No baseline found. Run init_official_docs() first to create a baseline snapshot."
15519
+ }],
15520
+ isError: true
15521
+ };
15522
+ }
15523
+ const current = loadIndex("current");
15524
+ if (!current) {
15525
+ return {
15526
+ content: [{
15527
+ type: "text",
15528
+ text: "No current snapshot found. Run refresh_official_docs() to fetch the latest docs."
15529
+ }],
15530
+ isError: true
15531
+ };
15532
+ }
15533
+ if (baseline.contentHash === current.contentHash) {
15534
+ const ageDays2 = snapshotAgeDays(current);
15535
+ const staleWarn = ageDays2 > STALENESS_WARN_DAYS ? `
15536
+ \u26A0\uFE0F Current snapshot is ${ageDays2} days old \u2014 consider running refresh_official_docs()` : "";
15537
+ return {
15538
+ content: [{
15539
+ type: "text",
15540
+ text: [
15541
+ "# Official Docs Diff",
15542
+ "",
15543
+ `Baseline: ${formatDate(baseline.fetchedAt)} | Current: ${formatDate(current.fetchedAt)}`,
15544
+ "",
15545
+ `No changes detected. All ${baseline.sectionCount} sections unchanged.${staleWarn}`,
15546
+ "",
15547
+ "Run refresh_official_docs() to fetch latest docs and update current."
15548
+ ].join("\n")
15549
+ }]
15550
+ };
15551
+ }
15552
+ const diff = diffSnapshots(baseline, current);
15553
+ if (diff.modified.length > 0) {
15554
+ const modifiedSlugs = diff.modified.map((m) => m.slug);
15555
+ const baselineContents = loadSections("baseline", modifiedSlugs);
15556
+ const currentContents = loadSections("current", modifiedSlugs);
15557
+ for (const entry of diff.modified) {
15558
+ const baseContent = baselineContents[entry.slug];
15559
+ const currContent = currentContents[entry.slug];
15560
+ if (baseContent && currContent) {
15561
+ const line = findFirstChangedLine(baseContent, currContent);
15562
+ if (line) entry.firstChangedLine = line;
15563
+ }
15564
+ }
15565
+ }
15566
+ const lines = [
15567
+ "# Official Docs Diff",
15568
+ "",
15569
+ `Baseline: ${formatDate(baseline.fetchedAt)} | Current: ${formatDate(current.fetchedAt)}`,
15570
+ ""
15571
+ ];
15572
+ const ageDays = snapshotAgeDays(current);
15573
+ if (ageDays > STALENESS_WARN_DAYS) {
15574
+ lines.push(`\u26A0\uFE0F Current snapshot is ${ageDays} days old \u2014 consider running refresh_official_docs()`);
15575
+ lines.push("");
15576
+ }
15577
+ if (diff.added.length > 0) {
15578
+ lines.push(`## Added (${diff.added.length})`);
15579
+ for (const s of diff.added) {
15580
+ lines.push(`+ ${s.slug} \u2014 "${s.title}"`);
15581
+ lines.push(` ${s.sourceUrl}`);
15582
+ }
15583
+ lines.push("");
15584
+ }
15585
+ if (diff.removed.length > 0) {
15586
+ lines.push(`## Removed (${diff.removed.length})`);
15587
+ for (const s of diff.removed) {
15588
+ lines.push(`- ${s.slug} \u2014 "${s.title}"`);
15589
+ lines.push(` ${s.sourceUrl}`);
15590
+ }
15591
+ lines.push("");
15592
+ }
15593
+ if (diff.modified.length > 0) {
15594
+ lines.push(`## Modified (${diff.modified.length})`);
15595
+ for (const s of diff.modified) {
15596
+ const delta = s.lineDelta !== void 0 ? s.lineDelta >= 0 ? `+${s.lineDelta}` : `${s.lineDelta}` : "";
15597
+ const charDelta = s.charsAfter !== void 0 && s.charsBefore !== void 0 ? s.charsAfter - s.charsBefore >= 0 ? `+${s.charsAfter - s.charsBefore}` : `${s.charsAfter - s.charsBefore}` : "";
15598
+ lines.push(`~ ${s.slug} (L${s.linesBefore} \u2192 L${s.linesAfter}, ${delta} lines, ${charDelta} chars)`);
15599
+ if (s.firstChangedLine) {
15600
+ lines.push(` First change: "${s.firstChangedLine}"`);
15601
+ }
15602
+ lines.push(` ${s.sourceUrl}`);
15603
+ }
15604
+ lines.push("");
15605
+ }
15606
+ const total = diff.added.length + diff.removed.length + diff.modified.length;
15607
+ lines.push("---");
15608
+ lines.push(`${diff.added.length} added, ${diff.removed.length} removed, ${diff.modified.length} modified, ${diff.unchanged} unchanged (${total} total changes)`);
15609
+ lines.push("Run refresh_official_docs() to update current to latest.");
15610
+ return { content: [{ type: "text", text: lines.join("\n") }] };
15611
+ }
15612
+ );
15613
+ server.tool(
15614
+ "search_official_docs",
15615
+ "Search the official Anthropic Claude Code documentation by keyword or topic. Uses the local current snapshot \u2014 no network call. Run init_official_docs() first.",
15616
+ {
15617
+ query: external_exports.string().describe('Search term or topic (e.g. "hooks", "MCP authentication", "cost limits")'),
15618
+ limit: external_exports.number().min(1).max(10).optional().default(5).describe("Max sections to return (default 5)")
15619
+ },
15620
+ { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
15621
+ async ({ query, limit }) => {
15622
+ const current = loadIndex("current");
15623
+ if (!current) {
15624
+ return {
15625
+ content: [{
15626
+ type: "text",
15627
+ text: "No snapshot found. Run init_official_docs() first to create a local cache of the official docs."
15628
+ }],
15629
+ isError: true
15630
+ };
15631
+ }
15632
+ const scored = Object.values(current.sections).map((sec) => ({ meta: sec, titleScore: sec.title.toLowerCase().includes(query.toLowerCase()) ? 3 : 0 })).filter((item) => item.titleScore > 0);
15633
+ let finalSlugs;
15634
+ if (scored.length >= (limit ?? 5)) {
15635
+ finalSlugs = scored.sort((a, b) => b.titleScore - a.titleScore).slice(0, limit ?? 5).map((item) => item.meta.slug);
15636
+ } else {
15637
+ const allSlugs = Object.keys(current.sections);
15638
+ const allContents = loadSections("current", allSlugs);
15639
+ const fullScored = Object.values(current.sections).map((sec) => ({
15640
+ meta: sec,
15641
+ score: scoreSection(sec.title, allContents[sec.slug] ?? "", query)
15642
+ })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit ?? 5);
15643
+ finalSlugs = fullScored.map((item) => item.meta.slug);
15644
+ }
15645
+ if (finalSlugs.length === 0) {
15646
+ return {
15647
+ content: [{
15648
+ type: "text",
15649
+ text: `No results for "${query}" in ${current.sectionCount} sections (snapshot: ${formatDate(current.fetchedAt)}).
15650
+
15651
+ Try broader terms or run refresh_official_docs() to update the cache.`
15652
+ }]
15653
+ };
15654
+ }
15655
+ const matchingContents = loadSections("current", finalSlugs);
15656
+ const lines = [
15657
+ `# Search: "${query}" \u2014 ${finalSlugs.length} result${finalSlugs.length !== 1 ? "s" : ""} (snapshot: ${formatDate(current.fetchedAt)})`,
15658
+ ""
15659
+ ];
15660
+ for (const slug of finalSlugs) {
15661
+ const meta3 = current.sections[slug];
15662
+ const content = matchingContents[slug] ?? "";
15663
+ const excerpt = extractExcerpt(content, query);
15664
+ lines.push(`## ${meta3.title}`);
15665
+ lines.push(meta3.sourceUrl);
15666
+ lines.push("");
15667
+ lines.push(excerpt);
15668
+ lines.push("");
15669
+ }
15670
+ lines.push("---");
15671
+ lines.push(`Showing ${finalSlugs.length} of ${current.sectionCount} sections. Run refresh_official_docs() to update the cache.`);
15672
+ return { content: [{ type: "text", text: lines.join("\n") }] };
15673
+ }
15674
+ );
15675
+ }
15676
+
15105
15677
  // src/resources/index.ts
15106
15678
  function registerResources(server) {
15107
15679
  server.resource(
@@ -15265,6 +15837,7 @@ function createServer() {
15265
15837
  registerGetThreat(server);
15266
15838
  registerListThreats(server);
15267
15839
  registerSearchExamples(server);
15840
+ registerOfficialDocs(server);
15268
15841
  registerResources(server);
15269
15842
  registerPrompts(server);
15270
15843
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-ultimate-guide-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for the Claude Code Ultimate Guide — search, read, and explore 20K+ lines of documentation directly from Claude Code",
5
5
  "keywords": [
6
6
  "mcp",