@thallylabs/mcp 0.8.1 → 0.10.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,15 @@
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
+ STABLE_SCAFFOLD_RELEASE,
7
+ STARTER_REPOSITORY,
8
+ scaffold as scaffoldProject
9
+ } from "create-thally-docs/scaffold";
10
+ var MCP_STARTER_COMMIT_SHA = STABLE_SCAFFOLD_RELEASE.source.commitSha;
11
+ var MCP_SCAFFOLD_RELEASE_ID = STABLE_SCAFFOLD_RELEASE.id;
239
12
  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 };
13
+ return scaffoldProject(options);
257
14
  }
258
15
 
259
16
  // src/tools/create-project.ts
@@ -265,7 +22,7 @@ var createProjectSchema = z.object({
265
22
  repoUrl: z.string().optional().describe("GitHub repository URL (optional)"),
266
23
  install: z.boolean().optional().default(true).describe("Whether to run npm install after scaffolding"),
267
24
  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"}])')
25
+ i18nLocales: z.array(z.object({ code: z.string(), label: z.string() })).optional().describe('Additional documentation locales to include alongside English (e.g. [{code:"fr",label:"Fran\xE7ais"}])')
269
26
  });
270
27
  async function handleCreateProject(input) {
271
28
  const { projectDir, brandPreset = "primary", install = true } = input;
@@ -304,20 +61,20 @@ async function handleCreateProject(input) {
304
61
 
305
62
  // src/tools/add-page.ts
306
63
  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";
64
+ import { existsSync, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
65
+ import { join as join2, dirname } from "path";
309
66
 
310
67
  // src/lib/docs-json.ts
311
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
312
- import { join as join2 } from "path";
68
+ import { readFileSync, writeFileSync } from "fs";
69
+ import { join } from "path";
313
70
  function readDocsJson(projectDir) {
314
- const docsPath = join2(projectDir, "docs.json");
315
- const raw = readFileSync2(docsPath, "utf8");
71
+ const docsPath = join(projectDir, "docs.json");
72
+ const raw = readFileSync(docsPath, "utf8");
316
73
  return JSON.parse(raw);
317
74
  }
318
75
  function writeDocsJson(projectDir, config) {
319
- const docsPath = join2(projectDir, "docs.json");
320
- writeFileSync2(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
76
+ const docsPath = join(projectDir, "docs.json");
77
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
321
78
  }
322
79
 
323
80
  // src/lib/page-echo.ts
@@ -369,11 +126,11 @@ async function handleAddPage(input) {
369
126
  `Invalid pageId "${pageId}". Use only alphanumeric characters, hyphens, and slashes. Do not include .mdx extension.`
370
127
  );
371
128
  }
372
- const mdxPath = join3(projectDir, "src", "content", `${pageId}.mdx`);
373
- if (existsSync2(mdxPath)) {
129
+ const mdxPath = join2(projectDir, "src", "content", `${pageId}.mdx`);
130
+ if (existsSync(mdxPath)) {
374
131
  throw new Error(`Page already exists at: ${mdxPath}`);
375
132
  }
376
- mkdirSync2(dirname(mdxPath), { recursive: true });
133
+ mkdirSync(dirname(mdxPath), { recursive: true });
377
134
  const frontmatterLines = [`title: ${title}`];
378
135
  if (description) {
379
136
  frontmatterLines.push(`description: ${description}`);
@@ -387,7 +144,7 @@ ${frontmatterLines.join("\n")}
387
144
 
388
145
  ${bodyContent}
389
146
  `;
390
- writeFileSync3(mdxPath, mdxContent, "utf8");
147
+ writeFileSync2(mdxPath, mdxContent, "utf8");
391
148
  const config = readDocsJson(projectDir);
392
149
  let targetTab = config.tabs[0];
393
150
  if (input.tab) {
@@ -500,8 +257,8 @@ async function handleListPages(input) {
500
257
 
501
258
  // src/tools/update-page.ts
502
259
  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";
260
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
261
+ import { join as join3 } from "path";
505
262
 
506
263
  // src/lib/frontmatter.ts
507
264
  import matter from "gray-matter";
@@ -529,11 +286,11 @@ var updatePageSchema = z5.object({
529
286
  });
530
287
  function findPageFile(projectDir, pageId) {
531
288
  const candidates = [
532
- join4(projectDir, "src", "content", `${pageId}.mdx`),
533
- join4(projectDir, "src", "content", `${pageId}/index.mdx`)
289
+ join3(projectDir, "src", "content", `${pageId}.mdx`),
290
+ join3(projectDir, "src", "content", `${pageId}/index.mdx`)
534
291
  ];
535
292
  for (const candidate of candidates) {
536
- if (existsSync3(candidate)) return candidate;
293
+ if (existsSync2(candidate)) return candidate;
537
294
  }
538
295
  return null;
539
296
  }
@@ -547,7 +304,7 @@ async function handleUpdatePage(input) {
547
304
  src/content/${pageId}/index.mdx`
548
305
  );
549
306
  }
550
- const raw = readFileSync3(filePath, "utf8");
307
+ const raw = readFileSync2(filePath, "utf8");
551
308
  const parsed = parseFrontmatter(raw);
552
309
  const newFm = { ...parsed.data };
553
310
  if (input.title !== void 0) newFm["title"] = input.title;
@@ -557,7 +314,7 @@ async function handleUpdatePage(input) {
557
314
  }
558
315
  const newBody = input.content !== void 0 ? stripEchoedPageHeader(input.content, pageId) : parsed.content;
559
316
  const newContent = stringifyFrontmatter(newBody.trim(), newFm);
560
- writeFileSync4(filePath, newContent, "utf8");
317
+ writeFileSync3(filePath, newContent, "utf8");
561
318
  return [
562
319
  `\u2705 Page updated: ${filePath}`,
563
320
  ` pageId: ${pageId}`,
@@ -613,8 +370,8 @@ async function handleImportDocs(input) {
613
370
 
614
371
  // src/tools/search-docs.ts
615
372
  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";
373
+ import { readdirSync, statSync, readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
374
+ import { join as join4, relative, extname } from "path";
618
375
  var searchDocsSchema = z7.object({
619
376
  projectDir: z7.string().describe("Path to the Thally project root"),
620
377
  query: z7.string().describe("Search query"),
@@ -623,12 +380,12 @@ var searchDocsSchema = z7.object({
623
380
  function scanMdxFiles(dir, results) {
624
381
  let entries;
625
382
  try {
626
- entries = readdirSync2(dir);
383
+ entries = readdirSync(dir);
627
384
  } catch {
628
385
  return;
629
386
  }
630
387
  for (const entry of entries) {
631
- const fullPath = join5(dir, entry);
388
+ const fullPath = join4(dir, entry);
632
389
  try {
633
390
  const stat = statSync(fullPath);
634
391
  if (stat.isDirectory()) {
@@ -646,7 +403,7 @@ function scoreFiles(files, contentDir, query) {
646
403
  for (const filePath of files) {
647
404
  let raw;
648
405
  try {
649
- raw = readFileSync4(filePath, "utf8");
406
+ raw = readFileSync3(filePath, "utf8");
650
407
  } catch {
651
408
  continue;
652
409
  }
@@ -671,8 +428,8 @@ function scoreFiles(files, contentDir, query) {
671
428
  }
672
429
  async function handleSearchDocs(input) {
673
430
  const { projectDir, query, limit = 5 } = input;
674
- const contentDir = join5(projectDir, "src", "content");
675
- if (!existsSync4(contentDir)) {
431
+ const contentDir = join4(projectDir, "src", "content");
432
+ if (!existsSync3(contentDir)) {
676
433
  throw new Error(`Content directory not found: ${contentDir}`);
677
434
  }
678
435
  const files = [];
@@ -767,22 +524,22 @@ async function handleAgentReadiness(input) {
767
524
 
768
525
  // src/tools/read-page.ts
769
526
  import { z as z10 } from "zod";
770
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
771
- import { join as join6 } from "path";
527
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
528
+ import { join as join5 } from "path";
772
529
  var readPageSchema = z10.object({
773
530
  projectDir: z10.string().describe("Path to the Thally project root"),
774
531
  pageId: z10.string().describe('Page ID, e.g. "guides/authentication"')
775
532
  });
776
533
  async function handleReadPage(input) {
777
534
  const { projectDir, pageId } = input;
778
- const contentDir = join6(projectDir, "src", "content");
535
+ const contentDir = join5(projectDir, "src", "content");
779
536
  const candidates = [
780
- join6(contentDir, `${pageId}.mdx`),
781
- join6(contentDir, `${pageId}/index.mdx`)
537
+ join5(contentDir, `${pageId}.mdx`),
538
+ join5(contentDir, `${pageId}/index.mdx`)
782
539
  ];
783
540
  let filePath = null;
784
541
  for (const c of candidates) {
785
- if (existsSync5(c)) {
542
+ if (existsSync4(c)) {
786
543
  filePath = c;
787
544
  break;
788
545
  }
@@ -790,7 +547,7 @@ async function handleReadPage(input) {
790
547
  if (!filePath) {
791
548
  throw new Error(`Page not found: "${pageId}". No file at src/content/${pageId}.mdx`);
792
549
  }
793
- const raw = readFileSync5(filePath, "utf8");
550
+ const raw = readFileSync4(filePath, "utf8");
794
551
  const { data, content } = parseFrontmatter(raw);
795
552
  const title = data.title ?? pageId;
796
553
  const description = data.description ?? "";
@@ -802,8 +559,8 @@ async function handleReadPage(input) {
802
559
 
803
560
  // src/tools/get-context.ts
804
561
  import { z as z11 } from "zod";
805
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
806
- import { join as join7 } from "path";
562
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
563
+ import { join as join6 } from "path";
807
564
  var getContextSchema = z11.object({
808
565
  projectDir: z11.string().describe("Path to the Thally project root"),
809
566
  topic: z11.string().describe("Topic or question to find relevant docs for"),
@@ -811,8 +568,8 @@ var getContextSchema = z11.object({
811
568
  });
812
569
  async function handleGetContext(input) {
813
570
  const { projectDir, topic, maxTokens = 4e3 } = input;
814
- const contentDir = join7(projectDir, "src", "content");
815
- if (!existsSync6(contentDir)) {
571
+ const contentDir = join6(projectDir, "src", "content");
572
+ if (!existsSync5(contentDir)) {
816
573
  throw new Error(`Content directory not found: ${contentDir}`);
817
574
  }
818
575
  const files = [];
@@ -826,13 +583,13 @@ async function handleGetContext(input) {
826
583
  const sections = [];
827
584
  for (const result of scored) {
828
585
  const candidates = [
829
- join7(contentDir, `${result.pageId}.mdx`),
830
- join7(contentDir, `${result.pageId}/index.mdx`)
586
+ join6(contentDir, `${result.pageId}.mdx`),
587
+ join6(contentDir, `${result.pageId}/index.mdx`)
831
588
  ];
832
589
  let content = "";
833
590
  for (const c of candidates) {
834
- if (existsSync6(c)) {
835
- const raw = readFileSync6(c, "utf8");
591
+ if (existsSync5(c)) {
592
+ const raw = readFileSync5(c, "utf8");
836
593
  const { content: body } = parseFrontmatter(raw);
837
594
  content = body.trim();
838
595
  break;
@@ -857,8 +614,8 @@ async function handleGetContext(input) {
857
614
 
858
615
  // src/tools/lint-project.ts
859
616
  import { z as z12 } from "zod";
860
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
861
- import { join as join8 } from "path";
617
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
618
+ import { join as join7 } from "path";
862
619
  var lintProjectSchema = z12.object({
863
620
  projectDir: z12.string().describe("Path to the Thally project root"),
864
621
  fix: z12.boolean().optional().default(false).describe("Auto-fix issues where possible (adds orphan pages to nav)")
@@ -889,9 +646,9 @@ function addOrphanToNav(projectDir, pageId) {
889
646
  }
890
647
  async function handleLintProject(input) {
891
648
  const { projectDir, fix = false } = input;
892
- const contentDir = join8(projectDir, "src", "content");
649
+ const contentDir = join7(projectDir, "src", "content");
893
650
  const issues = [];
894
- if (!existsSync7(join8(projectDir, "docs.json"))) {
651
+ if (!existsSync6(join7(projectDir, "docs.json"))) {
895
652
  throw new Error(`Not a Thally project: docs.json not found in ${projectDir}`);
896
653
  }
897
654
  const config = readDocsJson(projectDir);
@@ -910,10 +667,10 @@ async function handleLintProject(input) {
910
667
  }
911
668
  for (const pageId of navPageIds) {
912
669
  const candidates = [
913
- join8(contentDir, `${pageId}.mdx`),
914
- join8(contentDir, `${pageId}/index.mdx`)
670
+ join7(contentDir, `${pageId}.mdx`),
671
+ join7(contentDir, `${pageId}/index.mdx`)
915
672
  ];
916
- if (!candidates.some((c) => existsSync7(c))) {
673
+ if (!candidates.some((c) => existsSync6(c))) {
917
674
  issues.push({
918
675
  severity: "error",
919
676
  message: `"${pageId}" is in docs.json but has no MDX file`,
@@ -922,7 +679,7 @@ async function handleLintProject(input) {
922
679
  }
923
680
  }
924
681
  const allFiles = [];
925
- if (existsSync7(contentDir)) {
682
+ if (existsSync6(contentDir)) {
926
683
  scanMdxFiles(contentDir, allFiles);
927
684
  }
928
685
  const fixedOrphans = [];
@@ -940,7 +697,7 @@ async function handleLintProject(input) {
940
697
  let data = {};
941
698
  let content = "";
942
699
  try {
943
- const raw = readFileSync7(filePath, "utf8");
700
+ const raw = readFileSync6(filePath, "utf8");
944
701
  const parsed = parseFrontmatter(raw);
945
702
  data = parsed.data;
946
703
  content = parsed.content;
@@ -997,8 +754,8 @@ async function handleLintProject(input) {
997
754
 
998
755
  // src/tools/translate-docs.ts
999
756
  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";
757
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
758
+ import { join as join8, dirname as dirname2 } from "path";
1002
759
  import Anthropic from "@anthropic-ai/sdk";
1003
760
  import pLimit from "p-limit";
1004
761
  var translateDocsSchema = z13.object({
@@ -1010,8 +767,8 @@ var translateDocsSchema = z13.object({
1010
767
  model: z13.string().optional().default("claude-sonnet-4-6").describe("Claude model to use for translation")
1011
768
  });
1012
769
  function readDocsJson2(projectDir) {
1013
- const docsPath = join9(projectDir, "docs.json");
1014
- const raw = readFileSync8(docsPath, "utf8");
770
+ const docsPath = join8(projectDir, "docs.json");
771
+ const raw = readFileSync7(docsPath, "utf8");
1015
772
  return JSON.parse(raw);
1016
773
  }
1017
774
  function collectPageIds(pages) {
@@ -1053,12 +810,12 @@ function getAllPageIds(config) {
1053
810
  return { ids, hrefOnlyPages };
1054
811
  }
1055
812
  function findSourceFile(projectDir, pageId) {
1056
- const contentRoot = join9(projectDir, "src", "content");
813
+ const contentRoot = join8(projectDir, "src", "content");
1057
814
  const candidates = [
1058
- join9(contentRoot, `${pageId}.mdx`),
1059
- join9(contentRoot, `${pageId}/index.mdx`)
815
+ join8(contentRoot, `${pageId}.mdx`),
816
+ join8(contentRoot, `${pageId}/index.mdx`)
1060
817
  ];
1061
- return candidates.find((p) => existsSync8(p)) ?? null;
818
+ return candidates.find((p) => existsSync7(p)) ?? null;
1062
819
  }
1063
820
  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
821
 
@@ -1110,7 +867,7 @@ async function handleTranslateDocs(input) {
1110
867
  }
1111
868
  const { ids: allPageIds, hrefOnlyPages } = getAllPageIds(config);
1112
869
  const targetPageIds = pages ?? allPageIds;
1113
- const contentRoot = join9(projectDir, "src", "content");
870
+ const contentRoot = join8(projectDir, "src", "content");
1114
871
  const toTranslate = [];
1115
872
  const skipped = [];
1116
873
  for (const pageId of targetPageIds) {
@@ -1120,8 +877,8 @@ async function handleTranslateDocs(input) {
1120
877
  continue;
1121
878
  }
1122
879
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
1123
- const targetFile = join9(contentRoot, locale, relativeFromContent);
1124
- if (existsSync8(targetFile) && !force) {
880
+ const targetFile = join8(contentRoot, locale, relativeFromContent);
881
+ if (existsSync7(targetFile) && !force) {
1125
882
  skipped.push(`${pageId} (already translated)`);
1126
883
  continue;
1127
884
  }
@@ -1137,14 +894,14 @@ async function handleTranslateDocs(input) {
1137
894
  toTranslate.map(
1138
895
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
1139
896
  try {
1140
- const sourceContent = readFileSync8(sourceFile, "utf8");
897
+ const sourceContent = readFileSync7(sourceFile, "utf8");
1141
898
  const parsed = parseFrontmatter(sourceContent);
1142
899
  if (!parsed.data.title) {
1143
900
  console.warn(`[translate] ${pageId}: missing title in frontmatter`);
1144
901
  }
1145
902
  const translated = await translatePage(sourceContent, targetLocale.label, locale, model, client);
1146
- mkdirSync3(dirname2(targetFile), { recursive: true });
1147
- writeFileSync6(targetFile, translated + "\n", "utf8");
903
+ mkdirSync2(dirname2(targetFile), { recursive: true });
904
+ writeFileSync5(targetFile, translated + "\n", "utf8");
1148
905
  results.push({ pageId, success: true });
1149
906
  } catch (err) {
1150
907
  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.10.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.10.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
  },