@thallylabs/mcp 0.7.5 → 0.8.1

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/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # @thallylabs/mcp
2
2
 
3
3
  A [Model Context Protocol](https://modelcontextprotocol.io) server that lets AI
4
- tools — Claude Code, Claude Desktop, Cursor, Windsurf — create, read, update,
5
- search, and migrate [Thally](https://github.com/thallylabs/thally) documentation projects
6
- through natural language.
4
+ tools — Claude Code, Claude Desktop, Cursor, Windsurf — manage
5
+ [Thally](https://github.com/thallylabs/thally) knowledge surfaces through
6
+ natural language. Tools can create, read, update, search, and migrate
7
+ documentation, then trace a product-repository change into reviewable docs
8
+ work.
7
9
 
8
10
  ## Setup
9
11
 
@@ -28,17 +30,22 @@ Or in a `mcp.json` / client config:
28
30
 
29
31
  ## Tools
30
32
 
31
- 13 tools, including:
33
+ 15 tools, including:
32
34
 
33
35
  - **Authoring** — `create_project`, `add_page`, `update_page`, `read_page`, `list_pages`, `add_tab`
34
36
  - **Context & search** — `get_context`, `search_docs`, `semantic_search` (against a deployed site)
35
37
  - **Quality** — `lint_project`, `agent_readiness` (the Agent Readiness Score of a deployed site)
36
- - **Migration** — `migrate_docs`, `translate_docs`
38
+ - **Migration** — `migrate_docs`, `import_docs`, `translate_docs`
39
+ - **Product changes** — `sync_from_repo`
37
40
 
38
41
  `search_docs`, `read_page`, and `get_context` work against a local project on
39
42
  disk; `semantic_search` and `agent_readiness` run against any **deployed** Thally
40
43
  site over HTTP.
41
44
 
45
+ `migrate_docs` always scaffolds a fresh canonical Thally template before it
46
+ imports content. To preserve an existing Thally runtime and import content in
47
+ place, the caller must explicitly choose `import_docs`.
48
+
42
49
  ## License
43
50
 
44
51
  MIT
package/dist/index.js CHANGED
@@ -329,6 +329,34 @@ function writeDocsJson(projectDir, config) {
329
329
  writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
330
330
  }
331
331
 
332
+ // src/lib/page-echo.ts
333
+ var BODY_DELIMITER = "--- MDX body ---";
334
+ function readPageBodyDelimiter() {
335
+ return BODY_DELIMITER;
336
+ }
337
+ function stripEchoedPageHeader(content, pageId) {
338
+ const lines = content.split("\n");
339
+ const delimiterIndex = lines.findIndex((line) => line.trim() === BODY_DELIMITER);
340
+ if (delimiterIndex !== -1 && delimiterIndex <= 6) {
341
+ return lines.slice(delimiterIndex + 1).join("\n").replace(/^\n+/, "");
342
+ }
343
+ const first = lines[0]?.trim() ?? "";
344
+ const second = lines[1]?.trim() ?? "";
345
+ if (first.startsWith("# ") && second === `*${pageId}*`) {
346
+ let index = 2;
347
+ while (index < lines.length && lines[index].trim() === "") index += 1;
348
+ if (lines[index]?.trim().startsWith("> ")) {
349
+ index += 1;
350
+ while (index < lines.length && lines[index].trim() === "") index += 1;
351
+ }
352
+ if (lines[index]?.trim() === "---") {
353
+ index += 1;
354
+ }
355
+ return lines.slice(index).join("\n").replace(/^\n+/, "");
356
+ }
357
+ return content;
358
+ }
359
+
332
360
  // src/tools/add-page.ts
333
361
  var addPageSchema = z2.object({
334
362
  projectDir: z2.string().describe("Path to the Thally project root"),
@@ -359,7 +387,7 @@ async function handleAddPage(input) {
359
387
  if (description) {
360
388
  frontmatterLines.push(`description: ${description}`);
361
389
  }
362
- const bodyContent = content ?? `## ${title}
390
+ const bodyContent = content ? stripEchoedPageHeader(content, pageId) : `## ${title}
363
391
 
364
392
  Add your content here.`;
365
393
  const mdxContent = `---
@@ -483,7 +511,23 @@ async function handleListPages(input) {
483
511
  import { z as z5 } from "zod";
484
512
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
485
513
  import { join as join4 } from "path";
514
+
515
+ // src/lib/frontmatter.ts
486
516
  import matter from "gray-matter";
517
+ var FRONTMATTER_OPTIONS = {
518
+ engines: {
519
+ javascript: () => ({}),
520
+ js: () => ({})
521
+ }
522
+ };
523
+ function parseFrontmatter(raw) {
524
+ return matter(raw, FRONTMATTER_OPTIONS);
525
+ }
526
+ function stringifyFrontmatter(body, data) {
527
+ return matter.stringify(body, data);
528
+ }
529
+
530
+ // src/tools/update-page.ts
487
531
  var updatePageSchema = z5.object({
488
532
  projectDir: z5.string().describe("Path to the Thally project root"),
489
533
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -513,15 +557,15 @@ async function handleUpdatePage(input) {
513
557
  );
514
558
  }
515
559
  const raw = readFileSync3(filePath, "utf8");
516
- const parsed = matter(raw);
560
+ const parsed = parseFrontmatter(raw);
517
561
  const newFm = { ...parsed.data };
518
562
  if (input.title !== void 0) newFm["title"] = input.title;
519
563
  if (input.description !== void 0) newFm["description"] = input.description;
520
564
  if (input.mergeFrontmatter) {
521
565
  Object.assign(newFm, input.mergeFrontmatter);
522
566
  }
523
- const newBody = input.content !== void 0 ? input.content : parsed.content;
524
- const newContent = matter.stringify(newBody.trim(), newFm);
567
+ const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
568
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
525
569
  writeFileSync4(filePath, newContent, "utf8");
526
570
  return [
527
571
  `\u2705 Page updated: ${filePath}`,
@@ -535,35 +579,51 @@ async function handleUpdatePage(input) {
535
579
  // src/tools/migrate-docs.ts
536
580
  import { z as z6 } from "zod";
537
581
  import { migrateDocs } from "create-thally-docs/migrate";
538
- var migrateDocsSchema = z6.object({
582
+ var migrationSourceShape = {
539
583
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
540
- projectDir: z6.string().describe("Path for new project or existing project dir"),
541
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
542
584
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
543
585
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
544
586
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
545
- maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import")
587
+ maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
588
+ platform: z6.enum(["mintlify", "docusaurus"]).optional().describe("Source platform (default: auto-detect)")
589
+ };
590
+ var migrateDocsSchema = z6.object({
591
+ ...migrationSourceShape,
592
+ projectDir: z6.string().describe("Path for the new canonical Thally project; the directory must be absent or empty")
546
593
  });
547
- async function handleMigrateDocs(input) {
594
+ var importDocsSchema = z6.object({
595
+ ...migrationSourceShape,
596
+ projectDir: z6.string().describe("Path to an existing Thally project whose runtime should be preserved")
597
+ });
598
+ async function runMigration(input, isInPlaceImport) {
548
599
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
549
- const result = await migrateDocs({
600
+ return migrateDocs({
550
601
  sourceUrl: input.sourceUrl,
551
602
  projectDir: input.projectDir,
552
- into: input.into ?? false,
603
+ // Keep this decision inside the adapter so stale or adversarial callers
604
+ // cannot turn a template-first migration into an in-place mutation.
605
+ into: isInPlaceImport,
553
606
  apiKey,
554
607
  branch: input.branch,
555
608
  docsDir: input.docsDir,
556
609
  maxPages: input.maxPages,
610
+ platform: input.platform,
557
611
  yes: true
558
612
  });
559
- return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
613
+ }
614
+ async function handleMigrateDocs(input) {
615
+ const result = await runMigration(input, false);
616
+ return `Migration complete! Created a fresh Thally template at ${result.projectDir} and imported ${result.pagesWritten} pages.`;
617
+ }
618
+ async function handleImportDocs(input) {
619
+ const result = await runMigration(input, true);
620
+ return `Import complete! Imported ${result.pagesWritten} pages into the existing Thally project at ${result.projectDir}.`;
560
621
  }
561
622
 
562
623
  // src/tools/search-docs.ts
563
624
  import { z as z7 } from "zod";
564
625
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
565
626
  import { join as join5, relative, extname } from "path";
566
- import matter2 from "gray-matter";
567
627
  var searchDocsSchema = z7.object({
568
628
  projectDir: z7.string().describe("Path to the Thally project root"),
569
629
  query: z7.string().describe("Search query"),
@@ -599,7 +659,7 @@ function scoreFiles(files, contentDir, query) {
599
659
  } catch {
600
660
  continue;
601
661
  }
602
- const { data, content } = matter2(raw);
662
+ const { data, content } = parseFrontmatter(raw);
603
663
  const title = data.title ?? "";
604
664
  const description = data.description ?? "";
605
665
  const keywords = data.keywords ?? [];
@@ -718,7 +778,6 @@ async function handleAgentReadiness(input) {
718
778
  import { z as z10 } from "zod";
719
779
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
720
780
  import { join as join6 } from "path";
721
- import matter3 from "gray-matter";
722
781
  var readPageSchema = z10.object({
723
782
  projectDir: z10.string().describe("Path to the Thally project root"),
724
783
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
@@ -741,15 +800,12 @@ async function handleReadPage(input) {
741
800
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
742
801
  }
743
802
  const raw = readFileSync5(filePath, "utf8");
744
- const { data, content } = matter3(raw);
803
+ const { data, content } = parseFrontmatter(raw);
745
804
  const title = data.title ?? pageId;
746
805
  const description = data.description ?? "";
747
- const lines = [`# ${title}`, `*${pageId}*`, ""];
748
- if (description) {
749
- lines.push(`> ${description}`);
750
- lines.push("");
751
- }
752
- lines.push("---", "", content.trim());
806
+ const lines = [`id: ${pageId}`, `title: ${title}`];
807
+ if (description) lines.push(`description: ${description}`);
808
+ lines.push("", readPageBodyDelimiter(), "", content.trim());
753
809
  return lines.join("\n");
754
810
  }
755
811
 
@@ -757,7 +813,6 @@ async function handleReadPage(input) {
757
813
  import { z as z11 } from "zod";
758
814
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
759
815
  import { join as join7 } from "path";
760
- import matter4 from "gray-matter";
761
816
  var getContextSchema = z11.object({
762
817
  projectDir: z11.string().describe("Path to the Thally project root"),
763
818
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -787,7 +842,7 @@ async function handleGetContext(input) {
787
842
  for (const c of candidates) {
788
843
  if (existsSync6(c)) {
789
844
  const raw = readFileSync6(c, "utf8");
790
- const { content: body } = matter4(raw);
845
+ const { content: body } = parseFrontmatter(raw);
791
846
  content = body.trim();
792
847
  break;
793
848
  }
@@ -813,7 +868,6 @@ async function handleGetContext(input) {
813
868
  import { z as z12 } from "zod";
814
869
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
815
870
  import { join as join8 } from "path";
816
- import matter5 from "gray-matter";
817
871
  var lintProjectSchema = z12.object({
818
872
  projectDir: z12.string().describe("Path to the Thally project root"),
819
873
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -896,7 +950,7 @@ async function handleLintProject(input) {
896
950
  let content = "";
897
951
  try {
898
952
  const raw = readFileSync7(filePath, "utf8");
899
- const parsed = matter5(raw);
953
+ const parsed = parseFrontmatter(raw);
900
954
  data = parsed.data;
901
955
  content = parsed.content;
902
956
  } catch {
@@ -954,7 +1008,6 @@ async function handleLintProject(input) {
954
1008
  import { z as z13 } from "zod";
955
1009
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
956
1010
  import { join as join9, dirname as dirname2 } from "path";
957
- import matter6 from "gray-matter";
958
1011
  import Anthropic from "@anthropic-ai/sdk";
959
1012
  import pLimit from "p-limit";
960
1013
  var translateDocsSchema = z13.object({
@@ -1094,7 +1147,7 @@ async function handleTranslateDocs(input) {
1094
1147
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1095
1148
  try {
1096
1149
  const sourceContent = readFileSync8(sourceFile, "utf8");
1097
- const parsed = matter6(sourceContent);
1150
+ const parsed = parseFrontmatter(sourceContent);
1098
1151
  if (!parsed.data.title) {
1099
1152
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1100
1153
  }
@@ -1449,11 +1502,18 @@ var tools = [
1449
1502
  }),
1450
1503
  defineTool({
1451
1504
  name: "migrate_docs",
1452
- description: "Crawl a docs site and migrate it into a Thally project",
1505
+ description: "Create a fresh canonical Thally template, then migrate a GitHub repository or public docs site into it; the target must be new or empty",
1453
1506
  scope: "project",
1454
1507
  schema: migrateDocsSchema,
1455
1508
  handler: handleMigrateDocs
1456
1509
  }),
1510
+ defineTool({
1511
+ name: "import_docs",
1512
+ description: "Import content into an existing Thally project without scaffolding; use only when an in-place import is explicitly requested",
1513
+ scope: "project",
1514
+ schema: importDocsSchema,
1515
+ handler: handleImportDocs
1516
+ }),
1457
1517
  defineTool({
1458
1518
  name: "search_docs",
1459
1519
  description: "Search documentation pages by keyword \u2014 returns ranked list of matching pages",
package/dist/tools.js CHANGED
@@ -320,6 +320,34 @@ function writeDocsJson(projectDir, config) {
320
320
  writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
321
321
  }
322
322
 
323
+ // src/lib/page-echo.ts
324
+ var BODY_DELIMITER = "--- MDX body ---";
325
+ function readPageBodyDelimiter() {
326
+ return BODY_DELIMITER;
327
+ }
328
+ function stripEchoedPageHeader(content, pageId) {
329
+ const lines = content.split("\n");
330
+ const delimiterIndex = lines.findIndex((line) => line.trim() === BODY_DELIMITER);
331
+ if (delimiterIndex !== -1 && delimiterIndex <= 6) {
332
+ return lines.slice(delimiterIndex + 1).join("\n").replace(/^\n+/, "");
333
+ }
334
+ const first = lines[0]?.trim() ?? "";
335
+ const second = lines[1]?.trim() ?? "";
336
+ if (first.startsWith("# ") && second === `*${pageId}*`) {
337
+ let index = 2;
338
+ while (index < lines.length && lines[index].trim() === "") index += 1;
339
+ if (lines[index]?.trim().startsWith("> ")) {
340
+ index += 1;
341
+ while (index < lines.length && lines[index].trim() === "") index += 1;
342
+ }
343
+ if (lines[index]?.trim() === "---") {
344
+ index += 1;
345
+ }
346
+ return lines.slice(index).join("\n").replace(/^\n+/, "");
347
+ }
348
+ return content;
349
+ }
350
+
323
351
  // src/tools/add-page.ts
324
352
  var addPageSchema = z2.object({
325
353
  projectDir: z2.string().describe("Path to the Thally project root"),
@@ -350,7 +378,7 @@ async function handleAddPage(input) {
350
378
  if (description) {
351
379
  frontmatterLines.push(`description: ${description}`);
352
380
  }
353
- const bodyContent = content ?? `## ${title}
381
+ const bodyContent = content ? stripEchoedPageHeader(content, pageId) : `## ${title}
354
382
 
355
383
  Add your content here.`;
356
384
  const mdxContent = `---
@@ -474,7 +502,23 @@ async function handleListPages(input) {
474
502
  import { z as z5 } from "zod";
475
503
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
476
504
  import { join as join4 } from "path";
505
+
506
+ // src/lib/frontmatter.ts
477
507
  import matter from "gray-matter";
508
+ var FRONTMATTER_OPTIONS = {
509
+ engines: {
510
+ javascript: () => ({}),
511
+ js: () => ({})
512
+ }
513
+ };
514
+ function parseFrontmatter(raw) {
515
+ return matter(raw, FRONTMATTER_OPTIONS);
516
+ }
517
+ function stringifyFrontmatter(body, data) {
518
+ return matter.stringify(body, data);
519
+ }
520
+
521
+ // src/tools/update-page.ts
478
522
  var updatePageSchema = z5.object({
479
523
  projectDir: z5.string().describe("Path to the Thally project root"),
480
524
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -504,15 +548,15 @@ async function handleUpdatePage(input) {
504
548
  );
505
549
  }
506
550
  const raw = readFileSync3(filePath, "utf8");
507
- const parsed = matter(raw);
551
+ const parsed = parseFrontmatter(raw);
508
552
  const newFm = { ...parsed.data };
509
553
  if (input.title !== void 0) newFm["title"] = input.title;
510
554
  if (input.description !== void 0) newFm["description"] = input.description;
511
555
  if (input.mergeFrontmatter) {
512
556
  Object.assign(newFm, input.mergeFrontmatter);
513
557
  }
514
- const newBody = input.content !== void 0 ? input.content : parsed.content;
515
- const newContent = matter.stringify(newBody.trim(), newFm);
558
+ const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
559
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
516
560
  writeFileSync4(filePath, newContent, "utf8");
517
561
  return [
518
562
  `\u2705 Page updated: ${filePath}`,
@@ -526,35 +570,51 @@ async function handleUpdatePage(input) {
526
570
  // src/tools/migrate-docs.ts
527
571
  import { z as z6 } from "zod";
528
572
  import { migrateDocs } from "create-thally-docs/migrate";
529
- var migrateDocsSchema = z6.object({
573
+ var migrationSourceShape = {
530
574
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
531
- projectDir: z6.string().describe("Path for new project or existing project dir"),
532
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
533
575
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
534
576
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
535
577
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
536
- maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import")
578
+ maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
579
+ platform: z6.enum(["mintlify", "docusaurus"]).optional().describe("Source platform (default: auto-detect)")
580
+ };
581
+ var migrateDocsSchema = z6.object({
582
+ ...migrationSourceShape,
583
+ projectDir: z6.string().describe("Path for the new canonical Thally project; the directory must be absent or empty")
537
584
  });
538
- async function handleMigrateDocs(input) {
585
+ var importDocsSchema = z6.object({
586
+ ...migrationSourceShape,
587
+ projectDir: z6.string().describe("Path to an existing Thally project whose runtime should be preserved")
588
+ });
589
+ async function runMigration(input, isInPlaceImport) {
539
590
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
540
- const result = await migrateDocs({
591
+ return migrateDocs({
541
592
  sourceUrl: input.sourceUrl,
542
593
  projectDir: input.projectDir,
543
- into: input.into ?? false,
594
+ // Keep this decision inside the adapter so stale or adversarial callers
595
+ // cannot turn a template-first migration into an in-place mutation.
596
+ into: isInPlaceImport,
544
597
  apiKey,
545
598
  branch: input.branch,
546
599
  docsDir: input.docsDir,
547
600
  maxPages: input.maxPages,
601
+ platform: input.platform,
548
602
  yes: true
549
603
  });
550
- return `Migration complete! ${result.pagesWritten} pages written to ${result.projectDir}/src/content/`;
604
+ }
605
+ async function handleMigrateDocs(input) {
606
+ const result = await runMigration(input, false);
607
+ return `Migration complete! Created a fresh Thally template at ${result.projectDir} and imported ${result.pagesWritten} pages.`;
608
+ }
609
+ async function handleImportDocs(input) {
610
+ const result = await runMigration(input, true);
611
+ return `Import complete! Imported ${result.pagesWritten} pages into the existing Thally project at ${result.projectDir}.`;
551
612
  }
552
613
 
553
614
  // src/tools/search-docs.ts
554
615
  import { z as z7 } from "zod";
555
616
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
556
617
  import { join as join5, relative, extname } from "path";
557
- import matter2 from "gray-matter";
558
618
  var searchDocsSchema = z7.object({
559
619
  projectDir: z7.string().describe("Path to the Thally project root"),
560
620
  query: z7.string().describe("Search query"),
@@ -590,7 +650,7 @@ function scoreFiles(files, contentDir, query) {
590
650
  } catch {
591
651
  continue;
592
652
  }
593
- const { data, content } = matter2(raw);
653
+ const { data, content } = parseFrontmatter(raw);
594
654
  const title = data.title ?? "";
595
655
  const description = data.description ?? "";
596
656
  const keywords = data.keywords ?? [];
@@ -709,7 +769,6 @@ async function handleAgentReadiness(input) {
709
769
  import { z as z10 } from "zod";
710
770
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
711
771
  import { join as join6 } from "path";
712
- import matter3 from "gray-matter";
713
772
  var readPageSchema = z10.object({
714
773
  projectDir: z10.string().describe("Path to the Thally project root"),
715
774
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
@@ -732,15 +791,12 @@ async function handleReadPage(input) {
732
791
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
733
792
  }
734
793
  const raw = readFileSync5(filePath, "utf8");
735
- const { data, content } = matter3(raw);
794
+ const { data, content } = parseFrontmatter(raw);
736
795
  const title = data.title ?? pageId;
737
796
  const description = data.description ?? "";
738
- const lines = [`# ${title}`, `*${pageId}*`, ""];
739
- if (description) {
740
- lines.push(`> ${description}`);
741
- lines.push("");
742
- }
743
- lines.push("---", "", content.trim());
797
+ const lines = [`id: ${pageId}`, `title: ${title}`];
798
+ if (description) lines.push(`description: ${description}`);
799
+ lines.push("", readPageBodyDelimiter(), "", content.trim());
744
800
  return lines.join("\n");
745
801
  }
746
802
 
@@ -748,7 +804,6 @@ async function handleReadPage(input) {
748
804
  import { z as z11 } from "zod";
749
805
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
750
806
  import { join as join7 } from "path";
751
- import matter4 from "gray-matter";
752
807
  var getContextSchema = z11.object({
753
808
  projectDir: z11.string().describe("Path to the Thally project root"),
754
809
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -778,7 +833,7 @@ async function handleGetContext(input) {
778
833
  for (const c of candidates) {
779
834
  if (existsSync6(c)) {
780
835
  const raw = readFileSync6(c, "utf8");
781
- const { content: body } = matter4(raw);
836
+ const { content: body } = parseFrontmatter(raw);
782
837
  content = body.trim();
783
838
  break;
784
839
  }
@@ -804,7 +859,6 @@ async function handleGetContext(input) {
804
859
  import { z as z12 } from "zod";
805
860
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
806
861
  import { join as join8 } from "path";
807
- import matter5 from "gray-matter";
808
862
  var lintProjectSchema = z12.object({
809
863
  projectDir: z12.string().describe("Path to the Thally project root"),
810
864
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -887,7 +941,7 @@ async function handleLintProject(input) {
887
941
  let content = "";
888
942
  try {
889
943
  const raw = readFileSync7(filePath, "utf8");
890
- const parsed = matter5(raw);
944
+ const parsed = parseFrontmatter(raw);
891
945
  data = parsed.data;
892
946
  content = parsed.content;
893
947
  } catch {
@@ -945,7 +999,6 @@ async function handleLintProject(input) {
945
999
  import { z as z13 } from "zod";
946
1000
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
947
1001
  import { join as join9, dirname as dirname2 } from "path";
948
- import matter6 from "gray-matter";
949
1002
  import Anthropic from "@anthropic-ai/sdk";
950
1003
  import pLimit from "p-limit";
951
1004
  var translateDocsSchema = z13.object({
@@ -1085,7 +1138,7 @@ async function handleTranslateDocs(input) {
1085
1138
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1086
1139
  try {
1087
1140
  const sourceContent = readFileSync8(sourceFile, "utf8");
1088
- const parsed = matter6(sourceContent);
1141
+ const parsed = parseFrontmatter(sourceContent);
1089
1142
  if (!parsed.data.title) {
1090
1143
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1091
1144
  }
@@ -1440,11 +1493,18 @@ var tools = [
1440
1493
  }),
1441
1494
  defineTool({
1442
1495
  name: "migrate_docs",
1443
- description: "Crawl a docs site and migrate it into a Thally project",
1496
+ description: "Create a fresh canonical Thally template, then migrate a GitHub repository or public docs site into it; the target must be new or empty",
1444
1497
  scope: "project",
1445
1498
  schema: migrateDocsSchema,
1446
1499
  handler: handleMigrateDocs
1447
1500
  }),
1501
+ defineTool({
1502
+ name: "import_docs",
1503
+ description: "Import content into an existing Thally project without scaffolding; use only when an in-place import is explicitly requested",
1504
+ scope: "project",
1505
+ schema: importDocsSchema,
1506
+ handler: handleImportDocs
1507
+ }),
1448
1508
  defineTool({
1449
1509
  name: "search_docs",
1450
1510
  description: "Search documentation pages by keyword \u2014 returns ranked list of matching pages",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thallylabs/mcp",
3
- "version": "0.7.5",
4
- "description": "MCP server for scaffolding and managing Thally documentation projects",
3
+ "version": "0.8.1",
4
+ "description": "MCP server for managing Thally knowledge surfaces and tracing product changes into documentation work.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@anthropic-ai/sdk": "^0.36.0",
36
36
  "@modelcontextprotocol/sdk": "^1.15.0",
37
- "create-thally-docs": "0.7.9",
37
+ "create-thally-docs": "0.8.0",
38
38
  "gray-matter": "^4.0.3",
39
39
  "p-limit": "^6.1.0",
40
40
  "tar": "^6.2.0",