@thallylabs/mcp 0.10.8 → 0.10.10
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/index.js +111 -0
- package/dist/tools.js +111 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1217,6 +1217,103 @@ async function handleSyncFromRepo(input) {
|
|
|
1217
1217
|
return `\u2705 Dispatched docs task for ${target.owner}/${target.repo}#${pr.number} to ${input.docsRepo}. The "Thally docs agent" workflow there will draft the PR \u2014 watch its Actions tab.`;
|
|
1218
1218
|
}
|
|
1219
1219
|
|
|
1220
|
+
// src/tools/read-api-spec.ts
|
|
1221
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1222
|
+
import { z as z15 } from "zod";
|
|
1223
|
+
|
|
1224
|
+
// src/lib/api-spec.ts
|
|
1225
|
+
import { existsSync as existsSync8, lstatSync, realpathSync } from "fs";
|
|
1226
|
+
import { isAbsolute, join as join9, posix, relative as relative2, resolve, sep } from "path";
|
|
1227
|
+
var CONVENTIONAL_API_SOURCES = ["openapi.yaml", "openapi.yml", "openapi.json"];
|
|
1228
|
+
function normalizeApiSource(source) {
|
|
1229
|
+
const normalized = posix.normalize(source.trim().replace(/^\/+/, ""));
|
|
1230
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,239}$/.test(normalized) || !/\.(?:json|ya?ml)$/i.test(normalized)) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
return normalized;
|
|
1234
|
+
}
|
|
1235
|
+
function configuredApiSources(projectDir) {
|
|
1236
|
+
const configured = readDocsJson(projectDir).tabs.map((tab) => tab.api?.source).filter((source) => typeof source === "string").map(normalizeApiSource).filter((source) => source !== null);
|
|
1237
|
+
const conventional = CONVENTIONAL_API_SOURCES.filter(
|
|
1238
|
+
(source) => existsSync8(join9(projectDir, source))
|
|
1239
|
+
);
|
|
1240
|
+
return [.../* @__PURE__ */ new Set([...configured, ...conventional])];
|
|
1241
|
+
}
|
|
1242
|
+
function resolveApiSource(projectDir, requested) {
|
|
1243
|
+
const sources = configuredApiSources(projectDir);
|
|
1244
|
+
const normalizedRequested = requested ? normalizeApiSource(requested) : null;
|
|
1245
|
+
if (requested && !normalizedRequested) {
|
|
1246
|
+
throw new Error("API source must be a repository-relative JSON or YAML file.");
|
|
1247
|
+
}
|
|
1248
|
+
if (normalizedRequested && sources.includes(normalizedRequested)) {
|
|
1249
|
+
return normalizedRequested;
|
|
1250
|
+
}
|
|
1251
|
+
if (!requested && sources.length === 1) return sources[0];
|
|
1252
|
+
if (normalizedRequested) {
|
|
1253
|
+
throw new Error(`API source is not configured in docs.json: ${normalizedRequested}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (sources.length === 0) {
|
|
1256
|
+
throw new Error("No OpenAPI source is configured in docs.json.");
|
|
1257
|
+
}
|
|
1258
|
+
throw new Error(`Multiple OpenAPI sources are configured; choose one of: ${sources.join(", ")}`);
|
|
1259
|
+
}
|
|
1260
|
+
function resolveApiSourcePath(projectDir, requested) {
|
|
1261
|
+
const source = resolveApiSource(projectDir, requested);
|
|
1262
|
+
const projectRoot = realpathSync(projectDir);
|
|
1263
|
+
const candidate = resolve(projectRoot, source);
|
|
1264
|
+
try {
|
|
1265
|
+
const resolved = realpathSync(candidate);
|
|
1266
|
+
const fromRoot = relative2(projectRoot, resolved);
|
|
1267
|
+
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot) || !lstatSync(candidate).isFile() || lstatSync(candidate).isSymbolicLink()) {
|
|
1268
|
+
throw new Error("unsafe");
|
|
1269
|
+
}
|
|
1270
|
+
return { source, path: resolved };
|
|
1271
|
+
} catch {
|
|
1272
|
+
throw new Error(`API source must be an existing regular file inside the project: ${source}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/tools/read-api-spec.ts
|
|
1277
|
+
var readApiSpecSchema = z15.object({
|
|
1278
|
+
projectDir: z15.string().describe("Path to the Thally project root"),
|
|
1279
|
+
source: z15.string().optional().describe("Configured OpenAPI source; omit when the project has one")
|
|
1280
|
+
});
|
|
1281
|
+
async function handleReadApiSpec(input) {
|
|
1282
|
+
const source = resolveApiSourcePath(input.projectDir, input.source);
|
|
1283
|
+
return [`API source: ${source.source}`, "", readFileSync8(source.path, "utf8")].join("\n");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/tools/update-api-spec.ts
|
|
1287
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
1288
|
+
import { extname as extname2 } from "path";
|
|
1289
|
+
import { z as z16 } from "zod";
|
|
1290
|
+
import { parse as parseYaml } from "yaml";
|
|
1291
|
+
var updateApiSpecSchema = z16.object({
|
|
1292
|
+
projectDir: z16.string().describe("Path to the Thally project root"),
|
|
1293
|
+
source: z16.string().optional().describe("Configured OpenAPI source; omit when the project has one"),
|
|
1294
|
+
content: z16.string().min(2).max(512e3).describe("Complete replacement OpenAPI JSON or YAML document")
|
|
1295
|
+
});
|
|
1296
|
+
function parseDocument(source, content) {
|
|
1297
|
+
try {
|
|
1298
|
+
return extname2(source).toLowerCase() === ".json" ? JSON.parse(content) : parseYaml(content);
|
|
1299
|
+
} catch {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`OpenAPI source is not valid ${extname2(source).toLowerCase() === ".json" ? "JSON" : "YAML"}.`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function handleUpdateApiSpec(input) {
|
|
1306
|
+
const source = resolveApiSourcePath(input.projectDir, input.source);
|
|
1307
|
+
const parsed = parseDocument(source.source, input.content);
|
|
1308
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("openapi" in parsed) || !("paths" in parsed)) {
|
|
1309
|
+
throw new Error("OpenAPI source must contain top-level openapi and paths fields.");
|
|
1310
|
+
}
|
|
1311
|
+
const content = input.content.endsWith("\n") ? input.content : `${input.content}
|
|
1312
|
+
`;
|
|
1313
|
+
writeFileSync6(source.path, content, "utf8");
|
|
1314
|
+
return `\u2705 API source updated: ${source.source}`;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1220
1317
|
// src/lib/tools.ts
|
|
1221
1318
|
function defineTool(def) {
|
|
1222
1319
|
return def;
|
|
@@ -1257,6 +1354,20 @@ var tools = [
|
|
|
1257
1354
|
schema: updatePageSchema,
|
|
1258
1355
|
handler: handleUpdatePage
|
|
1259
1356
|
}),
|
|
1357
|
+
defineTool({
|
|
1358
|
+
name: "read_api_spec",
|
|
1359
|
+
description: "Read an OpenAPI JSON or YAML source explicitly configured by this Thally project",
|
|
1360
|
+
scope: "project",
|
|
1361
|
+
schema: readApiSpecSchema,
|
|
1362
|
+
handler: handleReadApiSpec
|
|
1363
|
+
}),
|
|
1364
|
+
defineTool({
|
|
1365
|
+
name: "update_api_spec",
|
|
1366
|
+
description: "Replace and validate an OpenAPI JSON or YAML source explicitly configured by this Thally project",
|
|
1367
|
+
scope: "project",
|
|
1368
|
+
schema: updateApiSpecSchema,
|
|
1369
|
+
handler: handleUpdateApiSpec
|
|
1370
|
+
}),
|
|
1260
1371
|
defineTool({
|
|
1261
1372
|
name: "migrate_docs",
|
|
1262
1373
|
description: "Create a fresh canonical Thally template, then migrate a GitHub repository or public docs site into it; the target must be new or empty",
|
package/dist/tools.js
CHANGED
|
@@ -1208,6 +1208,103 @@ async function handleSyncFromRepo(input) {
|
|
|
1208
1208
|
return `\u2705 Dispatched docs task for ${target.owner}/${target.repo}#${pr.number} to ${input.docsRepo}. The "Thally docs agent" workflow there will draft the PR \u2014 watch its Actions tab.`;
|
|
1209
1209
|
}
|
|
1210
1210
|
|
|
1211
|
+
// src/tools/read-api-spec.ts
|
|
1212
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1213
|
+
import { z as z15 } from "zod";
|
|
1214
|
+
|
|
1215
|
+
// src/lib/api-spec.ts
|
|
1216
|
+
import { existsSync as existsSync8, lstatSync, realpathSync } from "fs";
|
|
1217
|
+
import { isAbsolute, join as join9, posix, relative as relative2, resolve, sep } from "path";
|
|
1218
|
+
var CONVENTIONAL_API_SOURCES = ["openapi.yaml", "openapi.yml", "openapi.json"];
|
|
1219
|
+
function normalizeApiSource(source) {
|
|
1220
|
+
const normalized = posix.normalize(source.trim().replace(/^\/+/, ""));
|
|
1221
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,239}$/.test(normalized) || !/\.(?:json|ya?ml)$/i.test(normalized)) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
return normalized;
|
|
1225
|
+
}
|
|
1226
|
+
function configuredApiSources(projectDir) {
|
|
1227
|
+
const configured = readDocsJson(projectDir).tabs.map((tab) => tab.api?.source).filter((source) => typeof source === "string").map(normalizeApiSource).filter((source) => source !== null);
|
|
1228
|
+
const conventional = CONVENTIONAL_API_SOURCES.filter(
|
|
1229
|
+
(source) => existsSync8(join9(projectDir, source))
|
|
1230
|
+
);
|
|
1231
|
+
return [.../* @__PURE__ */ new Set([...configured, ...conventional])];
|
|
1232
|
+
}
|
|
1233
|
+
function resolveApiSource(projectDir, requested) {
|
|
1234
|
+
const sources = configuredApiSources(projectDir);
|
|
1235
|
+
const normalizedRequested = requested ? normalizeApiSource(requested) : null;
|
|
1236
|
+
if (requested && !normalizedRequested) {
|
|
1237
|
+
throw new Error("API source must be a repository-relative JSON or YAML file.");
|
|
1238
|
+
}
|
|
1239
|
+
if (normalizedRequested && sources.includes(normalizedRequested)) {
|
|
1240
|
+
return normalizedRequested;
|
|
1241
|
+
}
|
|
1242
|
+
if (!requested && sources.length === 1) return sources[0];
|
|
1243
|
+
if (normalizedRequested) {
|
|
1244
|
+
throw new Error(`API source is not configured in docs.json: ${normalizedRequested}`);
|
|
1245
|
+
}
|
|
1246
|
+
if (sources.length === 0) {
|
|
1247
|
+
throw new Error("No OpenAPI source is configured in docs.json.");
|
|
1248
|
+
}
|
|
1249
|
+
throw new Error(`Multiple OpenAPI sources are configured; choose one of: ${sources.join(", ")}`);
|
|
1250
|
+
}
|
|
1251
|
+
function resolveApiSourcePath(projectDir, requested) {
|
|
1252
|
+
const source = resolveApiSource(projectDir, requested);
|
|
1253
|
+
const projectRoot = realpathSync(projectDir);
|
|
1254
|
+
const candidate = resolve(projectRoot, source);
|
|
1255
|
+
try {
|
|
1256
|
+
const resolved = realpathSync(candidate);
|
|
1257
|
+
const fromRoot = relative2(projectRoot, resolved);
|
|
1258
|
+
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot) || !lstatSync(candidate).isFile() || lstatSync(candidate).isSymbolicLink()) {
|
|
1259
|
+
throw new Error("unsafe");
|
|
1260
|
+
}
|
|
1261
|
+
return { source, path: resolved };
|
|
1262
|
+
} catch {
|
|
1263
|
+
throw new Error(`API source must be an existing regular file inside the project: ${source}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/tools/read-api-spec.ts
|
|
1268
|
+
var readApiSpecSchema = z15.object({
|
|
1269
|
+
projectDir: z15.string().describe("Path to the Thally project root"),
|
|
1270
|
+
source: z15.string().optional().describe("Configured OpenAPI source; omit when the project has one")
|
|
1271
|
+
});
|
|
1272
|
+
async function handleReadApiSpec(input) {
|
|
1273
|
+
const source = resolveApiSourcePath(input.projectDir, input.source);
|
|
1274
|
+
return [`API source: ${source.source}`, "", readFileSync8(source.path, "utf8")].join("\n");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/tools/update-api-spec.ts
|
|
1278
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
1279
|
+
import { extname as extname2 } from "path";
|
|
1280
|
+
import { z as z16 } from "zod";
|
|
1281
|
+
import { parse as parseYaml } from "yaml";
|
|
1282
|
+
var updateApiSpecSchema = z16.object({
|
|
1283
|
+
projectDir: z16.string().describe("Path to the Thally project root"),
|
|
1284
|
+
source: z16.string().optional().describe("Configured OpenAPI source; omit when the project has one"),
|
|
1285
|
+
content: z16.string().min(2).max(512e3).describe("Complete replacement OpenAPI JSON or YAML document")
|
|
1286
|
+
});
|
|
1287
|
+
function parseDocument(source, content) {
|
|
1288
|
+
try {
|
|
1289
|
+
return extname2(source).toLowerCase() === ".json" ? JSON.parse(content) : parseYaml(content);
|
|
1290
|
+
} catch {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`OpenAPI source is not valid ${extname2(source).toLowerCase() === ".json" ? "JSON" : "YAML"}.`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async function handleUpdateApiSpec(input) {
|
|
1297
|
+
const source = resolveApiSourcePath(input.projectDir, input.source);
|
|
1298
|
+
const parsed = parseDocument(source.source, input.content);
|
|
1299
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("openapi" in parsed) || !("paths" in parsed)) {
|
|
1300
|
+
throw new Error("OpenAPI source must contain top-level openapi and paths fields.");
|
|
1301
|
+
}
|
|
1302
|
+
const content = input.content.endsWith("\n") ? input.content : `${input.content}
|
|
1303
|
+
`;
|
|
1304
|
+
writeFileSync6(source.path, content, "utf8");
|
|
1305
|
+
return `\u2705 API source updated: ${source.source}`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1211
1308
|
// src/lib/tools.ts
|
|
1212
1309
|
function defineTool(def) {
|
|
1213
1310
|
return def;
|
|
@@ -1248,6 +1345,20 @@ var tools = [
|
|
|
1248
1345
|
schema: updatePageSchema,
|
|
1249
1346
|
handler: handleUpdatePage
|
|
1250
1347
|
}),
|
|
1348
|
+
defineTool({
|
|
1349
|
+
name: "read_api_spec",
|
|
1350
|
+
description: "Read an OpenAPI JSON or YAML source explicitly configured by this Thally project",
|
|
1351
|
+
scope: "project",
|
|
1352
|
+
schema: readApiSpecSchema,
|
|
1353
|
+
handler: handleReadApiSpec
|
|
1354
|
+
}),
|
|
1355
|
+
defineTool({
|
|
1356
|
+
name: "update_api_spec",
|
|
1357
|
+
description: "Replace and validate an OpenAPI JSON or YAML source explicitly configured by this Thally project",
|
|
1358
|
+
scope: "project",
|
|
1359
|
+
schema: updateApiSpecSchema,
|
|
1360
|
+
handler: handleUpdateApiSpec
|
|
1361
|
+
}),
|
|
1251
1362
|
defineTool({
|
|
1252
1363
|
name: "migrate_docs",
|
|
1253
1364
|
description: "Create a fresh canonical Thally template, then migrate a GitHub repository or public docs site into it; the target must be new or empty",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thallylabs/mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"description": "MCP server for managing Thally knowledge surfaces and tracing product changes into documentation work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@anthropic-ai/sdk": "^0.36.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
41
|
-
"create-thally-docs": "0.10.
|
|
41
|
+
"create-thally-docs": "0.10.9",
|
|
42
42
|
"gray-matter": "^4.0.3",
|
|
43
43
|
"p-limit": "^6.1.0",
|
|
44
|
+
"yaml": "^2.8.2",
|
|
44
45
|
"zod": "^3.0.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|