@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/create-project.d.ts +49 -0
- package/dist/create-project.js +64 -0
- package/dist/index.js +73 -316
- package/dist/tools.js +73 -316
- package/package.json +6 -4
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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var
|
|
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
|
-
|
|
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('
|
|
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
|
|
308
|
-
import { join as
|
|
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
|
|
312
|
-
import { join
|
|
68
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
69
|
+
import { join } from "path";
|
|
313
70
|
function readDocsJson(projectDir) {
|
|
314
|
-
const docsPath =
|
|
315
|
-
const raw =
|
|
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 =
|
|
320
|
-
|
|
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 =
|
|
373
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
504
|
-
import { join as
|
|
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
|
-
|
|
533
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
617
|
-
import { join as
|
|
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 =
|
|
383
|
+
entries = readdirSync(dir);
|
|
627
384
|
} catch {
|
|
628
385
|
return;
|
|
629
386
|
}
|
|
630
387
|
for (const entry of entries) {
|
|
631
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
675
|
-
if (!
|
|
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
|
|
771
|
-
import { join as
|
|
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 =
|
|
535
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
779
536
|
const candidates = [
|
|
780
|
-
|
|
781
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
806
|
-
import { join as
|
|
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 =
|
|
815
|
-
if (!
|
|
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
|
-
|
|
830
|
-
|
|
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 (
|
|
835
|
-
const raw =
|
|
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
|
|
861
|
-
import { join as
|
|
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 =
|
|
649
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
893
650
|
const issues = [];
|
|
894
|
-
if (!
|
|
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
|
-
|
|
914
|
-
|
|
670
|
+
join7(contentDir, `${pageId}.mdx`),
|
|
671
|
+
join7(contentDir, `${pageId}/index.mdx`)
|
|
915
672
|
];
|
|
916
|
-
if (!candidates.some((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 (
|
|
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 =
|
|
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
|
|
1001
|
-
import { join as
|
|
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 =
|
|
1014
|
-
const raw =
|
|
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 =
|
|
813
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1057
814
|
const candidates = [
|
|
1058
|
-
|
|
1059
|
-
|
|
815
|
+
join8(contentRoot, `${pageId}.mdx`),
|
|
816
|
+
join8(contentRoot, `${pageId}/index.mdx`)
|
|
1060
817
|
];
|
|
1061
|
-
return candidates.find((p) =>
|
|
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 =
|
|
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 =
|
|
1124
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
1147
|
-
|
|
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.
|
|
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.
|
|
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
|
},
|