@thallylabs/mcp 0.10.7 → 0.10.9

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 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.7",
3
+ "version": "0.10.9",
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.7",
41
+ "create-thally-docs": "0.10.8",
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": {