@thallylabs/mcp 0.8.1 → 0.9.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/tools.js CHANGED
@@ -2,258 +2,18 @@
2
2
  import { z } from "zod";
3
3
 
4
4
  // src/lib/scaffold.ts
5
- import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, cpSync } from "fs";
6
- import { resolve, join } from "path";
7
- import { execSync } from "child_process";
8
- import { Readable, pipeline } from "stream";
9
- import { promisify } from "util";
10
- import tar from "tar";
11
- var pipelineAsync = promisify(pipeline);
12
- var MCP_TEMPLATE_REPOSITORY = "thallylabs/docs";
13
- var TARBALL_URL = `https://codeload.github.com/${MCP_TEMPLATE_REPOSITORY}/tar.gz/main`;
14
- var MCP_EXCLUDE_PATHS = [
15
- "/cli/",
16
- "/packages/",
17
- "/node_modules/",
18
- "/.git/",
19
- "/thally-track.yml",
20
- "/CODEOWNERS",
21
- "/CLAUDE.md",
22
- "/notes/"
23
- ];
24
- function shouldIncludeMcpTemplatePath(path) {
25
- return !MCP_EXCLUDE_PATHS.some((excluded) => path.includes(excluded));
26
- }
27
- var STARTER_PAGES = {
28
- "introduction.mdx": `---
29
- title: Introduction
30
- description: Welcome to {NAME} documentation.
31
- ---
32
-
33
- ## Welcome
34
-
35
- This is the home page of your **{NAME}** documentation site, powered by [Thally](https://github.com/thallylabs/thally).
36
-
37
- Get started by editing this file at \`src/content/introduction.mdx\`.
38
- `,
39
- "quickstart.mdx": `---
40
- title: Quickstart
41
- description: Get up and running with {NAME} in under 5 minutes.
42
- ---
43
-
44
- ## Installation
45
-
46
- \`\`\`bash
47
- npm install {SLUG}
48
- \`\`\`
49
-
50
- ## Basic usage
51
-
52
- \`\`\`ts
53
- import { create } from '{SLUG}'
54
-
55
- const client = create({ apiKey: 'your-api-key' })
56
- \`\`\`
57
-
58
- That's it \u2014 you're ready to go!
59
- `
60
- };
61
- function buildStarterDocsJson({
62
- enableAiChat,
63
- repoUrl,
64
- i18nLocales
65
- }) {
66
- const config = {};
67
- if (enableAiChat) {
68
- config.ai = { chat: true };
69
- }
70
- if (repoUrl) {
71
- config.navbar = {
72
- links: [{ label: "GitHub", href: repoUrl, type: "github" }],
73
- primary: { label: "Get started", href: "/quickstart" }
74
- };
75
- }
76
- if (i18nLocales && i18nLocales.length > 0) {
77
- config.i18n = {
78
- defaultLocale: "en",
79
- locales: [{ code: "en", label: "English" }, ...i18nLocales]
80
- };
81
- }
82
- config.tabs = [
83
- {
84
- tab: "Overview",
85
- groups: [{ group: "Getting Started", pages: ["introduction", "quickstart"] }]
86
- },
87
- { tab: "API Reference", api: { source: "openapi.yaml" } },
88
- { tab: "Changelog", href: "/changelog" }
89
- ];
90
- return JSON.stringify(config, null, 2) + "\n";
91
- }
92
- function slugify(name) {
93
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
94
- }
95
- function run(cmd, cwd) {
96
- execSync(cmd, { cwd, stdio: "inherit" });
97
- }
98
- async function downloadTemplate(targetDir) {
99
- const response = await fetch(TARBALL_URL);
100
- if (!response.ok) {
101
- throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
102
- }
103
- if (!response.body) {
104
- throw new Error("Response body is empty");
105
- }
106
- const nodeStream = Readable.fromWeb(response.body);
107
- await pipelineAsync(
108
- nodeStream,
109
- tar.extract({
110
- cwd: targetDir,
111
- strip: 1,
112
- filter: shouldIncludeMcpTemplatePath
113
- })
114
- );
115
- }
116
- function writeStarterContent(targetDir, projectName, slug, enableAiChat = true, repoUrl = "", i18nLocales) {
117
- const contentDir = join(targetDir, "src", "content");
118
- if (existsSync(contentDir)) {
119
- const entries = readdirSync(contentDir);
120
- for (const entry of entries) {
121
- execSync(`rm -rf "${join(contentDir, entry)}"`);
122
- }
123
- } else {
124
- mkdirSync(contentDir, { recursive: true });
125
- }
126
- for (const [filename, template] of Object.entries(STARTER_PAGES)) {
127
- const content = template.replace(/\{NAME\}/g, projectName).replace(/\{SLUG\}/g, slug);
128
- writeFileSync(join(contentDir, filename), content, "utf8");
129
- }
130
- writeFileSync(
131
- join(targetDir, "docs.json"),
132
- buildStarterDocsJson({ enableAiChat, repoUrl: repoUrl || void 0, i18nLocales }),
133
- "utf8"
134
- );
135
- }
136
- function updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl) {
137
- const siteFile = join(targetDir, "src", "data", "site.ts");
138
- if (!existsSync(siteFile)) return;
139
- let source = readFileSync(siteFile, "utf8");
140
- source = source.replace(/name:\s*'[^']*'/, `name: '${projectName.replace(/'/g, "\\'")}'`);
141
- source = source.replace(
142
- /description:\s*\n\s*'[^']*'/,
143
- `description:
144
- '${description.replace(/'/g, "\\'")}'`
145
- );
146
- source = source.replace(
147
- /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
148
- `const brandPreset: BrandPresetKey = '${brandPreset}'`
149
- );
150
- if (repoUrl) {
151
- source = source.replace(/repoUrl:\s*'[^']*'/, `repoUrl: '${repoUrl}'`);
152
- source = source.replace(
153
- /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
154
- `{ label: 'GitHub', href: '${repoUrl}' }`
155
- );
156
- source = source.replace(
157
- /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
158
- `{ label: 'Support', href: '${repoUrl}/issues/new' }`
159
- );
160
- }
161
- writeFileSync(siteFile, source, "utf8");
162
- }
163
- function patchTopBarNavigation(targetDir) {
164
- const filePath = join(targetDir, "src", "components", "layout", "top-bar.tsx");
165
- if (!existsSync(filePath)) return;
166
- const source = readFileSync(filePath, "utf8");
167
- if (!source.includes("target={isExternal ? '_blank' : undefined}")) return;
168
- const patched = source.replace(
169
- /if \(collection\.href\) \{\n const isExternal[^\n]+\n return \(\n <a[\s\S]*?<\/a>\n \)\n \}/,
170
- `if (collection.href) {
171
- const isExternal = /^https?:\\/\\//.test(collection.href)
172
- if (isExternal) {
173
- return (
174
- <a
175
- key={collection.id}
176
- href={collection.href}
177
- target="_blank"
178
- rel="noreferrer"
179
- className={baseClasses}
180
- >
181
- {collection.label}
182
- </a>
183
- )
184
- }
185
- return (
186
- <Link
187
- key={collection.id}
188
- href={collection.href}
189
- className={baseClasses}
190
- >
191
- {collection.label}
192
- </Link>
193
- )
194
- }`
195
- );
196
- writeFileSync(filePath, patched, "utf8");
197
- }
198
- function patchApiReferenceGuard(targetDir) {
199
- const filePath = join(targetDir, "src", "data", "api-reference.ts");
200
- if (!existsSync(filePath)) return;
201
- let source = readFileSync(filePath, "utf8");
202
- source = source.replace(
203
- /export async function buildApiNavigation\([^)]*\)[^{]*\{\n/,
204
- (match) => `${match} if (apiReferenceConfig.specs.length === 0) return []
205
- `
206
- );
207
- writeFileSync(filePath, source, "utf8");
208
- }
209
- function patchOpenApiFetch(targetDir) {
210
- const filePath = join(targetDir, "src", "lib", "openapi", "fetch.ts");
211
- if (!existsSync(filePath)) return;
212
- let source = readFileSync(filePath, "utf8");
213
- source = source.replace(
214
- /const absolutePath = path\.isAbsolute\(filePath\) \? filePath : path\.resolve\(process\.cwd\(\), filePath\)/,
215
- `const absolutePath = filePath.startsWith('/')
216
- ? path.resolve(process.cwd(), 'public', filePath.slice(1))
217
- : path.resolve(process.cwd(), filePath)`
218
- );
219
- writeFileSync(filePath, source, "utf8");
220
- }
221
- function updateEnvExample(targetDir) {
222
- const envFile = join(targetDir, ".env.example");
223
- if (existsSync(envFile)) {
224
- const envLocal = join(targetDir, ".env.local");
225
- if (!existsSync(envLocal)) cpSync(envFile, envLocal);
226
- }
227
- }
228
- function installDeps(targetDir) {
229
- run("npm install", targetDir);
230
- }
231
- function initGit(targetDir) {
232
- try {
233
- run("git init", targetDir);
234
- run("git add -A", targetDir);
235
- run('git commit -m "Initial commit from create-thally-docs"', targetDir);
236
- } catch {
237
- }
238
- }
5
+ import {
6
+ EXCLUDE_PATHS,
7
+ STABLE_SCAFFOLD_RELEASE,
8
+ TEMPLATE_REPOSITORY,
9
+ scaffold as scaffoldProject,
10
+ shouldInclude
11
+ } from "create-thally-docs/scaffold";
12
+ import { buildStarterDocsJson } from "create-thally-docs/starter";
13
+ var MCP_TEMPLATE_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
14
+ var MCP_SCAFFOLD_RELEASE_ID = STABLE_SCAFFOLD_RELEASE.id;
239
15
  async function scaffold(options) {
240
- const { projectDir, projectName, description, brandPreset, repoUrl, doInstall, enableAiChat = true, i18nLocales } = options;
241
- const targetDir = resolve(projectDir);
242
- if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
243
- throw new Error(`Directory "${targetDir}" already exists and is not empty.`);
244
- }
245
- mkdirSync(targetDir, { recursive: true });
246
- const slug = slugify(projectName);
247
- await downloadTemplate(targetDir);
248
- writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
249
- updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
250
- patchApiReferenceGuard(targetDir);
251
- patchTopBarNavigation(targetDir);
252
- patchOpenApiFetch(targetDir);
253
- updateEnvExample(targetDir);
254
- if (doInstall) installDeps(targetDir);
255
- initGit(targetDir);
256
- return { projectDir: targetDir };
16
+ return scaffoldProject(options);
257
17
  }
258
18
 
259
19
  // src/tools/create-project.ts
@@ -265,7 +25,7 @@ var createProjectSchema = z.object({
265
25
  repoUrl: z.string().optional().describe("GitHub repository URL (optional)"),
266
26
  install: z.boolean().optional().default(true).describe("Whether to run npm install after scaffolding"),
267
27
  enableAiChat: z.boolean().optional().default(true).describe("Enable AI chat in docs.json (default true)"),
268
- i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Secondary locales to enable (e.g. [{code:"es",label:"Espa\xF1ol"}])')
28
+ i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Additional locales beyond the included English and Spanish defaults (e.g. [{code:"fr",label:"Fran\xE7ais"}])')
269
29
  });
270
30
  async function handleCreateProject(input) {
271
31
  const { projectDir, brandPreset = "primary", install = true } = input;
@@ -304,20 +64,20 @@ async function handleCreateProject(input) {
304
64
 
305
65
  // src/tools/add-page.ts
306
66
  import { z as z2 } from "zod";
307
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
308
- import { join as join3, dirname } from "path";
67
+ import { existsSync, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
68
+ import { join as join2, dirname } from "path";
309
69
 
310
70
  // src/lib/docs-json.ts
311
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
312
- import { join as join2 } from "path";
71
+ import { readFileSync, writeFileSync } from "fs";
72
+ import { join } from "path";
313
73
  function readDocsJson(projectDir) {
314
- const docsPath = join2(projectDir, "docs.json");
315
- const raw = readFileSync2(docsPath, "utf8");
74
+ const docsPath = join(projectDir, "docs.json");
75
+ const raw = readFileSync(docsPath, "utf8");
316
76
  return JSON.parse(raw);
317
77
  }
318
78
  function writeDocsJson(projectDir, config) {
319
- const docsPath = join2(projectDir, "docs.json");
320
- writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
79
+ const docsPath = join(projectDir, "docs.json");
80
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
321
81
  }
322
82
 
323
83
  // src/lib/page-echo.ts
@@ -369,11 +129,11 @@ async function handleAddPage(input) {
369
129
  `Invalid pageId "${pageId}". Use only alphanumeric characters, hyphens, and slashes. Do not include .mdx extension.`
370
130
  );
371
131
  }
372
- const mdxPath = join3(projectDir, "src", "content", `${pageId}.mdx`);
373
- if (existsSync2(mdxPath)) {
132
+ const mdxPath = join2(projectDir, "src", "content", `${pageId}.mdx`);
133
+ if (existsSync(mdxPath)) {
374
134
  throw new Error(`Page already exists at: ${mdxPath}`);
375
135
  }
376
- mkdirSync2(dirname(mdxPath), { recursive: true });
136
+ mkdirSync(dirname(mdxPath), { recursive: true });
377
137
  const frontmatterLines = [`title: ${title}`];
378
138
  if (description) {
379
139
  frontmatterLines.push(`description: ${description}`);
@@ -387,7 +147,7 @@ ${frontmatterLines.join("\n")}
387
147
 
388
148
  ${bodyContent}
389
149
  `;
390
- writeFileSync3(mdxPath, mdxContent, "utf8");
150
+ writeFileSync2(mdxPath, mdxContent, "utf8");
391
151
  const config = readDocsJson(projectDir);
392
152
  let targetTab = config.tabs[0];
393
153
  if (input.tab) {
@@ -500,8 +260,8 @@ async function handleListPages(input) {
500
260
 
501
261
  // src/tools/update-page.ts
502
262
  import { z as z5 } from "zod";
503
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
504
- import { join as join4 } from "path";
263
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
264
+ import { join as join3 } from "path";
505
265
 
506
266
  // src/lib/frontmatter.ts
507
267
  import matter from "gray-matter";
@@ -529,11 +289,11 @@ var updatePageSchema = z5.object({
529
289
  });
530
290
  function findPageFile(projectDir, pageId) {
531
291
  const candidates = [
532
- join4(projectDir, "src", "content", `${pageId}.mdx`),
533
- join4(projectDir, "src", "content", `${pageId}/index.mdx`)
292
+ join3(projectDir, "src", "content", `${pageId}.mdx`),
293
+ join3(projectDir, "src", "content", `${pageId}/index.mdx`)
534
294
  ];
535
295
  for (const candidate of candidates) {
536
- if (existsSync3(candidate)) return candidate;
296
+ if (existsSync2(candidate)) return candidate;
537
297
  }
538
298
  return null;
539
299
  }
@@ -547,7 +307,7 @@ async function handleUpdatePage(input) {
547
307
  src/content/${pageId}/index.mdx`
548
308
  );
549
309
  }
550
- const raw = readFileSync3(filePath, "utf8");
310
+ const raw = readFileSync2(filePath, "utf8");
551
311
  const parsed = parseFrontmatter(raw);
552
312
  const newFm = { ...parsed.data };
553
313
  if (input.title !== void 0) newFm["title"] = input.title;
@@ -557,7 +317,7 @@ async function handleUpdatePage(input) {
557
317
  }
558
318
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
559
319
  const newContent = stringifyFrontmatter(newBody.trim(), newFm);
560
- writeFileSync4(filePath, newContent, "utf8");
320
+ writeFileSync3(filePath, newContent, "utf8");
561
321
  return [
562
322
  `\u2705 Page updated: ${filePath}`,
563
323
  ` pageId: ${pageId}`,
@@ -613,8 +373,8 @@ async function handleImportDocs(input) {
613
373
 
614
374
  // src/tools/search-docs.ts
615
375
  import { z as z7 } from "zod";
616
- import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
617
- import { join as join5, relative, extname } from "path";
376
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
377
+ import { join as join4, relative, extname } from "path";
618
378
  var searchDocsSchema = z7.object({
619
379
  projectDir: z7.string().describe("Path to the Thally project root"),
620
380
  query: z7.string().describe("Search query"),
@@ -623,12 +383,12 @@ var searchDocsSchema = z7.object({
623
383
  function scanMdxFiles(dir, results) {
624
384
  let entries;
625
385
  try {
626
- entries = readdirSync2(dir);
386
+ entries = readdirSync(dir);
627
387
  } catch {
628
388
  return;
629
389
  }
630
390
  for (const entry of entries) {
631
- const fullPath = join5(dir, entry);
391
+ const fullPath = join4(dir, entry);
632
392
  try {
633
393
  const stat = statSync(fullPath);
634
394
  if (stat.isDirectory()) {
@@ -646,7 +406,7 @@ function scoreFiles(files, contentDir, query) {
646
406
  for (const filePath of files) {
647
407
  let raw;
648
408
  try {
649
- raw = readFileSync4(filePath, "utf8");
409
+ raw = readFileSync3(filePath, "utf8");
650
410
  } catch {
651
411
  continue;
652
412
  }
@@ -671,8 +431,8 @@ function scoreFiles(files, contentDir, query) {
671
431
  }
672
432
  async function handleSearchDocs(input) {
673
433
  const { projectDir, query, limit = 5 } = input;
674
- const contentDir = join5(projectDir, "src", "content");
675
- if (!existsSync4(contentDir)) {
434
+ const contentDir = join4(projectDir, "src", "content");
435
+ if (!existsSync3(contentDir)) {
676
436
  throw new Error(`Content directory not found: ${contentDir}`);
677
437
  }
678
438
  const files = [];
@@ -767,22 +527,22 @@ async function handleAgentReadiness(input) {
767
527
 
768
528
  // src/tools/read-page.ts
769
529
  import { z as z10 } from "zod";
770
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
771
- import { join as join6 } from "path";
530
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
531
+ import { join as join5 } from "path";
772
532
  var readPageSchema = z10.object({
773
533
  projectDir: z10.string().describe("Path to the Thally project root"),
774
534
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
775
535
  });
776
536
  async function handleReadPage(input) {
777
537
  const { projectDir, pageId } = input;
778
- const contentDir = join6(projectDir, "src", "content");
538
+ const contentDir = join5(projectDir, "src", "content");
779
539
  const candidates = [
780
- join6(contentDir, `${pageId}.mdx`),
781
- join6(contentDir, `${pageId}/index.mdx`)
540
+ join5(contentDir, `${pageId}.mdx`),
541
+ join5(contentDir, `${pageId}/index.mdx`)
782
542
  ];
783
543
  let filePath = null;
784
544
  for (const c of candidates) {
785
- if (existsSync5(c)) {
545
+ if (existsSync4(c)) {
786
546
  filePath = c;
787
547
  break;
788
548
  }
@@ -790,7 +550,7 @@ async function handleReadPage(input) {
790
550
  if (!filePath) {
791
551
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
792
552
  }
793
- const raw = readFileSync5(filePath, "utf8");
553
+ const raw = readFileSync4(filePath, "utf8");
794
554
  const { data, content } = parseFrontmatter(raw);
795
555
  const title = data.title ?? pageId;
796
556
  const description = data.description ?? "";
@@ -802,8 +562,8 @@ async function handleReadPage(input) {
802
562
 
803
563
  // src/tools/get-context.ts
804
564
  import { z as z11 } from "zod";
805
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
806
- import { join as join7 } from "path";
565
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
566
+ import { join as join6 } from "path";
807
567
  var getContextSchema = z11.object({
808
568
  projectDir: z11.string().describe("Path to the Thally project root"),
809
569
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -811,8 +571,8 @@ var getContextSchema = z11.object({
811
571
  });
812
572
  async function handleGetContext(input) {
813
573
  const { projectDir, topic, maxTokens = 4e3 } = input;
814
- const contentDir = join7(projectDir, "src", "content");
815
- if (!existsSync6(contentDir)) {
574
+ const contentDir = join6(projectDir, "src", "content");
575
+ if (!existsSync5(contentDir)) {
816
576
  throw new Error(`Content directory not found: ${contentDir}`);
817
577
  }
818
578
  const files = [];
@@ -826,13 +586,13 @@ async function handleGetContext(input) {
826
586
  const sections = [];
827
587
  for (const result of scored) {
828
588
  const candidates = [
829
- join7(contentDir, `${result.pageId}.mdx`),
830
- join7(contentDir, `${result.pageId}/index.mdx`)
589
+ join6(contentDir, `${result.pageId}.mdx`),
590
+ join6(contentDir, `${result.pageId}/index.mdx`)
831
591
  ];
832
592
  let content = "";
833
593
  for (const c of candidates) {
834
- if (existsSync6(c)) {
835
- const raw = readFileSync6(c, "utf8");
594
+ if (existsSync5(c)) {
595
+ const raw = readFileSync5(c, "utf8");
836
596
  const { content: body } = parseFrontmatter(raw);
837
597
  content = body.trim();
838
598
  break;
@@ -857,8 +617,8 @@ async function handleGetContext(input) {
857
617
 
858
618
  // src/tools/lint-project.ts
859
619
  import { z as z12 } from "zod";
860
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
861
- import { join as join8 } from "path";
620
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
621
+ import { join as join7 } from "path";
862
622
  var lintProjectSchema = z12.object({
863
623
  projectDir: z12.string().describe("Path to the Thally project root"),
864
624
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -889,9 +649,9 @@ function addOrphanToNav(projectDir, pageId) {
889
649
  }
890
650
  async function handleLintProject(input) {
891
651
  const { projectDir, fix = false } = input;
892
- const contentDir = join8(projectDir, "src", "content");
652
+ const contentDir = join7(projectDir, "src", "content");
893
653
  const issues = [];
894
- if (!existsSync7(join8(projectDir, "docs.json"))) {
654
+ if (!existsSync6(join7(projectDir, "docs.json"))) {
895
655
  throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
896
656
  }
897
657
  const config = readDocsJson(projectDir);
@@ -910,10 +670,10 @@ async function handleLintProject(input) {
910
670
  }
911
671
  for (const pageId of navPageIds) {
912
672
  const candidates = [
913
- join8(contentDir, `${pageId}.mdx`),
914
- join8(contentDir, `${pageId}/index.mdx`)
673
+ join7(contentDir, `${pageId}.mdx`),
674
+ join7(contentDir, `${pageId}/index.mdx`)
915
675
  ];
916
- if (!candidates.some((c) => existsSync7(c))) {
676
+ if (!candidates.some((c) => existsSync6(c))) {
917
677
  issues.push({
918
678
  severity: "error",
919
679
  message: `"${pageId}" is in docs.json but has no MDX file`,
@@ -922,7 +682,7 @@ async function handleLintProject(input) {
922
682
  }
923
683
  }
924
684
  const allFiles = [];
925
- if (existsSync7(contentDir)) {
685
+ if (existsSync6(contentDir)) {
926
686
  scanMdxFiles(contentDir, allFiles);
927
687
  }
928
688
  const fixedOrphans = [];
@@ -940,7 +700,7 @@ async function handleLintProject(input) {
940
700
  let data = {};
941
701
  let content = "";
942
702
  try {
943
- const raw = readFileSync7(filePath, "utf8");
703
+ const raw = readFileSync6(filePath, "utf8");
944
704
  const parsed = parseFrontmatter(raw);
945
705
  data = parsed.data;
946
706
  content = parsed.content;
@@ -997,8 +757,8 @@ async function handleLintProject(input) {
997
757
 
998
758
  // src/tools/translate-docs.ts
999
759
  import { z as z13 } from "zod";
1000
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
1001
- import { join as join9, dirname as dirname2 } from "path";
760
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
761
+ import { join as join8, dirname as dirname2 } from "path";
1002
762
  import Anthropic from "@anthropic-ai/sdk";
1003
763
  import pLimit from "p-limit";
1004
764
  var translateDocsSchema = z13.object({
@@ -1010,8 +770,8 @@ var translateDocsSchema = z13.object({
1010
770
  model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
1011
771
  });
1012
772
  function readDocsJson2(projectDir) {
1013
- const docsPath = join9(projectDir, "docs.json");
1014
- const raw = readFileSync8(docsPath, "utf8");
773
+ const docsPath = join8(projectDir, "docs.json");
774
+ const raw = readFileSync7(docsPath, "utf8");
1015
775
  return JSON.parse(raw);
1016
776
  }
1017
777
  function collectPageIds(pages) {
@@ -1053,12 +813,12 @@ function getAllPageIds(config) {
1053
813
  return { ids, hrefOnlyPages };
1054
814
  }
1055
815
  function findSourceFile(projectDir, pageId) {
1056
- const contentRoot = join9(projectDir, "src", "content");
816
+ const contentRoot = join8(projectDir, "src", "content");
1057
817
  const candidates = [
1058
- join9(contentRoot, `${pageId}.mdx`),
1059
- join9(contentRoot, `${pageId}/index.mdx`)
818
+ join8(contentRoot, `${pageId}.mdx`),
819
+ join8(contentRoot, `${pageId}/index.mdx`)
1060
820
  ];
1061
- return candidates.find((p) => existsSync8(p)) ?? null;
821
+ return candidates.find((p) => existsSync7(p)) ?? null;
1062
822
  }
1063
823
  var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
1064
824
 
@@ -1110,7 +870,7 @@ async function handleTranslateDocs(input) {
1110
870
  }
1111
871
  const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
1112
872
  const targetPageIds = pages ?? allPageIds;
1113
- const contentRoot = join9(projectDir, "src", "content");
873
+ const contentRoot = join8(projectDir, "src", "content");
1114
874
  const toTranslate = [];
1115
875
  const skipped = [];
1116
876
  for (const pageId of targetPageIds) {
@@ -1120,8 +880,8 @@ async function handleTranslateDocs(input) {
1120
880
  continue;
1121
881
  }
1122
882
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
1123
- const targetFile = join9(contentRoot, locale, relativeFromContent);
1124
- if (existsSync8(targetFile) && !force) {
883
+ const targetFile = join8(contentRoot, locale, relativeFromContent);
884
+ if (existsSync7(targetFile) && !force) {
1125
885
  skipped.push(`${pageId} (already translated)`);
1126
886
  continue;
1127
887
  }
@@ -1137,14 +897,14 @@ async function handleTranslateDocs(input) {
1137
897
  toTranslate.map(
1138
898
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1139
899
  try {
1140
- const sourceContent = readFileSync8(sourceFile, "utf8");
900
+ const sourceContent = readFileSync7(sourceFile, "utf8");
1141
901
  const parsed = parseFrontmatter(sourceContent);
1142
902
  if (!parsed.data.title) {
1143
903
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1144
904
  }
1145
905
  const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
1146
- mkdirSync3(dirname2(targetFile), { recursive: true });
1147
- writeFileSync6(targetFile, translated + "\n", "utf8");
906
+ mkdirSync2(dirname2(targetFile), { recursive: true });
907
+ writeFileSync5(targetFile, translated + "\n", "utf8");
1148
908
  results.push({ pageId, success: true });
1149
909
  } catch (err) {
1150
910
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thallylabs/mcp",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "MCP server for managing Thally knowledge surfaces and tracing product changes into documentation work.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,6 +19,10 @@
19
19
  "./track": {
20
20
  "types": "./dist/track.d.ts",
21
21
  "import": "./dist/track.js"
22
+ },
23
+ "./create-project": {
24
+ "types": "./dist/create-project.d.ts",
25
+ "import": "./dist/create-project.js"
22
26
  }
23
27
  },
24
28
  "files": [
@@ -34,15 +38,13 @@
34
38
  "dependencies": {
35
39
  "@anthropic-ai/sdk": "^0.36.0",
36
40
  "@modelcontextprotocol/sdk": "^1.15.0",
37
- "create-thally-docs": "0.8.0",
41
+ "create-thally-docs": "0.9.0",
38
42
  "gray-matter": "^4.0.3",
39
43
  "p-limit": "^6.1.0",
40
- "tar": "^6.2.0",
41
44
  "zod": "^3.0.0"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@types/node": "^22.0.0",
45
- "@types/tar": "^6.1.13",
46
48
  "tsup": "^8.0.0",
47
49
  "typescript": "^5.0.0"
48
50
  },