@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/create-project.d.ts +49 -0
- package/dist/create-project.js +67 -0
- package/dist/index.js +76 -316
- package/dist/tools.js +76 -316
- package/package.json +6 -4
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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var
|
|
14
|
-
var
|
|
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
|
-
|
|
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('
|
|
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
|
|
308
|
-
import { join as
|
|
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
|
|
312
|
-
import { join
|
|
71
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
72
|
+
import { join } from "path";
|
|
313
73
|
function readDocsJson(projectDir) {
|
|
314
|
-
const docsPath =
|
|
315
|
-
const raw =
|
|
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 =
|
|
320
|
-
|
|
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 =
|
|
373
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
504
|
-
import { join as
|
|
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
|
-
|
|
533
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
617
|
-
import { join as
|
|
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 =
|
|
386
|
+
entries = readdirSync(dir);
|
|
627
387
|
} catch {
|
|
628
388
|
return;
|
|
629
389
|
}
|
|
630
390
|
for (const entry of entries) {
|
|
631
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
675
|
-
if (!
|
|
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
|
|
771
|
-
import { join as
|
|
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 =
|
|
538
|
+
const contentDir = join5(projectDir, "src", "content");
|
|
779
539
|
const candidates = [
|
|
780
|
-
|
|
781
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
806
|
-
import { join as
|
|
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 =
|
|
815
|
-
if (!
|
|
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
|
-
|
|
830
|
-
|
|
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 (
|
|
835
|
-
const raw =
|
|
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
|
|
861
|
-
import { join as
|
|
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 =
|
|
652
|
+
const contentDir = join7(projectDir, "src", "content");
|
|
893
653
|
const issues = [];
|
|
894
|
-
if (!
|
|
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
|
-
|
|
914
|
-
|
|
673
|
+
join7(contentDir, `${pageId}.mdx`),
|
|
674
|
+
join7(contentDir, `${pageId}/index.mdx`)
|
|
915
675
|
];
|
|
916
|
-
if (!candidates.some((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 (
|
|
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 =
|
|
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
|
|
1001
|
-
import { join as
|
|
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 =
|
|
1014
|
-
const raw =
|
|
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 =
|
|
816
|
+
const contentRoot = join8(projectDir, "src", "content");
|
|
1057
817
|
const candidates = [
|
|
1058
|
-
|
|
1059
|
-
|
|
818
|
+
join8(contentRoot, `${pageId}.mdx`),
|
|
819
|
+
join8(contentRoot, `${pageId}/index.mdx`)
|
|
1060
820
|
];
|
|
1061
|
-
return candidates.find((p) =>
|
|
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 =
|
|
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 =
|
|
1124
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
1147
|
-
|
|
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.
|
|
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.
|
|
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
|
},
|