@myspec/mcp-server 0.1.2-next.38 → 0.1.2-next.39

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 +7 -4
  2. package/dist/index.js +256 -132
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -80,7 +80,7 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
80
80
  |---|---|
81
81
  | `MYSPEC_USER_AUTH_URL` | user-auth base URL (default `https://auth.myspec.dev`). The webapp URL is derived from it; the platform / ai-agent URLs are discovered via the webapp. |
82
82
  | `MYSPEC_REFRESH_TOKEN` | Skip file-based credentials; the server mints a fresh access token on first use via this refresh token. The supported way to wire the MCP server up without running `npx @myspec/mcp-server login`. |
83
- | `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
83
+ | `MYSPEC_DOWNLOAD_ROOT` | Absolute path used by `read_spec_file` as its on-disk cache root (default: `~/.myspec`). Tools never write outside this root. |
84
84
  | `MYSPEC_AI_AGENT_WS_URL` | ai-agent WebSocket URL for `reverse`; skips the webapp discovery call |
85
85
 
86
86
  ## Tools
@@ -89,14 +89,17 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
89
89
  |---|---|---|
90
90
  | `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
91
91
  | `get_project` | `project_id` | Project + active sessions + files + attachments |
92
- | `get_file` | `file_id`, optional `include_download_url` | File metadata; with `include_download_url=N` also returns a download URL targeting revision `N` |
93
- | `read_file` | `file_id`, optional `revision` | UTF-8 content of the file (errors for binary). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
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` |
94
+ | `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
+ | `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
+ | `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
94
97
  | `get_attachment` | `attachment_id`, optional `include_download_url` | Attachment metadata (looked up org-scoped by id); with `include_download_url>0` also returns a signed download URL |
95
98
  | `upload_attachment` | `project_id`, `file_path` (absolute), optional `file_name`, optional `mime_type`, optional `override` | Uploads the local file as a new attachment on the project. Returns `attachment_id`, `file_name` (the **effective** name the platform stored — may differ from the requested one when auto-dedup adds a `(N)` suffix), `has_file_name_conflict` (`true` when stored name ≠ requested name), `mime_type`, `file_size_bytes`, `checksum` (format: `sha256:<hex>`), `file_uri`, `reused_existing_attachment` (`true` when an existing attachment with the same name AND same checksum was found and returned without re-uploading; `file_uri` is then omitted because the list endpoint does not surface it — call `get_attachment` for a download URL). With `override=true`, any existing attachment whose recorded name matches is soft-deleted first (returned as `overridden_attachment_ids` — empty array means nothing matched) and the new upload keeps the requested name; the same-checksum short-circuit still applies, so a no-op re-upload never deletes anything. Max 10 MB. Supported content: PDF, DOCX, XLSX, and any UTF-8 text file (XML, JSON, YAML, HTML, Markdown, CSV, source code, ...) |
96
99
 
97
100
  ### `include_download_url` semantics
98
101
 
99
- `get_file`:
102
+ `get_spec_file`:
100
103
 
101
104
  - Omit or `0` → metadata only.
102
105
  - Equal to current `revision_count` → adds `download_url` (signed) and `expires_at`.
package/dist/index.js CHANGED
@@ -1890,6 +1890,26 @@ var PlatformClient = class {
1890
1890
  async deleteProject(projectId) {
1891
1891
  await this.withTokenRetry((jwt, c) => c.project.deleteProject(projectId, jwt));
1892
1892
  }
1893
+ /**
1894
+ * Moves a spec file to the Trash Bin (recoverable). The platform hides a
1895
+ * trashed file from the default file list and all AI-agent tools, but keeps
1896
+ * it restorable indefinitely via {@link restoreFile}.
1897
+ */
1898
+ async trashFile(fileId) {
1899
+ await this.withTokenRetry((jwt, c) => c.file.trashFile(fileId, jwt));
1900
+ }
1901
+ /** Restores a spec file from the Trash Bin back to the active file list. */
1902
+ async restoreFile(fileId) {
1903
+ await this.withTokenRetry((jwt, c) => c.file.restoreFile(fileId, jwt));
1904
+ }
1905
+ /**
1906
+ * Lists every spec file currently in the project's Trash Bin. The platform
1907
+ * returns all trashed files in one response (no pagination), so this is a
1908
+ * single call rather than a paged loop like {@link listFiles}.
1909
+ */
1910
+ async listTrashedFiles(projectId) {
1911
+ return this.withTokenRetry((jwt, c) => c.file.listTrashedFiles(projectId, jwt));
1912
+ }
1893
1913
  /**
1894
1914
  * Two-phase upload of a new, session-less ("unbound") spec file. Reserves a
1895
1915
  * file id (Phase 1, `sessionId` omitted so the file is unbound), then PUTs
@@ -2131,61 +2151,15 @@ var getAttachmentTool = {
2131
2151
  }
2132
2152
  };
2133
2153
 
2134
- // src/server/tools/get_file.ts
2154
+ // src/server/tools/get_project.ts
2135
2155
  import { z as z5 } from "zod";
2136
2156
  var inputSchema5 = {
2137
- file_id: z5.string().min(1).describe("UUID of the file"),
2138
- include_download_url: z5.number().int().nonnegative().optional().describe(
2139
- "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."
2140
- )
2141
- };
2142
- var getFileTool = {
2143
- name: "get_file",
2144
- description: "Get 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.",
2145
- inputSchema: inputSchema5,
2146
- handler: async (args, ctx) => {
2147
- const meta = await ctx.client.getFileMetadata(args.file_id);
2148
- const base = {
2149
- id: meta.id,
2150
- project_id: meta.projectId,
2151
- session_id: meta.sessionId,
2152
- file_type: meta.fileType,
2153
- file_path: meta.filePath,
2154
- file_size_bytes: meta.fileSizeBytes,
2155
- checksum: meta.checksum,
2156
- revision_count: meta.revisionCount,
2157
- created_at: meta.createdAt,
2158
- updated_at: meta.updatedAt
2159
- };
2160
- const requested = args.include_download_url ?? 0;
2161
- if (requested <= 0) {
2162
- return jsonResult(base);
2163
- }
2164
- if (requested > meta.revisionCount) {
2165
- return errorResult(
2166
- `include_download_url=${String(requested)} is greater than revision_count=${String(meta.revisionCount)} for file ${meta.id}.`
2167
- );
2168
- }
2169
- const isLatest = requested === meta.revisionCount;
2170
- const signed = isLatest ? await ctx.client.getFileDownloadUrl(meta.id) : await ctx.client.getRevisionDownloadUrl(meta.id, requested);
2171
- const downloadBlock = {
2172
- download_url: signed.signedUrl,
2173
- download_revision: requested,
2174
- expires_at: signed.expiresAt.toISOString()
2175
- };
2176
- return jsonResult({ ...base, ...downloadBlock });
2177
- }
2178
- };
2179
-
2180
- // src/server/tools/get_project.ts
2181
- import { z as z6 } from "zod";
2182
- var inputSchema6 = {
2183
- project_id: z6.string().min(1).describe("UUID of the project to fetch")
2157
+ project_id: z5.string().min(1).describe("UUID of the project to fetch")
2184
2158
  };
2185
2159
  var getProjectTool = {
2186
2160
  name: "get_project",
2187
2161
  description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
2188
- inputSchema: inputSchema6,
2162
+ inputSchema: inputSchema5,
2189
2163
  handler: async (args, ctx) => {
2190
2164
  const [project, sessions, files, attachments] = await Promise.all([
2191
2165
  ctx.client.getProject(args.project_id),
@@ -2230,6 +2204,52 @@ var getProjectTool = {
2230
2204
  }
2231
2205
  };
2232
2206
 
2207
+ // src/server/tools/get_spec_file.ts
2208
+ import { z as z6 } from "zod";
2209
+ var inputSchema6 = {
2210
+ file_id: z6.string().min(1).describe("UUID of the file"),
2211
+ include_download_url: z6.number().int().nonnegative().optional().describe(
2212
+ "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."
2213
+ )
2214
+ };
2215
+ var getSpecFileTool = {
2216
+ name: "get_spec_file",
2217
+ 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.",
2218
+ inputSchema: inputSchema6,
2219
+ handler: async (args, ctx) => {
2220
+ const meta = await ctx.client.getFileMetadata(args.file_id);
2221
+ const base = {
2222
+ id: meta.id,
2223
+ project_id: meta.projectId,
2224
+ session_id: meta.sessionId,
2225
+ file_type: meta.fileType,
2226
+ file_path: meta.filePath,
2227
+ file_size_bytes: meta.fileSizeBytes,
2228
+ checksum: meta.checksum,
2229
+ revision_count: meta.revisionCount,
2230
+ created_at: meta.createdAt,
2231
+ updated_at: meta.updatedAt
2232
+ };
2233
+ const requested = args.include_download_url ?? 0;
2234
+ if (requested <= 0) {
2235
+ return jsonResult(base);
2236
+ }
2237
+ if (requested > meta.revisionCount) {
2238
+ return errorResult(
2239
+ `include_download_url=${String(requested)} is greater than revision_count=${String(meta.revisionCount)} for file ${meta.id}.`
2240
+ );
2241
+ }
2242
+ const isLatest = requested === meta.revisionCount;
2243
+ const signed = isLatest ? await ctx.client.getFileDownloadUrl(meta.id) : await ctx.client.getRevisionDownloadUrl(meta.id, requested);
2244
+ const downloadBlock = {
2245
+ download_url: signed.signedUrl,
2246
+ download_revision: requested,
2247
+ expires_at: signed.expiresAt.toISOString()
2248
+ };
2249
+ return jsonResult({ ...base, ...downloadBlock });
2250
+ }
2251
+ };
2252
+
2233
2253
  // src/server/tools/list_projects.ts
2234
2254
  import { z as z7 } from "zod";
2235
2255
  var inputSchema7 = {
@@ -2264,11 +2284,97 @@ var listProjectsTool = {
2264
2284
  }
2265
2285
  };
2266
2286
 
2267
- // src/server/tools/read_file.ts
2287
+ // src/server/tools/list_spec_file.ts
2288
+ import { z as z8 } from "zod";
2289
+ var PAGE_SIZE = 100;
2290
+ var inputSchema8 = {
2291
+ project_id: z8.string().min(1).describe("UUID of the project whose spec files to list."),
2292
+ path: z8.string().optional().describe(
2293
+ '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.'
2294
+ ),
2295
+ trashed: z8.boolean().optional().describe(
2296
+ "When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
2297
+ )
2298
+ };
2299
+ var listSpecFileTool = {
2300
+ name: "list_spec_file",
2301
+ 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.',
2302
+ inputSchema: inputSchema8,
2303
+ handler: async (args, ctx) => {
2304
+ const trashed = args.trashed ?? false;
2305
+ const files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
2306
+ const prefix = normalizePrefix(args.path);
2307
+ const matches = files.filter((f) => isUnderPath(f.filePath, prefix)).map(toEntry).sort((a, b) => a.file_path.localeCompare(b.file_path));
2308
+ return jsonResult({
2309
+ project_id: args.project_id,
2310
+ path: prefix,
2311
+ trashed,
2312
+ total: matches.length,
2313
+ files: matches
2314
+ });
2315
+ }
2316
+ };
2317
+ async function listAllLiveFiles(client, projectId) {
2318
+ const all = [];
2319
+ let offset = 0;
2320
+ for (; ; ) {
2321
+ const page = await client.listFiles(projectId, { limit: PAGE_SIZE, offset });
2322
+ all.push(...page.files);
2323
+ offset += page.files.length;
2324
+ if (page.files.length < PAGE_SIZE || offset >= page.total) {
2325
+ return all;
2326
+ }
2327
+ }
2328
+ }
2329
+ function toEntry(file) {
2330
+ const entry = {
2331
+ file_id: file.id,
2332
+ file_path: file.filePath,
2333
+ file_type: file.fileType,
2334
+ file_size_bytes: file.fileSizeBytes,
2335
+ revision_count: file.revisionCount,
2336
+ session_id: file.sessionId,
2337
+ updated_at: file.updatedAt
2338
+ };
2339
+ if (file.trashedAt) {
2340
+ entry.trashed_at = file.trashedAt;
2341
+ }
2342
+ return entry;
2343
+ }
2344
+ function normalizePrefix(path4) {
2345
+ if (!path4) {
2346
+ return "";
2347
+ }
2348
+ const trimmed = path4.trim().replace(/^\/+/, "").replace(/\/+$/, "");
2349
+ return trimmed;
2350
+ }
2351
+ function isUnderPath(filePath, prefix) {
2352
+ if (prefix === "") {
2353
+ return true;
2354
+ }
2355
+ return filePath === prefix || filePath.startsWith(prefix + "/");
2356
+ }
2357
+
2358
+ // src/server/tools/move_spec_file_to_trash.ts
2359
+ import { z as z9 } from "zod";
2360
+ var inputSchema9 = {
2361
+ file_id: z9.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
2362
+ };
2363
+ var moveSpecFileToTrashTool = {
2364
+ name: "move_spec_file_to_trash",
2365
+ 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.",
2366
+ inputSchema: inputSchema9,
2367
+ handler: async (args, ctx) => {
2368
+ await ctx.client.trashFile(args.file_id);
2369
+ return jsonResult({ trashed: true, file_id: args.file_id });
2370
+ }
2371
+ };
2372
+
2373
+ // src/server/tools/read_spec_file.ts
2268
2374
  import { promises as fs3 } from "fs";
2269
2375
  import os3 from "os";
2270
2376
  import path3 from "path";
2271
- import { z as z8 } from "zod";
2377
+ import { z as z10 } from "zod";
2272
2378
 
2273
2379
  // src/server/download-path.ts
2274
2380
  import { promises as fs2 } from "fs";
@@ -2333,16 +2439,16 @@ async function writeBytesAtomically(target, bytes) {
2333
2439
  await fs2.rename(tmp, target);
2334
2440
  }
2335
2441
 
2336
- // src/server/tools/read_file.ts
2442
+ // src/server/tools/read_spec_file.ts
2337
2443
  var MAX_READ_BYTES = 1 * 1024 * 1024;
2338
- var inputSchema8 = {
2339
- file_id: z8.string().min(1).describe("UUID of the file"),
2340
- revision: z8.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2444
+ var inputSchema10 = {
2445
+ file_id: z10.string().min(1).describe("UUID of the file"),
2446
+ revision: z10.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
2341
2447
  };
2342
- var readFileTool = {
2343
- name: "read_file",
2344
- description: "Return a 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_file with `include_download_url` to obtain a download URL for those.",
2345
- inputSchema: inputSchema8,
2448
+ var readSpecFileTool = {
2449
+ name: "read_spec_file",
2450
+ 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.",
2451
+ inputSchema: inputSchema10,
2346
2452
  handler: async (args, ctx) => {
2347
2453
  const meta = await ctx.client.getFileMetadata(args.file_id);
2348
2454
  const targetRevision = args.revision ?? meta.revisionCount;
@@ -2360,7 +2466,7 @@ var readFileTool = {
2360
2466
  const escape = assertWithinRoot(cachePath, cacheRoot);
2361
2467
  if (escape) {
2362
2468
  return errorResult(
2363
- `read_file refused to write cache: ${escape}. This indicates an unexpected platform response shape (projectId/fileId/filePath).`
2469
+ `read_spec_file refused to write cache: ${escape}. This indicates an unexpected platform response shape (projectId/fileId/filePath).`
2364
2470
  );
2365
2471
  }
2366
2472
  let bytes;
@@ -2381,7 +2487,7 @@ var readFileTool = {
2381
2487
  }
2382
2488
  if (isBinary(bytes)) {
2383
2489
  return errorResult(
2384
- `File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use get_file with \`include_download_url\` to obtain a download URL.`
2490
+ `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.`
2385
2491
  );
2386
2492
  }
2387
2493
  let text;
@@ -2389,7 +2495,7 @@ var readFileTool = {
2389
2495
  text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2390
2496
  } catch {
2391
2497
  return errorResult(
2392
- `File ${args.file_id} is not valid UTF-8 text. Use get_file with \`include_download_url\` to obtain a download URL.`
2498
+ `File ${args.file_id} is not valid UTF-8 text. Use get_spec_file with \`include_download_url\` to obtain a download URL.`
2393
2499
  );
2394
2500
  }
2395
2501
  return jsonResult({
@@ -2404,7 +2510,7 @@ var readFileTool = {
2404
2510
  }
2405
2511
  };
2406
2512
  function buildOversizeMessage(fileId, byteLength) {
2407
- return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use get_file with \`include_download_url\` to obtain a download URL and fetch it directly.`;
2513
+ 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.`;
2408
2514
  }
2409
2515
  function maskHomedir(p, home = os3.homedir()) {
2410
2516
  if (!home) {
@@ -2427,15 +2533,30 @@ function isBinary(bytes) {
2427
2533
  return false;
2428
2534
  }
2429
2535
 
2536
+ // src/server/tools/restore_spec_file_from_trash.ts
2537
+ import { z as z11 } from "zod";
2538
+ var inputSchema11 = {
2539
+ file_id: z11.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
2540
+ };
2541
+ var restoreSpecFileFromTrashTool = {
2542
+ name: "restore_spec_file_from_trash",
2543
+ 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.",
2544
+ inputSchema: inputSchema11,
2545
+ handler: async (args, ctx) => {
2546
+ await ctx.client.restoreFile(args.file_id);
2547
+ return jsonResult({ restored: true, file_id: args.file_id });
2548
+ }
2549
+ };
2550
+
2430
2551
  // src/server/tools/unarchive_project.ts
2431
- import { z as z9 } from "zod";
2432
- var inputSchema9 = {
2433
- project_id: z9.string().min(1).describe("UUID of the project to unarchive.")
2552
+ import { z as z12 } from "zod";
2553
+ var inputSchema12 = {
2554
+ project_id: z12.string().min(1).describe("UUID of the project to unarchive.")
2434
2555
  };
2435
2556
  var unarchiveProjectTool = {
2436
2557
  name: "unarchive_project",
2437
2558
  description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
2438
- inputSchema: inputSchema9,
2559
+ inputSchema: inputSchema12,
2439
2560
  handler: async (args, ctx) => {
2440
2561
  const project = await ctx.client.unarchiveProject(args.project_id);
2441
2562
  return jsonResult({ project: projectSummary(project) });
@@ -2443,19 +2564,19 @@ var unarchiveProjectTool = {
2443
2564
  };
2444
2565
 
2445
2566
  // src/server/tools/update_project.ts
2446
- import { z as z10 } from "zod";
2447
- var inputSchema10 = {
2448
- project_id: z10.string().min(1).describe("UUID of the project to update."),
2449
- name: z10.string().min(1).optional().describe("New project name."),
2450
- description: z10.string().optional().describe("New project description."),
2451
- project_type: z10.enum(["greenfield", "brownfield", "auto"]).optional().describe(
2567
+ import { z as z13 } from "zod";
2568
+ var inputSchema13 = {
2569
+ project_id: z13.string().min(1).describe("UUID of the project to update."),
2570
+ name: z13.string().min(1).optional().describe("New project name."),
2571
+ description: z13.string().optional().describe("New project description."),
2572
+ project_type: z13.enum(["greenfield", "brownfield", "auto"]).optional().describe(
2452
2573
  "Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
2453
2574
  )
2454
2575
  };
2455
2576
  var updateProjectTool = {
2456
2577
  name: "update_project",
2457
2578
  description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
2458
- inputSchema: inputSchema10,
2579
+ inputSchema: inputSchema13,
2459
2580
  handler: async (args, ctx) => {
2460
2581
  if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
2461
2582
  return errorResult(
@@ -2478,7 +2599,7 @@ var updateProjectTool = {
2478
2599
  };
2479
2600
 
2480
2601
  // src/server/tools/update_spec_file.ts
2481
- import { z as z11 } from "zod";
2602
+ import { z as z14 } from "zod";
2482
2603
 
2483
2604
  // src/server/tools/spec-content-source.ts
2484
2605
  import { readFile, stat } from "fs/promises";
@@ -2550,21 +2671,21 @@ function specFileSummary(file) {
2550
2671
  }
2551
2672
 
2552
2673
  // src/server/tools/update_spec_file.ts
2553
- var inputSchema11 = {
2554
- file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2555
- project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2556
- file_path: z11.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2557
- content: z11.string().min(1).optional().describe(
2674
+ var inputSchema14 = {
2675
+ file_id: z14.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2676
+ project_id: z14.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2677
+ file_path: z14.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2678
+ content: z14.string().min(1).optional().describe(
2558
2679
  "Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
2559
2680
  ),
2560
- local_file_path: z11.string().min(1).optional().describe(
2681
+ local_file_path: z14.string().min(1).optional().describe(
2561
2682
  "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."
2562
2683
  )
2563
2684
  };
2564
2685
  var updateSpecFileTool = {
2565
2686
  name: "update_spec_file",
2566
2687
  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).",
2567
- inputSchema: inputSchema11,
2688
+ inputSchema: inputSchema14,
2568
2689
  handler: async (args, ctx) => {
2569
2690
  const resolved = await resolveSpecContent({
2570
2691
  content: args.content,
@@ -2600,7 +2721,7 @@ var updateSpecFileTool = {
2600
2721
  import { createHash as createHash2 } from "crypto";
2601
2722
  import { lstat, readFile as readFile2 } from "fs/promises";
2602
2723
  import { basename, extname, isAbsolute as isAbsolute2 } from "path";
2603
- import { z as z12 } from "zod";
2724
+ import { z as z15 } from "zod";
2604
2725
  var DEFAULT_MIME = "application/octet-stream";
2605
2726
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
2606
2727
  var STRUCTURED_EXTENSION_MIME = {
@@ -2700,23 +2821,23 @@ function inferMimeFromName(name) {
2700
2821
  }
2701
2822
  return DEFAULT_MIME;
2702
2823
  }
2703
- var inputSchema12 = {
2704
- project_id: z12.string().min(1).describe("UUID of the project to attach the file to."),
2705
- file_path: z12.string().min(1).describe(
2824
+ var inputSchema15 = {
2825
+ project_id: z15.string().min(1).describe("UUID of the project to attach the file to."),
2826
+ file_path: z15.string().min(1).describe(
2706
2827
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
2707
2828
  ),
2708
- file_name: z12.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2709
- mime_type: z12.string().min(1).optional().describe(
2829
+ file_name: z15.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2830
+ mime_type: z15.string().min(1).optional().describe(
2710
2831
  "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."
2711
2832
  ),
2712
- override: z12.boolean().optional().describe(
2833
+ override: z15.boolean().optional().describe(
2713
2834
  "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)."
2714
2835
  )
2715
2836
  };
2716
2837
  var uploadAttachmentTool = {
2717
2838
  name: "upload_attachment",
2718
2839
  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.`,
2719
- inputSchema: inputSchema12,
2840
+ inputSchema: inputSchema15,
2720
2841
  handler: async (args, ctx) => {
2721
2842
  if (!isAbsolute2(args.file_path)) {
2722
2843
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
@@ -2840,7 +2961,7 @@ var uploadAttachmentTool = {
2840
2961
  };
2841
2962
 
2842
2963
  // src/server/tools/upload_spec_file.ts
2843
- import { z as z13 } from "zod";
2964
+ import { z as z16 } from "zod";
2844
2965
 
2845
2966
  // src/server/spec-path.ts
2846
2967
  var MAX_SPEC_DIR_DEPTH = 3;
@@ -2893,23 +3014,23 @@ var SPEC_FILE_TYPES = [
2893
3014
  "proposal",
2894
3015
  "openspec-spec"
2895
3016
  ];
2896
- var inputSchema13 = {
2897
- project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2898
- file_path: z13.string().min(1).describe(
3017
+ var inputSchema16 = {
3018
+ project_id: z16.string().min(1).describe("UUID of the project to upload the file into."),
3019
+ file_path: z16.string().min(1).describe(
2899
3020
  '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).'
2900
3021
  ),
2901
- content: z13.string().min(1).optional().describe(
3022
+ content: z16.string().min(1).optional().describe(
2902
3023
  "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
2903
3024
  ),
2904
- local_file_path: z13.string().min(1).optional().describe(
3025
+ local_file_path: z16.string().min(1).optional().describe(
2905
3026
  "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."
2906
3027
  ),
2907
- file_type: z13.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
3028
+ file_type: z16.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
2908
3029
  };
2909
3030
  var uploadSpecFileTool = {
2910
3031
  name: "upload_spec_file",
2911
3032
  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).",
2912
- inputSchema: inputSchema13,
3033
+ inputSchema: inputSchema16,
2913
3034
  handler: async (args, ctx) => {
2914
3035
  const pathError = validateSpecFilePath(args.file_path);
2915
3036
  if (pathError) {
@@ -2947,8 +3068,9 @@ function buildServer(deps) {
2947
3068
  });
2948
3069
  registerTool(server, listProjectsTool, deps.client);
2949
3070
  registerTool(server, getProjectTool, deps.client);
2950
- registerTool(server, getFileTool, deps.client);
2951
- registerTool(server, readFileTool, deps.client);
3071
+ registerTool(server, getSpecFileTool, deps.client);
3072
+ registerTool(server, readSpecFileTool, deps.client);
3073
+ registerTool(server, listSpecFileTool, deps.client);
2952
3074
  registerTool(server, getAttachmentTool, deps.client);
2953
3075
  registerTool(server, uploadAttachmentTool, deps.client);
2954
3076
  registerTool(server, createProjectTool, deps.client);
@@ -2958,6 +3080,8 @@ function buildServer(deps) {
2958
3080
  registerTool(server, deleteProjectTool, deps.client);
2959
3081
  registerTool(server, uploadSpecFileTool, deps.client);
2960
3082
  registerTool(server, updateSpecFileTool, deps.client);
3083
+ registerTool(server, moveSpecFileToTrashTool, deps.client);
3084
+ registerTool(server, restoreSpecFileFromTrashTool, deps.client);
2961
3085
  return server;
2962
3086
  }
2963
3087
  async function startStdioServer(deps) {
@@ -3111,7 +3235,7 @@ async function canonicaliseRoot(root) {
3111
3235
  import { promises as fs4 } from "fs";
3112
3236
  import { join as join2, relative } from "path";
3113
3237
  import { spawn } from "child_process";
3114
- import { z as z14 } from "zod";
3238
+ import { z as z17 } from "zod";
3115
3239
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3116
3240
 
3117
3241
  // src/reverse/ignore.ts
@@ -3361,37 +3485,37 @@ var READ_FILE_MAX_LINES = 2e3;
3361
3485
  var LIST_DIR_HARD_CAP = 5e3;
3362
3486
  var LIST_DIR_DEFAULT_CAP = 1e3;
3363
3487
  var GREP_MAX_MATCHES_DEFAULT = 200;
3364
- var listDirInput = z14.object({
3365
- path: z14.string(),
3366
- recursive: z14.boolean().optional(),
3367
- maxEntries: z14.number().int().positive().optional(),
3368
- includeIgnored: z14.boolean().optional()
3488
+ var listDirInput = z17.object({
3489
+ path: z17.string(),
3490
+ recursive: z17.boolean().optional(),
3491
+ maxEntries: z17.number().int().positive().optional(),
3492
+ includeIgnored: z17.boolean().optional()
3369
3493
  });
3370
- var readFileInput = z14.object({
3371
- path: z14.string(),
3372
- offset: z14.number().int().nonnegative().optional(),
3373
- limit: z14.number().int().positive().optional(),
3374
- includeIgnored: z14.boolean().optional()
3494
+ var readFileInput = z17.object({
3495
+ path: z17.string(),
3496
+ offset: z17.number().int().nonnegative().optional(),
3497
+ limit: z17.number().int().positive().optional(),
3498
+ includeIgnored: z17.boolean().optional()
3375
3499
  });
3376
- var grepInput = z14.object({
3377
- pattern: z14.string().describe(
3500
+ var grepInput = z17.object({
3501
+ pattern: z17.string().describe(
3378
3502
  "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."
3379
3503
  ),
3380
- path: z14.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3381
- isRegex: z14.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3382
- caseSensitive: z14.boolean().optional(),
3383
- maxMatches: z14.number().int().positive().optional(),
3384
- contextLines: z14.number().int().nonnegative().optional(),
3385
- includeIgnored: z14.boolean().optional()
3504
+ path: z17.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3505
+ isRegex: z17.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3506
+ caseSensitive: z17.boolean().optional(),
3507
+ maxMatches: z17.number().int().positive().optional(),
3508
+ contextLines: z17.number().int().nonnegative().optional(),
3509
+ includeIgnored: z17.boolean().optional()
3386
3510
  });
3387
- var packCodebaseInput = z14.object({
3388
- subpath: z14.string().optional(),
3389
- includePatterns: z14.string().optional(),
3390
- ignorePatterns: z14.string().optional()
3511
+ var packCodebaseInput = z17.object({
3512
+ subpath: z17.string().optional(),
3513
+ includePatterns: z17.string().optional(),
3514
+ ignorePatterns: z17.string().optional()
3391
3515
  });
3392
- var packCodebaseReadPageInput = z14.object({
3393
- outputId: z14.string(),
3394
- page: z14.number().int().positive()
3516
+ var packCodebaseReadPageInput = z17.object({
3517
+ outputId: z17.string(),
3518
+ page: z17.number().int().positive()
3395
3519
  });
3396
3520
  function structuredError(error, message, extra) {
3397
3521
  const payload = { error, message, ...extra ?? {} };
@@ -3919,7 +4043,7 @@ function jsonSchemaFromZod(schema) {
3919
4043
  return zodToJson(schema);
3920
4044
  }
3921
4045
  function zodToJson(schema) {
3922
- if (schema instanceof z14.ZodObject) {
4046
+ if (schema instanceof z17.ZodObject) {
3923
4047
  const shape = schema.shape;
3924
4048
  const properties = {};
3925
4049
  const required = [];
@@ -3934,19 +4058,19 @@ function zodToJson(schema) {
3934
4058
  if (required.length > 0) out.required = required;
3935
4059
  return out;
3936
4060
  }
3937
- if (schema instanceof z14.ZodOptional) {
4061
+ if (schema instanceof z17.ZodOptional) {
3938
4062
  return zodToJson(schema.unwrap());
3939
4063
  }
3940
- if (schema instanceof z14.ZodString) {
4064
+ if (schema instanceof z17.ZodString) {
3941
4065
  return { type: "string" };
3942
4066
  }
3943
- if (schema instanceof z14.ZodNumber) {
4067
+ if (schema instanceof z17.ZodNumber) {
3944
4068
  return { type: "number" };
3945
4069
  }
3946
- if (schema instanceof z14.ZodBoolean) {
4070
+ if (schema instanceof z17.ZodBoolean) {
3947
4071
  return { type: "boolean" };
3948
4072
  }
3949
- if (schema instanceof z14.ZodEnum) {
4073
+ if (schema instanceof z17.ZodEnum) {
3950
4074
  return { type: "string", enum: schema.options };
3951
4075
  }
3952
4076
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.2-next.38",
3
+ "version": "0.1.2-next.39",
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": {