@myspec/mcp-server 0.1.2-next.37 → 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 +370 -170
  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
@@ -99,7 +99,7 @@ function applyCorsHeaders(res) {
99
99
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
100
100
  }
101
101
  async function waitForOAuthCallback(opts) {
102
- return new Promise((resolve2, reject) => {
102
+ return new Promise((resolve3, reject) => {
103
103
  const settledFlag = { value: false };
104
104
  const timeoutRef = { handle: void 0 };
105
105
  const rlRef = { handle: void 0 };
@@ -157,7 +157,7 @@ async function waitForOAuthCallback(opts) {
157
157
  res.setHeader("Content-Type", "text/html");
158
158
  res.end(SUCCESS_HTML);
159
159
  settle(() => {
160
- resolve2({ code });
160
+ resolve3({ code });
161
161
  });
162
162
  });
163
163
  server.on("error", (err) => {
@@ -204,7 +204,7 @@ async function waitForOAuthCallback(opts) {
204
204
  }
205
205
  settle(() => {
206
206
  opts.logger("Captured pasted token; finishing sign-in...");
207
- resolve2({ code });
207
+ resolve3({ code });
208
208
  });
209
209
  });
210
210
  }
@@ -348,10 +348,10 @@ function createFileCredentialsStore(paths = {}) {
348
348
  async function loadOauthCreds(target) {
349
349
  try {
350
350
  if (process.platform !== "win32") {
351
- const stat = await fs.stat(target);
352
- if ((stat.mode & 63) !== 0) {
351
+ const stat2 = await fs.stat(target);
352
+ if ((stat2.mode & 63) !== 0) {
353
353
  throw new Error(
354
- `Refusing to load credentials from ${target}: file mode is too permissive (${(stat.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
354
+ `Refusing to load credentials from ${target}: file mode is too permissive (${(stat2.mode & 511).toString(8)}). Run \`chmod 600 ${target}\` and retry.`
355
355
  );
356
356
  }
357
357
  }
@@ -840,7 +840,7 @@ function calculateDelay(attempt, config) {
840
840
  return Math.round(cappedDelay * jitterMultiplier);
841
841
  }
842
842
  function sleep(ms) {
843
- return new Promise((resolve2) => setTimeout(resolve2, ms));
843
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
844
844
  }
845
845
  async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, shouldRetry = defaultShouldRetry) {
846
846
  const startTime = Date.now();
@@ -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,60 @@ 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";
2603
+
2604
+ // src/server/tools/spec-content-source.ts
2605
+ import { readFile, stat } from "fs/promises";
2606
+ import { isAbsolute, resolve } from "path";
2607
+ var MAX_LOCAL_SPEC_FILE_BYTES = 5 * 1024 * 1024;
2608
+ async function resolveSpecContent(input, maxBytes = MAX_LOCAL_SPEC_FILE_BYTES) {
2609
+ const { content, localFilePath } = input;
2610
+ if (content !== void 0 && localFilePath !== void 0) {
2611
+ return { error: "Provide either content or local_file_path, not both." };
2612
+ }
2613
+ if (content !== void 0) {
2614
+ return { bytes: new TextEncoder().encode(content) };
2615
+ }
2616
+ if (localFilePath === void 0) {
2617
+ return { error: "Provide the file body via content or local_file_path (one is required)." };
2618
+ }
2619
+ const absPath = isAbsolute(localFilePath) ? localFilePath : resolve(process.cwd(), localFilePath);
2620
+ let info;
2621
+ try {
2622
+ info = await stat(absPath);
2623
+ } catch (err) {
2624
+ const code = err.code;
2625
+ if (code === "ENOENT") {
2626
+ return { error: `local file not found: ${absPath}` };
2627
+ }
2628
+ const message = err instanceof Error ? err.message : String(err);
2629
+ return { error: `failed to read local file ${absPath}: ${message}` };
2630
+ }
2631
+ if (!info.isFile()) {
2632
+ return { error: `local_file_path is not a regular file: ${absPath}` };
2633
+ }
2634
+ if (info.size === 0) {
2635
+ return { error: `local file is empty: ${absPath}` };
2636
+ }
2637
+ if (info.size > maxBytes) {
2638
+ return {
2639
+ error: `local file is too large (${String(info.size)} bytes); max is ${String(maxBytes)} bytes.`
2640
+ };
2641
+ }
2642
+ let raw;
2643
+ try {
2644
+ raw = await readFile(absPath);
2645
+ } catch (err) {
2646
+ const message = err instanceof Error ? err.message : String(err);
2647
+ return { error: `failed to read local file ${absPath}: ${message}` };
2648
+ }
2649
+ if (raw.byteLength > maxBytes) {
2650
+ return {
2651
+ error: `local file is too large (${String(raw.byteLength)} bytes); max is ${String(maxBytes)} bytes.`
2652
+ };
2653
+ }
2654
+ return { bytes: new Uint8Array(raw) };
2655
+ }
2482
2656
 
2483
2657
  // src/server/tools/spec-file-output.ts
2484
2658
  function specFileSummary(file) {
@@ -2497,17 +2671,29 @@ function specFileSummary(file) {
2497
2671
  }
2498
2672
 
2499
2673
  // src/server/tools/update_spec_file.ts
2500
- var inputSchema11 = {
2501
- file_id: z11.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
2502
- project_id: z11.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
2503
- file_path: z11.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
2504
- content: z11.string().min(1).describe("Full UTF-8 replacement content (non-empty), saved as a new revision.")
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(
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."
2680
+ ),
2681
+ local_file_path: z14.string().min(1).optional().describe(
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."
2683
+ )
2505
2684
  };
2506
2685
  var updateSpecFileTool = {
2507
2686
  name: "update_spec_file",
2508
- description: "Overwrite an existing spec file by saving a new revision from full content. Reference the file by file_id or by (project_id + file_path).",
2509
- inputSchema: inputSchema11,
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).",
2688
+ inputSchema: inputSchema14,
2510
2689
  handler: async (args, ctx) => {
2690
+ const resolved = await resolveSpecContent({
2691
+ content: args.content,
2692
+ localFilePath: args.local_file_path
2693
+ });
2694
+ if (resolved.error || !resolved.bytes) {
2695
+ return errorResult(resolved.error ?? "Failed to resolve file content.");
2696
+ }
2511
2697
  let fileId;
2512
2698
  if (args.file_id) {
2513
2699
  const meta = await ctx.client.getFileMetadata(args.file_id).catch(() => null);
@@ -2526,17 +2712,16 @@ var updateSpecFileTool = {
2526
2712
  } else {
2527
2713
  return errorResult("Provide either file_id, or both project_id and file_path.");
2528
2714
  }
2529
- const content = new TextEncoder().encode(args.content);
2530
- const file = await ctx.client.saveSpecFileRevision(fileId, content);
2715
+ const file = await ctx.client.saveSpecFileRevision(fileId, resolved.bytes);
2531
2716
  return jsonResult({ file: specFileSummary(file) });
2532
2717
  }
2533
2718
  };
2534
2719
 
2535
2720
  // src/server/tools/upload_attachment.ts
2536
2721
  import { createHash as createHash2 } from "crypto";
2537
- import { lstat, readFile } from "fs/promises";
2538
- import { basename, extname, isAbsolute } from "path";
2539
- import { z as z12 } from "zod";
2722
+ import { lstat, readFile as readFile2 } from "fs/promises";
2723
+ import { basename, extname, isAbsolute as isAbsolute2 } from "path";
2724
+ import { z as z15 } from "zod";
2540
2725
  var DEFAULT_MIME = "application/octet-stream";
2541
2726
  var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
2542
2727
  var STRUCTURED_EXTENSION_MIME = {
@@ -2636,25 +2821,25 @@ function inferMimeFromName(name) {
2636
2821
  }
2637
2822
  return DEFAULT_MIME;
2638
2823
  }
2639
- var inputSchema12 = {
2640
- project_id: z12.string().min(1).describe("UUID of the project to attach the file to."),
2641
- 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(
2642
2827
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
2643
2828
  ),
2644
- file_name: z12.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
2645
- 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(
2646
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."
2647
2832
  ),
2648
- override: z12.boolean().optional().describe(
2833
+ override: z15.boolean().optional().describe(
2649
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)."
2650
2835
  )
2651
2836
  };
2652
2837
  var uploadAttachmentTool = {
2653
2838
  name: "upload_attachment",
2654
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.`,
2655
- inputSchema: inputSchema12,
2840
+ inputSchema: inputSchema15,
2656
2841
  handler: async (args, ctx) => {
2657
- if (!isAbsolute(args.file_path)) {
2842
+ if (!isAbsolute2(args.file_path)) {
2658
2843
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
2659
2844
  }
2660
2845
  let stats;
@@ -2690,7 +2875,7 @@ var uploadAttachmentTool = {
2690
2875
  }
2691
2876
  let buf;
2692
2877
  try {
2693
- buf = await readFile(args.file_path);
2878
+ buf = await readFile2(args.file_path);
2694
2879
  } catch (err) {
2695
2880
  const code = err.code;
2696
2881
  if (code === "EACCES" || code === "EPERM") {
@@ -2776,7 +2961,7 @@ var uploadAttachmentTool = {
2776
2961
  };
2777
2962
 
2778
2963
  // src/server/tools/upload_spec_file.ts
2779
- import { z as z13 } from "zod";
2964
+ import { z as z16 } from "zod";
2780
2965
 
2781
2966
  // src/server/spec-path.ts
2782
2967
  var MAX_SPEC_DIR_DEPTH = 3;
@@ -2794,15 +2979,16 @@ function validateSpecFilePath(filePath) {
2794
2979
  if (segments.some((s) => s === "" || s === ".")) {
2795
2980
  return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
2796
2981
  }
2797
- if (segments[0] !== "specs") {
2798
- return "file_path must be rooted at 'specs' (e.g. specs/changes/add-oauth-login/proposal.md).";
2982
+ const root = segments[0];
2983
+ if (root !== "specs" && root !== "openspec") {
2984
+ return "file_path must be rooted at 'specs' or 'openspec' (e.g. specs/changes/add-oauth-login/proposal.md).";
2799
2985
  }
2800
2986
  if (segments.length < 2) {
2801
- return "file_path must include a filename under 'specs' (e.g. specs/proposal.md).";
2987
+ return `file_path must include a filename under '${root}' (e.g. ${root}/proposal.md).`;
2802
2988
  }
2803
2989
  const dirLevels = segments.length - 2;
2804
2990
  if (dirLevels > MAX_SPEC_DIR_DEPTH) {
2805
- return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below specs.`;
2991
+ return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below the '${root}' root.`;
2806
2992
  }
2807
2993
  return null;
2808
2994
  }
@@ -2828,23 +3014,35 @@ var SPEC_FILE_TYPES = [
2828
3014
  "proposal",
2829
3015
  "openspec-spec"
2830
3016
  ];
2831
- var inputSchema13 = {
2832
- project_id: z13.string().min(1).describe("UUID of the project to upload the file into."),
2833
- file_path: z13.string().min(1).describe(
2834
- 'Path of the new spec file, rooted at "specs" with at most 3 directory levels below specs (e.g. specs/changes/add-oauth-login/proposal.md).'
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(
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).'
2835
3021
  ),
2836
- content: z13.string().min(1).describe("Full UTF-8 text content of the spec file (non-empty)."),
2837
- file_type: z13.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
3022
+ content: z16.string().min(1).optional().describe(
3023
+ "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
3024
+ ),
3025
+ local_file_path: z16.string().min(1).optional().describe(
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."
3027
+ ),
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).")
2838
3029
  };
2839
3030
  var uploadSpecFileTool = {
2840
3031
  name: "upload_spec_file",
2841
- description: "Upload a new, session-less spec file into a project from full content. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
2842
- inputSchema: inputSchema13,
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).",
3033
+ inputSchema: inputSchema16,
2843
3034
  handler: async (args, ctx) => {
2844
3035
  const pathError = validateSpecFilePath(args.file_path);
2845
3036
  if (pathError) {
2846
3037
  return errorResult(pathError);
2847
3038
  }
3039
+ const resolved = await resolveSpecContent({
3040
+ content: args.content,
3041
+ localFilePath: args.local_file_path
3042
+ });
3043
+ if (resolved.error || !resolved.bytes) {
3044
+ return errorResult(resolved.error ?? "Failed to resolve file content.");
3045
+ }
2848
3046
  const existing = await ctx.client.findSpecFileByPath(args.project_id, args.file_path);
2849
3047
  if (existing) {
2850
3048
  return errorResult(
@@ -2852,12 +3050,11 @@ var uploadSpecFileTool = {
2852
3050
  );
2853
3051
  }
2854
3052
  const fileType = args.file_type ?? deriveFileType(args.file_path);
2855
- const content = new TextEncoder().encode(args.content);
2856
3053
  const file = await ctx.client.uploadSpecFile({
2857
3054
  projectId: args.project_id,
2858
3055
  filePath: args.file_path,
2859
3056
  fileType,
2860
- content
3057
+ content: resolved.bytes
2861
3058
  });
2862
3059
  return jsonResult({ file: specFileSummary(file) });
2863
3060
  }
@@ -2871,8 +3068,9 @@ function buildServer(deps) {
2871
3068
  });
2872
3069
  registerTool(server, listProjectsTool, deps.client);
2873
3070
  registerTool(server, getProjectTool, deps.client);
2874
- registerTool(server, getFileTool, deps.client);
2875
- registerTool(server, readFileTool, deps.client);
3071
+ registerTool(server, getSpecFileTool, deps.client);
3072
+ registerTool(server, readSpecFileTool, deps.client);
3073
+ registerTool(server, listSpecFileTool, deps.client);
2876
3074
  registerTool(server, getAttachmentTool, deps.client);
2877
3075
  registerTool(server, uploadAttachmentTool, deps.client);
2878
3076
  registerTool(server, createProjectTool, deps.client);
@@ -2882,6 +3080,8 @@ function buildServer(deps) {
2882
3080
  registerTool(server, deleteProjectTool, deps.client);
2883
3081
  registerTool(server, uploadSpecFileTool, deps.client);
2884
3082
  registerTool(server, updateSpecFileTool, deps.client);
3083
+ registerTool(server, moveSpecFileToTrashTool, deps.client);
3084
+ registerTool(server, restoreSpecFileFromTrashTool, deps.client);
2885
3085
  return server;
2886
3086
  }
2887
3087
  async function startStdioServer(deps) {
@@ -2949,13 +3149,13 @@ var WebSocketClientTransport = class {
2949
3149
  return Promise.resolve();
2950
3150
  }
2951
3151
  send(message) {
2952
- return new Promise((resolve2, reject) => {
3152
+ return new Promise((resolve3, reject) => {
2953
3153
  this.ws.send(JSON.stringify(message), (err) => {
2954
3154
  if (err) {
2955
3155
  reject(err);
2956
3156
  return;
2957
3157
  }
2958
- resolve2();
3158
+ resolve3();
2959
3159
  });
2960
3160
  });
2961
3161
  }
@@ -2989,7 +3189,7 @@ var WebSocketClientTransport = class {
2989
3189
 
2990
3190
  // src/reverse/safe-path.ts
2991
3191
  import { realpath } from "fs/promises";
2992
- import { isAbsolute as isAbsolute2, resolve, sep } from "path";
3192
+ import { isAbsolute as isAbsolute3, resolve as resolve2, sep } from "path";
2993
3193
  var PathOutsideRootError = class extends Error {
2994
3194
  constructor(userPath) {
2995
3195
  super(`path_outside_root: ${userPath}`);
@@ -3007,10 +3207,10 @@ var AbsolutePathError = class extends Error {
3007
3207
  userPath;
3008
3208
  };
3009
3209
  async function resolveSafePath(realRoot, userPath, opts = {}) {
3010
- if (isAbsolute2(userPath)) {
3210
+ if (isAbsolute3(userPath)) {
3011
3211
  throw new AbsolutePathError(userPath);
3012
3212
  }
3013
- const joined = resolve(realRoot, userPath);
3213
+ const joined = resolve2(realRoot, userPath);
3014
3214
  let resolved;
3015
3215
  try {
3016
3216
  resolved = await realpath(joined);
@@ -3035,7 +3235,7 @@ async function canonicaliseRoot(root) {
3035
3235
  import { promises as fs4 } from "fs";
3036
3236
  import { join as join2, relative } from "path";
3037
3237
  import { spawn } from "child_process";
3038
- import { z as z14 } from "zod";
3238
+ import { z as z17 } from "zod";
3039
3239
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3040
3240
 
3041
3241
  // src/reverse/ignore.ts
@@ -3115,7 +3315,7 @@ function ripgrepIgnoreGlobs() {
3115
3315
 
3116
3316
  // src/reverse/pack-codebase.ts
3117
3317
  import { createHash as createHash3 } from "crypto";
3118
- import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
3318
+ import { mkdtemp, readFile as readFile3, rm } from "fs/promises";
3119
3319
  import { tmpdir } from "os";
3120
3320
  import { join } from "path";
3121
3321
  import { runCli } from "repomix";
@@ -3252,7 +3452,7 @@ async function invokeRepomix(exec, targetDir, args, timeoutMs) {
3252
3452
  const execPromise = exec(["."], targetDir, options);
3253
3453
  try {
3254
3454
  await withTimeout(execPromise, timeoutMs);
3255
- const content = await readFile2(outputFile, "utf-8");
3455
+ const content = await readFile3(outputFile, "utf-8");
3256
3456
  await rm(workDir, { recursive: true, force: true });
3257
3457
  return content;
3258
3458
  } catch (err) {
@@ -3285,37 +3485,37 @@ var READ_FILE_MAX_LINES = 2e3;
3285
3485
  var LIST_DIR_HARD_CAP = 5e3;
3286
3486
  var LIST_DIR_DEFAULT_CAP = 1e3;
3287
3487
  var GREP_MAX_MATCHES_DEFAULT = 200;
3288
- var listDirInput = z14.object({
3289
- path: z14.string(),
3290
- recursive: z14.boolean().optional(),
3291
- maxEntries: z14.number().int().positive().optional(),
3292
- 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()
3293
3493
  });
3294
- var readFileInput = z14.object({
3295
- path: z14.string(),
3296
- offset: z14.number().int().nonnegative().optional(),
3297
- limit: z14.number().int().positive().optional(),
3298
- 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()
3299
3499
  });
3300
- var grepInput = z14.object({
3301
- pattern: z14.string().describe(
3500
+ var grepInput = z17.object({
3501
+ pattern: z17.string().describe(
3302
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."
3303
3503
  ),
3304
- path: z14.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
3305
- isRegex: z14.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
3306
- caseSensitive: z14.boolean().optional(),
3307
- maxMatches: z14.number().int().positive().optional(),
3308
- contextLines: z14.number().int().nonnegative().optional(),
3309
- 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()
3310
3510
  });
3311
- var packCodebaseInput = z14.object({
3312
- subpath: z14.string().optional(),
3313
- includePatterns: z14.string().optional(),
3314
- ignorePatterns: z14.string().optional()
3511
+ var packCodebaseInput = z17.object({
3512
+ subpath: z17.string().optional(),
3513
+ includePatterns: z17.string().optional(),
3514
+ ignorePatterns: z17.string().optional()
3315
3515
  });
3316
- var packCodebaseReadPageInput = z14.object({
3317
- outputId: z14.string(),
3318
- page: z14.number().int().positive()
3516
+ var packCodebaseReadPageInput = z17.object({
3517
+ outputId: z17.string(),
3518
+ page: z17.number().int().positive()
3319
3519
  });
3320
3520
  function structuredError(error, message, extra) {
3321
3521
  const payload = { error, message, ...extra ?? {} };
@@ -3402,20 +3602,20 @@ async function handleRead(args, opts) {
3402
3602
  return structuredError("path_ignored", `Path matches ignore rule ${check.rule ?? ""}`);
3403
3603
  }
3404
3604
  }
3405
- let stat;
3605
+ let stat2;
3406
3606
  try {
3407
- stat = await fs4.stat(realPath);
3607
+ stat2 = await fs4.stat(realPath);
3408
3608
  } catch (err) {
3409
3609
  return structuredError("stat_failed", errorMessage(err));
3410
3610
  }
3411
- if (!stat.isFile()) {
3611
+ if (!stat2.isFile()) {
3412
3612
  return structuredError("not_a_file", `Not a regular file: ${args.path}`);
3413
3613
  }
3414
- if (stat.size > READ_FILE_MAX_BYTES) {
3614
+ if (stat2.size > READ_FILE_MAX_BYTES) {
3415
3615
  return structuredError(
3416
3616
  "file_too_large",
3417
- `File is ${String(stat.size)} bytes; the read cap is ${String(READ_FILE_MAX_BYTES)} bytes. Use local_fs_grep to find the lines you need, or local_pack_codebase to pull the whole tree.`,
3418
- { sizeBytes: stat.size, maxBytes: READ_FILE_MAX_BYTES }
3617
+ `File is ${String(stat2.size)} bytes; the read cap is ${String(READ_FILE_MAX_BYTES)} bytes. Use local_fs_grep to find the lines you need, or local_pack_codebase to pull the whole tree.`,
3618
+ { sizeBytes: stat2.size, maxBytes: READ_FILE_MAX_BYTES }
3419
3619
  );
3420
3620
  }
3421
3621
  let raw;
@@ -3843,7 +4043,7 @@ function jsonSchemaFromZod(schema) {
3843
4043
  return zodToJson(schema);
3844
4044
  }
3845
4045
  function zodToJson(schema) {
3846
- if (schema instanceof z14.ZodObject) {
4046
+ if (schema instanceof z17.ZodObject) {
3847
4047
  const shape = schema.shape;
3848
4048
  const properties = {};
3849
4049
  const required = [];
@@ -3858,19 +4058,19 @@ function zodToJson(schema) {
3858
4058
  if (required.length > 0) out.required = required;
3859
4059
  return out;
3860
4060
  }
3861
- if (schema instanceof z14.ZodOptional) {
4061
+ if (schema instanceof z17.ZodOptional) {
3862
4062
  return zodToJson(schema.unwrap());
3863
4063
  }
3864
- if (schema instanceof z14.ZodString) {
4064
+ if (schema instanceof z17.ZodString) {
3865
4065
  return { type: "string" };
3866
4066
  }
3867
- if (schema instanceof z14.ZodNumber) {
4067
+ if (schema instanceof z17.ZodNumber) {
3868
4068
  return { type: "number" };
3869
4069
  }
3870
- if (schema instanceof z14.ZodBoolean) {
4070
+ if (schema instanceof z17.ZodBoolean) {
3871
4071
  return { type: "boolean" };
3872
4072
  }
3873
- if (schema instanceof z14.ZodEnum) {
4073
+ if (schema instanceof z17.ZodEnum) {
3874
4074
  return { type: "string", enum: schema.options };
3875
4075
  }
3876
4076
  return { type: "string" };
@@ -4020,7 +4220,7 @@ async function runOneConnection(opts) {
4020
4220
  pongTimer = null;
4021
4221
  }
4022
4222
  });
4023
- return new Promise((resolve2) => {
4223
+ return new Promise((resolve3) => {
4024
4224
  ws.once("close", (code, reasonBuf) => {
4025
4225
  clearInterval(pingTimer);
4026
4226
  if (pongTimer) clearTimeout(pongTimer);
@@ -4030,7 +4230,7 @@ async function runOneConnection(opts) {
4030
4230
  `
4031
4231
  );
4032
4232
  const terminal = TERMINAL_CLOSE_CODES.has(code);
4033
- resolve2({
4233
+ resolve3({
4034
4234
  terminal,
4035
4235
  reason: `code=${String(code)} ${reasonStr}`,
4036
4236
  upMs: Date.now() - connectedAt,
@@ -4040,10 +4240,10 @@ async function runOneConnection(opts) {
4040
4240
  });
4041
4241
  }
4042
4242
  function waitForOpen(ws) {
4043
- return new Promise((resolve2, reject) => {
4243
+ return new Promise((resolve3, reject) => {
4044
4244
  const onOpen = () => {
4045
4245
  ws.removeListener("error", onError);
4046
- resolve2();
4246
+ resolve3();
4047
4247
  };
4048
4248
  const onError = (err) => {
4049
4249
  ws.removeListener("open", onOpen);
@@ -4054,7 +4254,7 @@ function waitForOpen(ws) {
4054
4254
  });
4055
4255
  }
4056
4256
  function sleep2(ms) {
4057
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4257
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4058
4258
  }
4059
4259
 
4060
4260
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.1.2-next.37",
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": {