@myspec/mcp-server 0.1.4-next.53 → 0.1.4-next.54

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +275 -192
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,7 +90,8 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
90
90
  | `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
91
91
  | `get_project` | `project_id` | Project + active sessions + files + attachments |
92
92
  | `get_spec_file` | `file_id`, optional `include_download_url` | File metadata; with `include_download_url=N` also returns a download URL targeting revision `N` |
93
- | `read_spec_file` | `file_id`, optional `revision` | UTF-8 content of the file (errors for binary). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
93
+ | `read_spec_file` | `file_id`, optional `revision` | UTF-8 content of the file (errors for binary or files > 1 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
94
+ | `download_spec_file` | `file_id`, `destination_path`, optional `revision`, optional `overwrite` | Downloads the file's raw bytes to `destination_path` (absolute or cwd-relative; an existing directory gets the basename appended). Handles binary files and files up to 50 MiB. Refuses to replace an existing file unless `overwrite=true`. Returns only metadata (`saved_path`, `bytes_written`, `revision_number`) — **not** the content, so a large file can be saved locally and read in chunks |
94
95
  | `list_spec_file` | `project_id`, optional `path`, optional `trashed` | Lists a project's spec files (`file_id`, `file_path`, `file_type`, `file_size_bytes`, `revision_count`, `session_id`, `updated_at`). Filter to a directory with `path` (exact file or everything under it; `""`/`"/"`/omit = all). `trashed=true` lists the Trash Bin instead of live files |
95
96
  | `move_spec_file_to_trash` | `file_id` | Moves a spec file to the Trash Bin (recoverable — hidden from listings and AI-agent tools, restorable indefinitely). Not a permanent delete |
96
97
  | `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
package/dist/index.js CHANGED
@@ -2132,16 +2132,177 @@ var deleteProjectTool = {
2132
2132
  }
2133
2133
  };
2134
2134
 
2135
- // src/server/tools/get_attachment.ts
2135
+ // src/server/tools/download_spec_file.ts
2136
+ import path3 from "path";
2136
2137
  import { z as z4 } from "zod";
2138
+
2139
+ // src/server/download-path.ts
2140
+ import { randomBytes as randomBytes2 } from "crypto";
2141
+ import { promises as fs2 } from "fs";
2142
+ import os2 from "os";
2143
+ import path2 from "path";
2144
+ function resolveDownloadRoot(env = process.env) {
2145
+ const configured = env.MYSPEC_DOWNLOAD_ROOT;
2146
+ if (configured) {
2147
+ if (!path2.isAbsolute(configured)) {
2148
+ throw new Error(`MYSPEC_DOWNLOAD_ROOT must be an absolute path (got: ${configured})`);
2149
+ }
2150
+ return path2.resolve(configured);
2151
+ }
2152
+ return path2.join(os2.homedir(), ".myspec");
2153
+ }
2154
+ function assertWithinRoot(target, root) {
2155
+ const resolvedTarget = path2.resolve(target);
2156
+ const resolvedRoot = path2.resolve(root);
2157
+ const rel = path2.relative(resolvedRoot, resolvedTarget);
2158
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
2159
+ return `path resolves outside the configured root (${resolvedRoot}): ${resolvedTarget}`;
2160
+ }
2161
+ return null;
2162
+ }
2163
+ function computeFileCachePath(args) {
2164
+ const basename4 = path2.basename(args.filePath) || "content";
2165
+ return path2.join(
2166
+ args.cacheRoot,
2167
+ "project",
2168
+ args.projectId,
2169
+ "file",
2170
+ args.fileId,
2171
+ "rev",
2172
+ String(args.revision),
2173
+ basename4
2174
+ );
2175
+ }
2176
+ function maskHomedir(p, home = os2.homedir()) {
2177
+ if (!home) {
2178
+ return p;
2179
+ }
2180
+ if (p === home) {
2181
+ return "~";
2182
+ }
2183
+ if (p.startsWith(home + path2.sep)) {
2184
+ return "~" + p.slice(home.length);
2185
+ }
2186
+ return p;
2187
+ }
2188
+ async function resolveDestinationFile(destPath, basename4) {
2189
+ const absPath = path2.isAbsolute(destPath) ? path2.resolve(destPath) : path2.resolve(process.cwd(), destPath);
2190
+ const endsWithSep = destPath.endsWith(path2.sep) || destPath.endsWith("/");
2191
+ let isDir = endsWithSep;
2192
+ if (!isDir) {
2193
+ try {
2194
+ const info = await fs2.stat(absPath);
2195
+ isDir = info.isDirectory();
2196
+ } catch (err) {
2197
+ if (!isFsNotFound2(err)) {
2198
+ throw err;
2199
+ }
2200
+ }
2201
+ }
2202
+ return isDir ? path2.join(absPath, basename4) : absPath;
2203
+ }
2204
+ async function pathExists(p) {
2205
+ try {
2206
+ await fs2.stat(p);
2207
+ return true;
2208
+ } catch (err) {
2209
+ if (isFsNotFound2(err)) {
2210
+ return false;
2211
+ }
2212
+ throw err;
2213
+ }
2214
+ }
2215
+ function isFsNotFound2(err) {
2216
+ const code = typeof err === "object" && err !== null ? err.code : void 0;
2217
+ return code === "ENOENT" || code === "ENOTDIR";
2218
+ }
2219
+ async function writeBytesAtomically(target, bytes) {
2220
+ const dir = path2.dirname(target);
2221
+ await fs2.mkdir(dir, { recursive: true });
2222
+ const suffix = randomBytes2(6).toString("hex");
2223
+ const tmp = path2.join(dir, `.${path2.basename(target)}.${process.pid.toString()}.${suffix}.tmp`);
2224
+ const handle = await fs2.open(tmp, "wx", 384);
2225
+ try {
2226
+ try {
2227
+ await handle.write(bytes);
2228
+ } finally {
2229
+ await handle.close();
2230
+ }
2231
+ await fs2.rename(tmp, target);
2232
+ } catch (err) {
2233
+ await fs2.rm(tmp, { force: true });
2234
+ throw err;
2235
+ }
2236
+ }
2237
+
2238
+ // src/server/tools/download_spec_file.ts
2239
+ var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
2137
2240
  var inputSchema4 = {
2138
- attachment_id: z4.string().min(1).describe("UUID of the attachment"),
2139
- include_download_url: z4.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
2241
+ file_id: z4.string().min(1).describe("UUID of the file"),
2242
+ revision: z4.number().int().positive().optional().describe("Specific revision number to download (defaults to the latest revision)."),
2243
+ destination_path: z4.string().min(1).describe(
2244
+ "Local path to save the file to. Absolute, or relative to the MCP server's working directory. If it points to an existing directory (or ends with a path separator), the spec file's basename is appended."
2245
+ ),
2246
+ overwrite: z4.boolean().optional().describe("Overwrite the destination file if it already exists (default: false).")
2247
+ };
2248
+ var downloadSpecFileTool = {
2249
+ name: "download_spec_file",
2250
+ description: "Download a spec file's raw bytes to a local file at `destination_path`. Unlike read_spec_file, this handles binary files and files up to 50 MiB, and the response returns only metadata (saved path, size, revision) \u2014 not the content. Use it to save a large or binary file locally, then search or read it in chunks with other tools.",
2251
+ inputSchema: inputSchema4,
2252
+ handler: async (args, ctx) => {
2253
+ const meta = await ctx.client.getFileMetadata(args.file_id);
2254
+ const targetRevision = args.revision ?? meta.revisionCount;
2255
+ if (targetRevision === meta.revisionCount && meta.fileSizeBytes > MAX_DOWNLOAD_BYTES) {
2256
+ return errorResult(buildOversizeMessage(args.file_id, meta.fileSizeBytes));
2257
+ }
2258
+ const basename4 = path3.basename(meta.filePath) || "content";
2259
+ let destFile;
2260
+ try {
2261
+ destFile = await resolveDestinationFile(args.destination_path, basename4);
2262
+ } catch (err) {
2263
+ const message = err instanceof Error ? err.message : String(err);
2264
+ return errorResult(`Failed to resolve destination_path ${args.destination_path}: ${message}`);
2265
+ }
2266
+ if (!args.overwrite && await pathExists(destFile)) {
2267
+ return errorResult(
2268
+ `Destination already exists: ${maskHomedir(destFile)}. Pass overwrite: true to replace it.`
2269
+ );
2270
+ }
2271
+ const downloaded = await ctx.client.downloadFile(args.file_id, args.revision);
2272
+ const bytes = downloaded.bytes;
2273
+ if (bytes.byteLength > MAX_DOWNLOAD_BYTES) {
2274
+ return errorResult(buildOversizeMessage(args.file_id, bytes.byteLength));
2275
+ }
2276
+ try {
2277
+ await writeBytesAtomically(destFile, bytes);
2278
+ } catch (err) {
2279
+ const message = err instanceof Error ? err.message : String(err);
2280
+ return errorResult(`Failed to write ${maskHomedir(destFile)}: ${message}`);
2281
+ }
2282
+ return jsonResult({
2283
+ file_id: meta.id,
2284
+ revision_number: downloaded.revisionNumber,
2285
+ file_path: meta.filePath,
2286
+ file_type: meta.fileType,
2287
+ saved_path: maskHomedir(destFile),
2288
+ bytes_written: bytes.byteLength
2289
+ });
2290
+ }
2291
+ };
2292
+ function buildOversizeMessage(fileId, byteLength) {
2293
+ return `File ${fileId} is ${String(byteLength)} bytes which exceeds the download cap of ${String(MAX_DOWNLOAD_BYTES)} bytes.`;
2294
+ }
2295
+
2296
+ // src/server/tools/get_attachment.ts
2297
+ import { z as z5 } from "zod";
2298
+ var inputSchema5 = {
2299
+ attachment_id: z5.string().min(1).describe("UUID of the attachment"),
2300
+ include_download_url: z5.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
2140
2301
  };
2141
2302
  var getAttachmentTool = {
2142
2303
  name: "get_attachment",
2143
2304
  description: "Get attachment metadata. With `include_download_url>0`, also returns a signed `download_url`.",
2144
- inputSchema: inputSchema4,
2305
+ inputSchema: inputSchema5,
2145
2306
  handler: async (args, ctx) => {
2146
2307
  const attachment = await ctx.client.getAttachmentMetadata(args.attachment_id);
2147
2308
  if (!attachment) {
@@ -2170,14 +2331,14 @@ var getAttachmentTool = {
2170
2331
  };
2171
2332
 
2172
2333
  // src/server/tools/get_project.ts
2173
- import { z as z5 } from "zod";
2174
- var inputSchema5 = {
2175
- project_id: z5.string().min(1).describe("UUID of the project to fetch")
2334
+ import { z as z6 } from "zod";
2335
+ var inputSchema6 = {
2336
+ project_id: z6.string().min(1).describe("UUID of the project to fetch")
2176
2337
  };
2177
2338
  var getProjectTool = {
2178
2339
  name: "get_project",
2179
2340
  description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
2180
- inputSchema: inputSchema5,
2341
+ inputSchema: inputSchema6,
2181
2342
  handler: async (args, ctx) => {
2182
2343
  const [project, sessions, files, attachments] = await Promise.all([
2183
2344
  ctx.client.getProject(args.project_id),
@@ -2223,17 +2384,17 @@ var getProjectTool = {
2223
2384
  };
2224
2385
 
2225
2386
  // src/server/tools/get_spec_file.ts
2226
- import { z as z6 } from "zod";
2227
- var inputSchema6 = {
2228
- file_id: z6.string().min(1).describe("UUID of the file"),
2229
- include_download_url: z6.number().int().nonnegative().optional().describe(
2387
+ import { z as z7 } from "zod";
2388
+ var inputSchema7 = {
2389
+ file_id: z7.string().min(1).describe("UUID of the file"),
2390
+ include_download_url: z7.number().int().nonnegative().optional().describe(
2230
2391
  "Revision number to include a download URL for. Returns a signed, time-limited URL for that revision (latest or historical) that needs no credentials. Omit or 0 to skip."
2231
2392
  )
2232
2393
  };
2233
2394
  var getSpecFileTool = {
2234
2395
  name: "get_spec_file",
2235
2396
  description: "Get spec file metadata. With `include_download_url=N`, also returns a signed, time-limited download URL for revision N (latest or historical). The URL is self-authenticating, so no credentials are returned.",
2236
- inputSchema: inputSchema6,
2397
+ inputSchema: inputSchema7,
2237
2398
  handler: async (args, ctx) => {
2238
2399
  const meta = await ctx.client.getFileMetadata(args.file_id);
2239
2400
  const base = {
@@ -2269,18 +2430,18 @@ var getSpecFileTool = {
2269
2430
  };
2270
2431
 
2271
2432
  // src/server/tools/list_projects.ts
2272
- import { z as z7 } from "zod";
2273
- var inputSchema7 = {
2274
- query: z7.string().optional().describe(
2433
+ import { z as z8 } from "zod";
2434
+ var inputSchema8 = {
2435
+ query: z8.string().optional().describe(
2275
2436
  "Search term to filter projects by name. Omit to list all projects."
2276
2437
  ),
2277
- limit: z7.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
2278
- offset: z7.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
2438
+ limit: z8.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
2439
+ offset: z8.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
2279
2440
  };
2280
2441
  var listProjectsTool = {
2281
2442
  name: "list_projects",
2282
2443
  description: "List or search MySpec projects accessible to the authenticated user.",
2283
- inputSchema: inputSchema7,
2444
+ inputSchema: inputSchema8,
2284
2445
  handler: async (args, ctx) => {
2285
2446
  const result = await ctx.client.listProjects({
2286
2447
  query: args.query,
@@ -2303,21 +2464,21 @@ var listProjectsTool = {
2303
2464
  };
2304
2465
 
2305
2466
  // src/server/tools/list_spec_file.ts
2306
- import { z as z8 } from "zod";
2467
+ import { z as z9 } from "zod";
2307
2468
  var PAGE_SIZE = 100;
2308
- var inputSchema8 = {
2309
- project_id: z8.string().min(1).describe("UUID of the project whose spec files to list."),
2310
- path: z8.string().optional().describe(
2469
+ var inputSchema9 = {
2470
+ project_id: z9.string().min(1).describe("UUID of the project whose spec files to list."),
2471
+ path: z9.string().optional().describe(
2311
2472
  'Directory prefix to filter by, e.g. ".spec" or ".spec/design". Matches the exact file and everything under that directory. Pass "", "/", or omit to list all files.'
2312
2473
  ),
2313
- trashed: z8.boolean().optional().describe(
2474
+ trashed: z9.boolean().optional().describe(
2314
2475
  "When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
2315
2476
  )
2316
2477
  };
2317
2478
  var listSpecFileTool = {
2318
2479
  name: "list_spec_file",
2319
2480
  description: 'List a project\'s spec files with id, path, and metadata (revision count, file size of the latest revision, file type, owning session, last update). Filter to a directory with `path` (exact file or everything under it; pass "", "/", or omit for all). Set `trashed=true` to list files in the Trash Bin instead of live files.',
2320
- inputSchema: inputSchema8,
2481
+ inputSchema: inputSchema9,
2321
2482
  handler: async (args, ctx) => {
2322
2483
  const trashed = args.trashed ?? false;
2323
2484
  const files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
@@ -2374,14 +2535,14 @@ function isUnderPath(filePath, prefix) {
2374
2535
  }
2375
2536
 
2376
2537
  // src/server/tools/move_spec_file_to_trash.ts
2377
- import { z as z9 } from "zod";
2378
- var inputSchema9 = {
2379
- file_id: z9.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
2538
+ import { z as z10 } from "zod";
2539
+ var inputSchema10 = {
2540
+ file_id: z10.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
2380
2541
  };
2381
2542
  var moveSpecFileToTrashTool = {
2382
2543
  name: "move_spec_file_to_trash",
2383
2544
  description: "Move a spec file to the Trash Bin. This is recoverable \u2014 the file is hidden from the default file list and AI-agent tools but kept restorable indefinitely via `restore_spec_file_from_trash`. It is NOT a permanent delete.",
2384
- inputSchema: inputSchema9,
2545
+ inputSchema: inputSchema10,
2385
2546
  handler: async (args, ctx) => {
2386
2547
  await ctx.client.trashFile(args.file_id);
2387
2548
  return jsonResult({ trashed: true, file_id: args.file_id });
@@ -2390,88 +2551,21 @@ var moveSpecFileToTrashTool = {
2390
2551
 
2391
2552
  // src/server/tools/read_spec_file.ts
2392
2553
  import { promises as fs3 } from "fs";
2393
- import os3 from "os";
2394
- import path3 from "path";
2395
- import { z as z10 } from "zod";
2396
-
2397
- // src/server/download-path.ts
2398
- import { promises as fs2 } from "fs";
2399
- import os2 from "os";
2400
- import path2 from "path";
2401
- function resolveDownloadRoot(env = process.env) {
2402
- const configured = env.MYSPEC_DOWNLOAD_ROOT;
2403
- if (configured) {
2404
- if (!path2.isAbsolute(configured)) {
2405
- throw new Error(`MYSPEC_DOWNLOAD_ROOT must be an absolute path (got: ${configured})`);
2406
- }
2407
- return path2.resolve(configured);
2408
- }
2409
- return path2.join(os2.homedir(), ".myspec");
2410
- }
2411
- function assertWithinRoot(target, root) {
2412
- const resolvedTarget = path2.resolve(target);
2413
- const resolvedRoot = path2.resolve(root);
2414
- const rel = path2.relative(resolvedRoot, resolvedTarget);
2415
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
2416
- return `path resolves outside the configured root (${resolvedRoot}): ${resolvedTarget}`;
2417
- }
2418
- return null;
2419
- }
2420
- function computeFileCachePath(args) {
2421
- const basename4 = path2.basename(args.filePath) || "content";
2422
- return path2.join(
2423
- args.cacheRoot,
2424
- "project",
2425
- args.projectId,
2426
- "file",
2427
- args.fileId,
2428
- "rev",
2429
- String(args.revision),
2430
- basename4
2431
- );
2432
- }
2433
- async function pathExists(p) {
2434
- try {
2435
- await fs2.stat(p);
2436
- return true;
2437
- } catch (err) {
2438
- if (isFsNotFound2(err)) {
2439
- return false;
2440
- }
2441
- throw err;
2442
- }
2443
- }
2444
- function isFsNotFound2(err) {
2445
- return typeof err === "object" && err !== null && err.code === "ENOENT";
2446
- }
2447
- async function writeBytesAtomically(target, bytes) {
2448
- const dir = path2.dirname(target);
2449
- await fs2.mkdir(dir, { recursive: true });
2450
- const tmp = path2.join(dir, `.${path2.basename(target)}.${process.pid.toString()}.tmp`);
2451
- const handle = await fs2.open(tmp, "wx", 384);
2452
- try {
2453
- await handle.write(bytes);
2454
- } finally {
2455
- await handle.close();
2456
- }
2457
- await fs2.rename(tmp, target);
2458
- }
2459
-
2460
- // src/server/tools/read_spec_file.ts
2554
+ import { z as z11 } from "zod";
2461
2555
  var MAX_READ_BYTES = 1 * 1024 * 1024;
2462
- var inputSchema10 = {
2463
- file_id: z10.string().min(1).describe("UUID of the file"),
2464
- revision: z10.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2556
+ var inputSchema11 = {
2557
+ file_id: z11.string().min(1).describe("UUID of the file"),
2558
+ revision: z11.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2465
2559
  };
2466
2560
  var readSpecFileTool = {
2467
2561
  name: "read_spec_file",
2468
- description: "Return a spec file's UTF-8 content. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision. Fails for binary files or files larger than 1 MiB \u2014 use get_spec_file with `include_download_url` to obtain a download URL for those.",
2469
- inputSchema: inputSchema10,
2562
+ description: "Return a spec file's UTF-8 content. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision. Fails for binary files or files larger than 1 MiB \u2014 use download_spec_file with a `destination_path` to save those to a local file instead.",
2563
+ inputSchema: inputSchema11,
2470
2564
  handler: async (args, ctx) => {
2471
2565
  const meta = await ctx.client.getFileMetadata(args.file_id);
2472
2566
  const targetRevision = args.revision ?? meta.revisionCount;
2473
2567
  if (targetRevision === meta.revisionCount && meta.fileSizeBytes > MAX_READ_BYTES) {
2474
- return errorResult(buildOversizeMessage(args.file_id, meta.fileSizeBytes));
2568
+ return errorResult(buildOversizeMessage2(args.file_id, meta.fileSizeBytes));
2475
2569
  }
2476
2570
  const cacheRoot = resolveDownloadRoot();
2477
2571
  const cachePath = computeFileCachePath({
@@ -2496,16 +2590,16 @@ var readSpecFileTool = {
2496
2590
  const downloaded = await ctx.client.downloadFile(args.file_id, args.revision);
2497
2591
  bytes = downloaded.bytes;
2498
2592
  if (bytes.byteLength > MAX_READ_BYTES) {
2499
- return errorResult(buildOversizeMessage(args.file_id, bytes.byteLength));
2593
+ return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
2500
2594
  }
2501
2595
  await writeBytesAtomically(cachePath, bytes);
2502
2596
  }
2503
2597
  if (bytes.byteLength > MAX_READ_BYTES) {
2504
- return errorResult(buildOversizeMessage(args.file_id, bytes.byteLength));
2598
+ return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
2505
2599
  }
2506
2600
  if (isBinary(bytes)) {
2507
2601
  return errorResult(
2508
- `File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use get_spec_file with \`include_download_url\` to obtain a download URL.`
2602
+ `File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use download_spec_file with a \`destination_path\` to save it to a local file.`
2509
2603
  );
2510
2604
  }
2511
2605
  let text;
@@ -2513,7 +2607,7 @@ var readSpecFileTool = {
2513
2607
  text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2514
2608
  } catch {
2515
2609
  return errorResult(
2516
- `File ${args.file_id} is not valid UTF-8 text. Use get_spec_file with \`include_download_url\` to obtain a download URL.`
2610
+ `File ${args.file_id} is not valid UTF-8 text. Use download_spec_file with a \`destination_path\` to save it to a local file.`
2517
2611
  );
2518
2612
  }
2519
2613
  return jsonResult({
@@ -2527,20 +2621,8 @@ var readSpecFileTool = {
2527
2621
  });
2528
2622
  }
2529
2623
  };
2530
- function buildOversizeMessage(fileId, byteLength) {
2531
- return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use get_spec_file with \`include_download_url\` to obtain a download URL and fetch it directly.`;
2532
- }
2533
- function maskHomedir(p, home = os3.homedir()) {
2534
- if (!home) {
2535
- return p;
2536
- }
2537
- if (p === home) {
2538
- return "~";
2539
- }
2540
- if (p.startsWith(home + path3.sep)) {
2541
- return "~" + p.slice(home.length);
2542
- }
2543
- return p;
2624
+ function buildOversizeMessage2(fileId, byteLength) {
2625
+ return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use download_spec_file with a \`destination_path\` to save it to a local file and read it in chunks.`;
2544
2626
  }
2545
2627
  function isBinary(bytes) {
2546
2628
  for (const byte of bytes) {
@@ -2552,14 +2634,14 @@ function isBinary(bytes) {
2552
2634
  }
2553
2635
 
2554
2636
  // src/server/tools/restore_spec_file_from_trash.ts
2555
- import { z as z11 } from "zod";
2556
- var inputSchema11 = {
2557
- file_id: z11.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
2637
+ import { z as z12 } from "zod";
2638
+ var inputSchema12 = {
2639
+ file_id: z12.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
2558
2640
  };
2559
2641
  var restoreSpecFileFromTrashTool = {
2560
2642
  name: "restore_spec_file_from_trash",
2561
2643
  description: "Restore a spec file from the Trash Bin back to the active file list, reversing `move_spec_file_to_trash`. The file becomes visible again to the default file list and AI-agent tools.",
2562
- inputSchema: inputSchema11,
2644
+ inputSchema: inputSchema12,
2563
2645
  handler: async (args, ctx) => {
2564
2646
  await ctx.client.restoreFile(args.file_id);
2565
2647
  return jsonResult({ restored: true, file_id: args.file_id });
@@ -2567,14 +2649,14 @@ var restoreSpecFileFromTrashTool = {
2567
2649
  };
2568
2650
 
2569
2651
  // src/server/tools/unarchive_project.ts
2570
- import { z as z12 } from "zod";
2571
- var inputSchema12 = {
2572
- project_id: z12.string().min(1).describe("UUID of the project to unarchive.")
2652
+ import { z as z13 } from "zod";
2653
+ var inputSchema13 = {
2654
+ project_id: z13.string().min(1).describe("UUID of the project to unarchive.")
2573
2655
  };
2574
2656
  var unarchiveProjectTool = {
2575
2657
  name: "unarchive_project",
2576
2658
  description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
2577
- inputSchema: inputSchema12,
2659
+ inputSchema: inputSchema13,
2578
2660
  handler: async (args, ctx) => {
2579
2661
  const project = await ctx.client.unarchiveProject(args.project_id);
2580
2662
  return jsonResult({ project: projectSummary(project) });
@@ -2582,19 +2664,19 @@ var unarchiveProjectTool = {
2582
2664
  };
2583
2665
 
2584
2666
  // src/server/tools/update_project.ts
2585
- import { z as z13 } from "zod";
2586
- var inputSchema13 = {
2587
- project_id: z13.string().min(1).describe("UUID of the project to update."),
2588
- name: z13.string().min(1).optional().describe("New project name."),
2589
- description: z13.string().optional().describe("New project description."),
2590
- project_type: z13.enum(["greenfield", "brownfield", "auto"]).optional().describe(
2667
+ import { z as z14 } from "zod";
2668
+ var inputSchema14 = {
2669
+ project_id: z14.string().min(1).describe("UUID of the project to update."),
2670
+ name: z14.string().min(1).optional().describe("New project name."),
2671
+ description: z14.string().optional().describe("New project description."),
2672
+ project_type: z14.enum(["greenfield", "brownfield", "auto"]).optional().describe(
2591
2673
  "Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
2592
2674
  )
2593
2675
  };
2594
2676
  var updateProjectTool = {
2595
2677
  name: "update_project",
2596
2678
  description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
2597
- inputSchema: inputSchema13,
2679
+ inputSchema: inputSchema14,
2598
2680
  handler: async (args, ctx) => {
2599
2681
  if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
2600
2682
  return errorResult(
@@ -2617,7 +2699,7 @@ var updateProjectTool = {
2617
2699
  };
2618
2700
 
2619
2701
  // src/server/tools/update_spec_file.ts
2620
- import { z as z14 } from "zod";
2702
+ import { z as z15 } from "zod";
2621
2703
 
2622
2704
  // src/server/tools/spec-content-source.ts
2623
2705
  import { readFile, stat } from "fs/promises";
@@ -2689,21 +2771,21 @@ function specFileSummary(file) {
2689
2771
  }
2690
2772
 
2691
2773
  // src/server/tools/update_spec_file.ts
2692
- var inputSchema14 = {
2693
- file_id: z14.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2694
- project_id: z14.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2695
- file_path: z14.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2696
- content: z14.string().min(1).optional().describe(
2774
+ var inputSchema15 = {
2775
+ file_id: z15.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2776
+ project_id: z15.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2777
+ file_path: z15.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2778
+ content: z15.string().min(1).optional().describe(
2697
2779
  "Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
2698
2780
  ),
2699
- local_file_path: z14.string().min(1).optional().describe(
2781
+ local_file_path: z15.string().min(1).optional().describe(
2700
2782
  "Path to a local file whose bytes become the new revision; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
2701
2783
  )
2702
2784
  };
2703
2785
  var updateSpecFileTool = {
2704
2786
  name: "update_spec_file",
2705
2787
  description: "Overwrite an existing spec file by saving a new revision. Provide the new body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly); use `content` for text generated in-memory. Reference the file by file_id or by (project_id + file_path).",
2706
- inputSchema: inputSchema14,
2788
+ inputSchema: inputSchema15,
2707
2789
  handler: async (args, ctx) => {
2708
2790
  const resolved = await resolveSpecContent({
2709
2791
  content: args.content,
@@ -2739,7 +2821,7 @@ var updateSpecFileTool = {
2739
2821
  import { createHash as createHash2 } from "crypto";
2740
2822
  import { lstat, readFile as readFile2 } from "fs/promises";
2741
2823
  import { basename, extname, isAbsolute as isAbsolute2 } from "path";
2742
- import { z as z15 } from "zod";
2824
+ import { z as z16 } from "zod";
2743
2825
  var DEFAULT_MIME = "application/octet-stream";
2744
2826
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
2745
2827
  var STRUCTURED_EXTENSION_MIME = {
@@ -2839,23 +2921,23 @@ function inferMimeFromName(name) {
2839
2921
  }
2840
2922
  return DEFAULT_MIME;
2841
2923
  }
2842
- var inputSchema15 = {
2843
- project_id: z15.string().min(1).describe("UUID of the project to attach the file to."),
2844
- file_path: z15.string().min(1).describe(
2924
+ var inputSchema16 = {
2925
+ project_id: z16.string().min(1).describe("UUID of the project to attach the file to."),
2926
+ file_path: z16.string().min(1).describe(
2845
2927
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
2846
2928
  ),
2847
- file_name: z15.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2848
- mime_type: z15.string().min(1).optional().describe(
2929
+ file_name: z16.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2930
+ mime_type: z16.string().min(1).optional().describe(
2849
2931
  "Override the MIME type sent to the platform. The platform re-detects from content, so this is only a hint. If omitted, inferred from the file extension \u2014 structured (.pdf, .docx, .xlsx) keep their canonical MIME; common UTF-8 text formats (.xml, .json, .yaml, .html, .csv, .md, source code, ...) are sent as text/plain."
2850
2932
  ),
2851
- override: z15.boolean().optional().describe(
2933
+ override: z16.boolean().optional().describe(
2852
2934
  "When true, any existing (non-deleted) attachment on the project whose recorded filename exactly matches `file_name` (or the path basename) is soft-deleted before the new upload, so the new attachment keeps the original name instead of getting an auto-dedup `(1)` suffix. The platform has no in-place content-replace API, so the new upload always gets a new `attachment_id`; any replaced ids are returned as `overridden_attachment_ids` (array, possibly empty) so callers can refresh stored references. Defaults to false (let the platform auto-dedup)."
2853
2935
  )
2854
2936
  };
2855
2937
  var uploadAttachmentTool = {
2856
2938
  name: "upload_attachment",
2857
2939
  description: `Upload a local file as a new attachment on a project. Reads \`file_path\` from the host running the MCP server, computes its SHA256, then performs the two-phase platform upload. Max ${String(MAX_ATTACHMENT_MB)} MB. Supported content: PDF, DOCX, XLSX, and any UTF-8 text file (XML, JSON, YAML, HTML, Markdown, CSV, source code, ...). Binary files outside the structured types will be rejected by the platform.`,
2858
- inputSchema: inputSchema15,
2940
+ inputSchema: inputSchema16,
2859
2941
  handler: async (args, ctx) => {
2860
2942
  if (!isAbsolute2(args.file_path)) {
2861
2943
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
@@ -2979,7 +3061,7 @@ var uploadAttachmentTool = {
2979
3061
  };
2980
3062
 
2981
3063
  // src/server/tools/upload_spec_file.ts
2982
- import { z as z16 } from "zod";
3064
+ import { z as z17 } from "zod";
2983
3065
 
2984
3066
  // src/server/spec-path.ts
2985
3067
  var MAX_SPEC_DIR_DEPTH = 3;
@@ -3032,23 +3114,23 @@ var SPEC_FILE_TYPES = [
3032
3114
  "proposal",
3033
3115
  "openspec-spec"
3034
3116
  ];
3035
- var inputSchema16 = {
3036
- project_id: z16.string().min(1).describe("UUID of the project to upload the file into."),
3037
- file_path: z16.string().min(1).describe(
3117
+ var inputSchema17 = {
3118
+ project_id: z17.string().min(1).describe("UUID of the project to upload the file into."),
3119
+ file_path: z17.string().min(1).describe(
3038
3120
  'Path of the new spec file, rooted at "specs" or "openspec" with at most 3 directory levels below the root (e.g. specs/changes/add-oauth-login/proposal.md).'
3039
3121
  ),
3040
- content: z16.string().min(1).optional().describe(
3122
+ content: z17.string().min(1).optional().describe(
3041
3123
  "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
3042
3124
  ),
3043
- local_file_path: z16.string().min(1).optional().describe(
3125
+ local_file_path: z17.string().min(1).optional().describe(
3044
3126
  "Path to a local file whose bytes become the spec file body; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
3045
3127
  ),
3046
- file_type: z16.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
3128
+ file_type: z17.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
3047
3129
  };
3048
3130
  var uploadSpecFileTool = {
3049
3131
  name: "upload_spec_file",
3050
3132
  description: "Upload a new, session-less spec file into a project. Provide the file body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly, avoiding sending large content through the model); use `content` for text generated in-memory. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
3051
- inputSchema: inputSchema16,
3133
+ inputSchema: inputSchema17,
3052
3134
  handler: async (args, ctx) => {
3053
3135
  const pathError = validateSpecFilePath(args.file_path);
3054
3136
  if (pathError) {
@@ -3088,6 +3170,7 @@ function buildServer(deps) {
3088
3170
  registerTool(server, getProjectTool, deps.client);
3089
3171
  registerTool(server, getSpecFileTool, deps.client);
3090
3172
  registerTool(server, readSpecFileTool, deps.client);
3173
+ registerTool(server, downloadSpecFileTool, deps.client);
3091
3174
  registerTool(server, listSpecFileTool, deps.client);
3092
3175
  registerTool(server, getAttachmentTool, deps.client);
3093
3176
  registerTool(server, uploadAttachmentTool, deps.client);
@@ -3253,7 +3336,7 @@ async function canonicaliseRoot(root) {
3253
3336
  import { promises as fs4 } from "fs";
3254
3337
  import { join as join2, relative } from "path";
3255
3338
  import { spawn } from "child_process";
3256
- import { z as z17 } from "zod";
3339
+ import { z as z18 } from "zod";
3257
3340
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3258
3341
 
3259
3342
  // src/reverse/ignore.ts
@@ -3558,37 +3641,37 @@ var READ_FILE_MAX_LINES = 2e3;
3558
3641
  var LIST_DIR_HARD_CAP = 5e3;
3559
3642
  var LIST_DIR_DEFAULT_CAP = 1e3;
3560
3643
  var GREP_MAX_MATCHES_DEFAULT = 200;
3561
- var listDirInput = z17.object({
3562
- path: z17.string(),
3563
- recursive: z17.boolean().optional(),
3564
- maxEntries: z17.number().int().positive().optional(),
3565
- includeIgnored: z17.boolean().optional()
3644
+ var listDirInput = z18.object({
3645
+ path: z18.string(),
3646
+ recursive: z18.boolean().optional(),
3647
+ maxEntries: z18.number().int().positive().optional(),
3648
+ includeIgnored: z18.boolean().optional()
3566
3649
  });
3567
- var readFileInput = z17.object({
3568
- path: z17.string(),
3569
- offset: z17.number().int().nonnegative().optional(),
3570
- limit: z17.number().int().positive().optional(),
3571
- includeIgnored: z17.boolean().optional()
3650
+ var readFileInput = z18.object({
3651
+ path: z18.string(),
3652
+ offset: z18.number().int().nonnegative().optional(),
3653
+ limit: z18.number().int().positive().optional(),
3654
+ includeIgnored: z18.boolean().optional()
3572
3655
  });
3573
- var grepInput = z17.object({
3574
- pattern: z17.string().describe(
3656
+ var grepInput = z18.object({
3657
+ pattern: z18.string().describe(
3575
3658
  "Search pattern. Interpreted as a regular expression by default (ripgrep syntax), e.g. `foo|bar` or `[Rr]ole`. Set `isRegex: false` to match the pattern as an exact literal string instead."
3576
3659
  ),
3577
- path: z17.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3578
- isRegex: z17.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3579
- caseSensitive: z17.boolean().optional(),
3580
- maxMatches: z17.number().int().positive().optional(),
3581
- contextLines: z17.number().int().nonnegative().optional(),
3582
- includeIgnored: z17.boolean().optional()
3660
+ path: z18.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3661
+ isRegex: z18.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3662
+ caseSensitive: z18.boolean().optional(),
3663
+ maxMatches: z18.number().int().positive().optional(),
3664
+ contextLines: z18.number().int().nonnegative().optional(),
3665
+ includeIgnored: z18.boolean().optional()
3583
3666
  });
3584
- var packCodebaseInput = z17.object({
3585
- subpath: z17.string().optional(),
3586
- includePatterns: z17.string().optional(),
3587
- ignorePatterns: z17.string().optional()
3667
+ var packCodebaseInput = z18.object({
3668
+ subpath: z18.string().optional(),
3669
+ includePatterns: z18.string().optional(),
3670
+ ignorePatterns: z18.string().optional()
3588
3671
  });
3589
- var packCodebaseReadPageInput = z17.object({
3590
- outputId: z17.string(),
3591
- page: z17.number().int().positive()
3672
+ var packCodebaseReadPageInput = z18.object({
3673
+ outputId: z18.string(),
3674
+ page: z18.number().int().positive()
3592
3675
  });
3593
3676
  function structuredError(error, message, extra) {
3594
3677
  const payload = { error, message, ...extra ?? {} };
@@ -4116,7 +4199,7 @@ function jsonSchemaFromZod(schema) {
4116
4199
  return zodToJson(schema);
4117
4200
  }
4118
4201
  function zodToJson(schema) {
4119
- if (schema instanceof z17.ZodObject) {
4202
+ if (schema instanceof z18.ZodObject) {
4120
4203
  const shape = schema.shape;
4121
4204
  const properties = {};
4122
4205
  const required = [];
@@ -4131,19 +4214,19 @@ function zodToJson(schema) {
4131
4214
  if (required.length > 0) out.required = required;
4132
4215
  return out;
4133
4216
  }
4134
- if (schema instanceof z17.ZodOptional) {
4217
+ if (schema instanceof z18.ZodOptional) {
4135
4218
  return zodToJson(schema.unwrap());
4136
4219
  }
4137
- if (schema instanceof z17.ZodString) {
4220
+ if (schema instanceof z18.ZodString) {
4138
4221
  return { type: "string" };
4139
4222
  }
4140
- if (schema instanceof z17.ZodNumber) {
4223
+ if (schema instanceof z18.ZodNumber) {
4141
4224
  return { type: "number" };
4142
4225
  }
4143
- if (schema instanceof z17.ZodBoolean) {
4226
+ if (schema instanceof z18.ZodBoolean) {
4144
4227
  return { type: "boolean" };
4145
4228
  }
4146
- if (schema instanceof z17.ZodEnum) {
4229
+ if (schema instanceof z18.ZodEnum) {
4147
4230
  return { type: "string", enum: schema.options };
4148
4231
  }
4149
4232
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.4-next.53",
3
+ "version": "0.1.4-next.54",
4
4
  "description": "MySpec MCP server — exposes MySpec platform projects, files and attachments to MCP-aware clients via OAuth-authenticated access tokens.",
5
5
  "type": "module",
6
6
  "repository": {