@thallylabs/mcp 0.8.0 → 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
@@ -511,7 +511,23 @@ async function handleListPages(input) {
511
511
  import { z as z5 } from "zod";
512
512
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
513
513
  import { join as join4 } from "path";
514
+
515
+ // src/lib/frontmatter.ts
514
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
515
531
  var updatePageSchema = z5.object({
516
532
  projectDir: z5.string().describe("Path to the Thally project root"),
517
533
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -541,7 +557,7 @@ async function handleUpdatePage(input) {
541
557
  );
542
558
  }
543
559
  const raw = readFileSync3(filePath, "utf8");
544
- const parsed = matter(raw);
560
+ const parsed = parseFrontmatter(raw);
545
561
  const newFm = { ...parsed.data };
546
562
  if (input.title !== void 0) newFm["title"] = input.title;
547
563
  if (input.description !== void 0) newFm["description"] = input.description;
@@ -549,7 +565,7 @@ async function handleUpdatePage(input) {
549
565
  Object.assign(newFm, input.mergeFrontmatter);
550
566
  }
551
567
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
552
- const newContent = matter.stringify(newBody.trim(), newFm);
568
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
553
569
  writeFileSync4(filePath, newContent, "utf8");
554
570
  return [
555
571
  `\u2705 Page updated: ${filePath}`,
@@ -563,22 +579,30 @@ async function handleUpdatePage(input) {
563
579
  // src/tools/migrate-docs.ts
564
580
  import { z as z6 } from "zod";
565
581
  import { migrateDocs } from "create-thally-docs/migrate";
566
- var migrateDocsSchema = z6.object({
582
+ var migrationSourceShape = {
567
583
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
568
- projectDir: z6.string().describe("Path for new project or existing project dir"),
569
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
570
584
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
571
585
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
572
586
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
573
587
  maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
574
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")
575
593
  });
576
- 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) {
577
599
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
578
- const result = await migrateDocs({
600
+ return migrateDocs({
579
601
  sourceUrl: input.sourceUrl,
580
602
  projectDir: input.projectDir,
581
- 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,
582
606
  apiKey,
583
607
  branch: input.branch,
584
608
  docsDir: input.docsDir,
@@ -586,14 +610,20 @@ async function handleMigrateDocs(input) {
586
610
  platform: input.platform,
587
611
  yes: true
588
612
  });
589
- 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}.`;
590
621
  }
591
622
 
592
623
  // src/tools/search-docs.ts
593
624
  import { z as z7 } from "zod";
594
625
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
595
626
  import { join as join5, relative, extname } from "path";
596
- import matter2 from "gray-matter";
597
627
  var searchDocsSchema = z7.object({
598
628
  projectDir: z7.string().describe("Path to the Thally project root"),
599
629
  query: z7.string().describe("Search query"),
@@ -629,7 +659,7 @@ function scoreFiles(files, contentDir, query) {
629
659
  } catch {
630
660
  continue;
631
661
  }
632
- const { data, content } = matter2(raw);
662
+ const { data, content } = parseFrontmatter(raw);
633
663
  const title = data.title ?? "";
634
664
  const description = data.description ?? "";
635
665
  const keywords = data.keywords ?? [];
@@ -748,7 +778,6 @@ async function handleAgentReadiness(input) {
748
778
  import { z as z10 } from "zod";
749
779
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
750
780
  import { join as join6 } from "path";
751
- import matter3 from "gray-matter";
752
781
  var readPageSchema = z10.object({
753
782
  projectDir: z10.string().describe("Path to the Thally project root"),
754
783
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
@@ -771,7 +800,7 @@ async function handleReadPage(input) {
771
800
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
772
801
  }
773
802
  const raw = readFileSync5(filePath, "utf8");
774
- const { data, content } = matter3(raw);
803
+ const { data, content } = parseFrontmatter(raw);
775
804
  const title = data.title ?? pageId;
776
805
  const description = data.description ?? "";
777
806
  const lines = [`id: ${pageId}`, `title: ${title}`];
@@ -784,7 +813,6 @@ async function handleReadPage(input) {
784
813
  import { z as z11 } from "zod";
785
814
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
786
815
  import { join as join7 } from "path";
787
- import matter4 from "gray-matter";
788
816
  var getContextSchema = z11.object({
789
817
  projectDir: z11.string().describe("Path to the Thally project root"),
790
818
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -814,7 +842,7 @@ async function handleGetContext(input) {
814
842
  for (const c of candidates) {
815
843
  if (existsSync6(c)) {
816
844
  const raw = readFileSync6(c, "utf8");
817
- const { content: body } = matter4(raw);
845
+ const { content: body } = parseFrontmatter(raw);
818
846
  content = body.trim();
819
847
  break;
820
848
  }
@@ -840,7 +868,6 @@ async function handleGetContext(input) {
840
868
  import { z as z12 } from "zod";
841
869
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
842
870
  import { join as join8 } from "path";
843
- import matter5 from "gray-matter";
844
871
  var lintProjectSchema = z12.object({
845
872
  projectDir: z12.string().describe("Path to the Thally project root"),
846
873
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -923,7 +950,7 @@ async function handleLintProject(input) {
923
950
  let content = "";
924
951
  try {
925
952
  const raw = readFileSync7(filePath, "utf8");
926
- const parsed = matter5(raw);
953
+ const parsed = parseFrontmatter(raw);
927
954
  data = parsed.data;
928
955
  content = parsed.content;
929
956
  } catch {
@@ -981,7 +1008,6 @@ async function handleLintProject(input) {
981
1008
  import { z as z13 } from "zod";
982
1009
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
983
1010
  import { join as join9, dirname as dirname2 } from "path";
984
- import matter6 from "gray-matter";
985
1011
  import Anthropic from "@anthropic-ai/sdk";
986
1012
  import pLimit from "p-limit";
987
1013
  var translateDocsSchema = z13.object({
@@ -1121,7 +1147,7 @@ async function handleTranslateDocs(input) {
1121
1147
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1122
1148
  try {
1123
1149
  const sourceContent = readFileSync8(sourceFile, "utf8");
1124
- const parsed = matter6(sourceContent);
1150
+ const parsed = parseFrontmatter(sourceContent);
1125
1151
  if (!parsed.data.title) {
1126
1152
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1127
1153
  }
@@ -1476,11 +1502,18 @@ var tools = [
1476
1502
  }),
1477
1503
  defineTool({
1478
1504
  name: "migrate_docs",
1479
- 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",
1480
1506
  scope: "project",
1481
1507
  schema: migrateDocsSchema,
1482
1508
  handler: handleMigrateDocs
1483
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
+ }),
1484
1517
  defineTool({
1485
1518
  name: "search_docs",
1486
1519
  description: "Search documentation pages by keyword \u2014 returns ranked list of matching pages",
package/dist/tools.js CHANGED
@@ -502,7 +502,23 @@ async function handleListPages(input) {
502
502
  import { z as z5 } from "zod";
503
503
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
504
504
  import { join as join4 } from "path";
505
+
506
+ // src/lib/frontmatter.ts
505
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
506
522
  var updatePageSchema = z5.object({
507
523
  projectDir: z5.string().describe("Path to the Thally project root"),
508
524
  pageId: z5.string().describe('Page identifier (e.g. "guides/auth"). No .mdx extension.'),
@@ -532,7 +548,7 @@ async function handleUpdatePage(input) {
532
548
  );
533
549
  }
534
550
  const raw = readFileSync3(filePath, "utf8");
535
- const parsed = matter(raw);
551
+ const parsed = parseFrontmatter(raw);
536
552
  const newFm = { ...parsed.data };
537
553
  if (input.title !== void 0) newFm["title"] = input.title;
538
554
  if (input.description !== void 0) newFm["description"] = input.description;
@@ -540,7 +556,7 @@ async function handleUpdatePage(input) {
540
556
  Object.assign(newFm, input.mergeFrontmatter);
541
557
  }
542
558
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
543
- const newContent = matter.stringify(newBody.trim(), newFm);
559
+ const newContent = stringifyFrontmatter(newBody.trim(), newFm);
544
560
  writeFileSync4(filePath, newContent, "utf8");
545
561
  return [
546
562
  `\u2705 Page updated: ${filePath}`,
@@ -554,22 +570,30 @@ async function handleUpdatePage(input) {
554
570
  // src/tools/migrate-docs.ts
555
571
  import { z as z6 } from "zod";
556
572
  import { migrateDocs } from "create-thally-docs/migrate";
557
- var migrateDocsSchema = z6.object({
573
+ var migrationSourceShape = {
558
574
  sourceUrl: z6.string().describe("GitHub repository URL or public documentation URL to migrate"),
559
- projectDir: z6.string().describe("Path for new project or existing project dir"),
560
- into: z6.boolean().optional().default(false).describe("Migrate into existing project instead of scaffolding"),
561
575
  branch: z6.string().optional().describe("Git branch (default: auto-detect)"),
562
576
  docsDir: z6.string().optional().describe("Docs subdirectory in repo (default: auto-detect)"),
563
577
  apiKey: z6.string().optional().describe("Anthropic API key for non-Markdown file conversion"),
564
578
  maxPages: z6.number().int().min(1).max(1e3).optional().describe("Maximum public URL pages to import"),
565
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")
566
584
  });
567
- 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) {
568
590
  const apiKey = input.apiKey ?? process.env.ANTHROPIC_API_KEY;
569
- const result = await migrateDocs({
591
+ return migrateDocs({
570
592
  sourceUrl: input.sourceUrl,
571
593
  projectDir: input.projectDir,
572
- 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,
573
597
  apiKey,
574
598
  branch: input.branch,
575
599
  docsDir: input.docsDir,
@@ -577,14 +601,20 @@ async function handleMigrateDocs(input) {
577
601
  platform: input.platform,
578
602
  yes: true
579
603
  });
580
- 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}.`;
581
612
  }
582
613
 
583
614
  // src/tools/search-docs.ts
584
615
  import { z as z7 } from "zod";
585
616
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
586
617
  import { join as join5, relative, extname } from "path";
587
- import matter2 from "gray-matter";
588
618
  var searchDocsSchema = z7.object({
589
619
  projectDir: z7.string().describe("Path to the Thally project root"),
590
620
  query: z7.string().describe("Search query"),
@@ -620,7 +650,7 @@ function scoreFiles(files, contentDir, query) {
620
650
  } catch {
621
651
  continue;
622
652
  }
623
- const { data, content } = matter2(raw);
653
+ const { data, content } = parseFrontmatter(raw);
624
654
  const title = data.title ?? "";
625
655
  const description = data.description ?? "";
626
656
  const keywords = data.keywords ?? [];
@@ -739,7 +769,6 @@ async function handleAgentReadiness(input) {
739
769
  import { z as z10 } from "zod";
740
770
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
741
771
  import { join as join6 } from "path";
742
- import matter3 from "gray-matter";
743
772
  var readPageSchema = z10.object({
744
773
  projectDir: z10.string().describe("Path to the Thally project root"),
745
774
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
@@ -762,7 +791,7 @@ async function handleReadPage(input) {
762
791
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
763
792
  }
764
793
  const raw = readFileSync5(filePath, "utf8");
765
- const { data, content } = matter3(raw);
794
+ const { data, content } = parseFrontmatter(raw);
766
795
  const title = data.title ?? pageId;
767
796
  const description = data.description ?? "";
768
797
  const lines = [`id: ${pageId}`, `title: ${title}`];
@@ -775,7 +804,6 @@ async function handleReadPage(input) {
775
804
  import { z as z11 } from "zod";
776
805
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
777
806
  import { join as join7 } from "path";
778
- import matter4 from "gray-matter";
779
807
  var getContextSchema = z11.object({
780
808
  projectDir: z11.string().describe("Path to the Thally project root"),
781
809
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -805,7 +833,7 @@ async function handleGetContext(input) {
805
833
  for (const c of candidates) {
806
834
  if (existsSync6(c)) {
807
835
  const raw = readFileSync6(c, "utf8");
808
- const { content: body } = matter4(raw);
836
+ const { content: body } = parseFrontmatter(raw);
809
837
  content = body.trim();
810
838
  break;
811
839
  }
@@ -831,7 +859,6 @@ async function handleGetContext(input) {
831
859
  import { z as z12 } from "zod";
832
860
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
833
861
  import { join as join8 } from "path";
834
- import matter5 from "gray-matter";
835
862
  var lintProjectSchema = z12.object({
836
863
  projectDir: z12.string().describe("Path to the Thally project root"),
837
864
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -914,7 +941,7 @@ async function handleLintProject(input) {
914
941
  let content = "";
915
942
  try {
916
943
  const raw = readFileSync7(filePath, "utf8");
917
- const parsed = matter5(raw);
944
+ const parsed = parseFrontmatter(raw);
918
945
  data = parsed.data;
919
946
  content = parsed.content;
920
947
  } catch {
@@ -972,7 +999,6 @@ async function handleLintProject(input) {
972
999
  import { z as z13 } from "zod";
973
1000
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
974
1001
  import { join as join9, dirname as dirname2 } from "path";
975
- import matter6 from "gray-matter";
976
1002
  import Anthropic from "@anthropic-ai/sdk";
977
1003
  import pLimit from "p-limit";
978
1004
  var translateDocsSchema = z13.object({
@@ -1112,7 +1138,7 @@ async function handleTranslateDocs(input) {
1112
1138
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1113
1139
  try {
1114
1140
  const sourceContent = readFileSync8(sourceFile, "utf8");
1115
- const parsed = matter6(sourceContent);
1141
+ const parsed = parseFrontmatter(sourceContent);
1116
1142
  if (!parsed.data.title) {
1117
1143
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1118
1144
  }
@@ -1467,11 +1493,18 @@ var tools = [
1467
1493
  }),
1468
1494
  defineTool({
1469
1495
  name: "migrate_docs",
1470
- 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",
1471
1497
  scope: "project",
1472
1498
  schema: migrateDocsSchema,
1473
1499
  handler: handleMigrateDocs
1474
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
+ }),
1475
1508
  defineTool({
1476
1509
  name: "search_docs",
1477
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.8.0",
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"