@myspec/mcp-server 1.1.1 → 1.1.2-next.178

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 +1 -0
  2. package/dist/index.js +387 -264
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -339,6 +339,7 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
339
339
  | `read_spec_file` | `file_id`, optional `revision`, optional `offset`, optional `limit` | UTF-8 content paginated by line (`offset` is 1-based, default/cap 2000 lines, 1 MiB per response) with `content`, `start_line`, `end_line`, `read_lines`, `total_lines`, `truncated`, `next_offset`; plus `content_version` when reading the latest revision (errors for binary or files > 8 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT`, so paging through a file downloads it once |
340
340
  | `download_spec_file` | `file_id`, optional `destination_path`, optional `revision`, optional `overwrite` | Downloads the file's raw bytes to a local file — by default under `.specs/` in the working directory (see [Download destinations](#download-destinations)). 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`, `destination_source`) — **not** the content, so a large file can be saved locally and read in chunks |
341
341
  | `list_spec_file` | `project_id`, optional `path`, optional `trashed`, optional `limit`, optional `offset` | Lists a project's spec files (`file_id`, `file_path`, `file_type`, `file_size_bytes`, `revision_count`, `session_id`, `updated_at`, plus `trashed_at` on Trash Bin entries). Filter to a directory with `path` (exact file or everything under it; `""`/`"/"`/omit = all). `trashed=true` lists the Trash Bin instead of live files. Returns `total`, `limit`, `offset` and, when more records remain, `next_offset` (default 50, capped at 200). Ordered by `file_path` when `path` or `trashed` is given, most-recently-created first otherwise |
342
+ | `move_spec_folder` | `project_id`, `from_path`, `to_path` | Moves or renames a folder under `specs/` (a bundle, or a group folder holding bundles) with everything in it, e.g. `specs/mcp` → `specs/archive/mcp`. A bundle's files always move together; ids and contents stay, only paths change |
342
343
  | `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 |
343
344
  | `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
344
345
  | `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 |
package/dist/index.js CHANGED
@@ -2335,6 +2335,24 @@ function transformFileListResult(raw) {
2335
2335
  };
2336
2336
  }
2337
2337
 
2338
+ // ../../packages/platform-client/project/types/bundle.types.ts
2339
+ function transformBundleResponse(raw) {
2340
+ return {
2341
+ name: raw.name,
2342
+ sourceSessionId: raw.source_session_id,
2343
+ status: raw.status,
2344
+ completedAt: raw.completed_at ? new Date(raw.completed_at) : void 0,
2345
+ files: raw.files.map(transformSpecFileResponse)
2346
+ };
2347
+ }
2348
+ function transformSpecFolderMoveResponse(raw) {
2349
+ return {
2350
+ fromPath: raw.from_path,
2351
+ toPath: raw.to_path,
2352
+ files: raw.files.map(transformSpecFileResponse)
2353
+ };
2354
+ }
2355
+
2338
2356
  // ../../packages/platform-client/project/types/project.types.ts
2339
2357
  function toProjectType(raw) {
2340
2358
  return raw === "greenfield" || raw === "brownfield" ? raw : void 0;
@@ -2814,6 +2832,40 @@ var HttpAttachmentClient = class {
2814
2832
  }
2815
2833
  };
2816
2834
 
2835
+ // ../../packages/platform-client/project/http/bundle.http-client.ts
2836
+ var HttpBundleClient = class {
2837
+ httpClient;
2838
+ constructor(httpClient) {
2839
+ this.httpClient = httpClient;
2840
+ }
2841
+ async listBundles(projectId, jwtToken, opts) {
2842
+ const queryParams = new URLSearchParams();
2843
+ if (opts?.status) queryParams.set("status", opts.status);
2844
+ const query = queryParams.toString();
2845
+ const path4 = query ? `/project/v1/projects/${projectId}/bundles?${query}` : `/project/v1/projects/${projectId}/bundles`;
2846
+ const raw = await this.httpClient.get(path4, jwtToken, {});
2847
+ return raw.bundles.map(transformBundleResponse);
2848
+ }
2849
+ async renameBundle(projectId, req, jwtToken) {
2850
+ const raw = await this.httpClient.post(
2851
+ `/project/v1/projects/${projectId}/renameBundle`,
2852
+ { old_name: req.oldName, new_name: req.newName },
2853
+ jwtToken,
2854
+ {}
2855
+ );
2856
+ return transformBundleResponse(raw);
2857
+ }
2858
+ async moveSpecFolder(projectId, req, jwtToken) {
2859
+ const raw = await this.httpClient.post(
2860
+ `/project/v1/projects/${projectId}/moveSpecFolder`,
2861
+ { from_path: req.fromPath, to_path: req.toPath },
2862
+ jwtToken,
2863
+ {}
2864
+ );
2865
+ return transformSpecFolderMoveResponse(raw);
2866
+ }
2867
+ };
2868
+
2817
2869
  // ../../packages/platform-client/project/http/file.http-client.ts
2818
2870
  var HEADER_EXPECTED_VERSION = "X-Expected-Version";
2819
2871
  var HEADER_ACTOR_KIND = "X-Actor-Kind";
@@ -4086,6 +4138,16 @@ var PlatformClient = class {
4086
4138
  async trashFile(fileId) {
4087
4139
  await this.withTokenRetry((jwt, c) => c.file.trashFile(fileId, jwt, { actorKind: "ai" }));
4088
4140
  }
4141
+ /**
4142
+ * Moves a folder under `specs/` (a bundle, or a group folder holding
4143
+ * bundles) and everything in it to a new path. Paths include the `specs/`
4144
+ * root. Only file paths change: ids, contents and storage stay put.
4145
+ */
4146
+ async moveSpecFolder(projectId, fromPath, toPath) {
4147
+ return this.withTokenRetry(
4148
+ (jwt, c) => c.bundle.moveSpecFolder(projectId, { fromPath, toPath }, jwt)
4149
+ );
4150
+ }
4089
4151
  /** Restores a spec file from the Trash Bin back to the active file list. */
4090
4152
  async restoreFile(fileId) {
4091
4153
  await this.withTokenRetry((jwt, c) => c.file.restoreFile(fileId, jwt));
@@ -4186,6 +4248,7 @@ var PlatformClient = class {
4186
4248
  const httpClient = new BaseHttpClient({ baseUrl, logger: silentLogger });
4187
4249
  return {
4188
4250
  project: new HttpProjectClient(httpClient),
4251
+ bundle: new HttpBundleClient(httpClient),
4189
4252
  session: new HttpSpecSessionClient(httpClient),
4190
4253
  file: new HttpFileClient(httpClient),
4191
4254
  attachment: new HttpAttachmentClient(httpClient),
@@ -5336,9 +5399,114 @@ var moveSpecFileToTrashTool = {
5336
5399
  }
5337
5400
  };
5338
5401
 
5339
- // src/server/tools/read_attachment.ts
5402
+ // src/server/tools/move_spec_folder.ts
5340
5403
  import { z as z16 } from "zod";
5341
5404
 
5405
+ // src/server/spec-path.ts
5406
+ var MAX_SPEC_DIR_DEPTH = 3;
5407
+ function validateSpecFilePath(filePath) {
5408
+ if (!filePath) {
5409
+ return "file_path is required.";
5410
+ }
5411
+ if (filePath.startsWith("/")) {
5412
+ return "file_path must be a relative path, not an absolute path.";
5413
+ }
5414
+ const segments = filePath.split("/");
5415
+ if (segments.some((s) => s === "..")) {
5416
+ return "file_path must not contain '..' (parent directory) segments.";
5417
+ }
5418
+ if (segments.some((s) => s === "" || s === ".")) {
5419
+ return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
5420
+ }
5421
+ const root = segments[0];
5422
+ if (root !== "specs" && root !== "openspec") {
5423
+ return "file_path must be rooted at 'specs' or 'openspec' (e.g. specs/changes/add-oauth-login/proposal.md).";
5424
+ }
5425
+ if (segments.length < 2) {
5426
+ return `file_path must include a filename under '${root}' (e.g. ${root}/proposal.md).`;
5427
+ }
5428
+ const dirLevels = segments.length - 2;
5429
+ if (dirLevels > MAX_SPEC_DIR_DEPTH) {
5430
+ return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below the '${root}' root.`;
5431
+ }
5432
+ return null;
5433
+ }
5434
+ function validateSpecFolderPath(field, folderPath) {
5435
+ const path4 = folderPath.endsWith("/") ? folderPath.slice(0, -1) : folderPath;
5436
+ if (!path4) {
5437
+ return { error: `${field} is required.` };
5438
+ }
5439
+ if (path4.startsWith("/")) {
5440
+ return { error: `${field} must be a relative path, not an absolute path.` };
5441
+ }
5442
+ const segments = path4.split("/");
5443
+ if (segments.some((s) => s === "..")) {
5444
+ return { error: `${field} must not contain '..' (parent directory) segments.` };
5445
+ }
5446
+ if (segments.some((s) => s === "" || s === ".")) {
5447
+ return { error: `${field} must be a normalized path (no empty or "." segments).` };
5448
+ }
5449
+ if (segments[0] === "openspec") {
5450
+ return {
5451
+ error: `${field}: OpenSpec folders cannot be moved; the OpenSpec CLI relies on the change id.`
5452
+ };
5453
+ }
5454
+ if (segments[0] !== "specs" || segments.length < 2) {
5455
+ return { error: `${field} must be a folder under specs/ (e.g. specs/group/my-bundle).` };
5456
+ }
5457
+ if (segments.length - 1 > MAX_SPEC_DIR_DEPTH) {
5458
+ return {
5459
+ error: `${field} must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below 'specs'.`
5460
+ };
5461
+ }
5462
+ return { path: path4 };
5463
+ }
5464
+ var FILE_TYPE_BY_BASENAME = {
5465
+ constitution: "constitution",
5466
+ requirements: "requirements",
5467
+ solution: "solution",
5468
+ tasks: "tasks",
5469
+ // Enterprise greenfield quality plan (generate-greenfield-enterprise).
5470
+ quality: "quality",
5471
+ proposal: "proposal"
5472
+ };
5473
+ function deriveFileType(filePath) {
5474
+ const filename = filePath.split("/").pop() ?? "";
5475
+ const base = filename.replace(/\.[^.]+$/, "").toLowerCase();
5476
+ return FILE_TYPE_BY_BASENAME[base] ?? "openspec-spec";
5477
+ }
5478
+
5479
+ // src/server/tools/move_spec_folder.ts
5480
+ var inputSchema16 = {
5481
+ project_id: z16.string().min(1).describe("UUID of the project holding the folder."),
5482
+ from_path: z16.string().min(1).describe('Folder to move, under "specs" (e.g. specs/mcp or specs/mcp/my-bundle).'),
5483
+ to_path: z16.string().min(1).describe(
5484
+ 'New folder path: "specs" plus 1-3 lowercase kebab-case segments (e.g. specs/archive/mcp). Under the same parent, this renames the folder.'
5485
+ )
5486
+ };
5487
+ var moveSpecFolderTool = {
5488
+ name: "move_spec_folder",
5489
+ description: "Move or rename a folder under specs/ together with everything in it. The folder can be a bundle (it holds spec files directly) or a group folder holding bundles. The files of a bundle always move together; single files cannot be moved out of a bundle. File ids, contents and revisions stay the same; only paths change, so address the files by their new paths afterwards. Refused: OpenSpec folders, specs/memory, a target inside the folder itself, a target that already exists (Trash included) or sits inside a bundle, a folder inside a bundle (move the bundle instead), a folder whose bundle is still generating, and a move that would nest a bundle more than 3 levels below specs/.",
5490
+ inputSchema: inputSchema16,
5491
+ handler: async (args, ctx) => {
5492
+ const from = validateSpecFolderPath("from_path", args.from_path);
5493
+ if (from.error !== void 0) return errorResult(from.error);
5494
+ const to = validateSpecFolderPath("to_path", args.to_path);
5495
+ if (to.error !== void 0) return errorResult(to.error);
5496
+ const moved = await ctx.client.moveSpecFolder(args.project_id, from.path, to.path);
5497
+ return jsonResult({
5498
+ moved: true,
5499
+ from_path: moved.fromPath,
5500
+ to_path: moved.toPath,
5501
+ file_count: moved.files.length,
5502
+ files: moved.files.map((f) => ({ id: f.id, file_path: f.filePath }))
5503
+ });
5504
+ }
5505
+ };
5506
+
5507
+ // src/server/tools/read_attachment.ts
5508
+ import { z as z17 } from "zod";
5509
+
5342
5510
  // src/util/paginate-lines.ts
5343
5511
  var MAX_LINES_PER_CALL = 2e3;
5344
5512
  var MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
@@ -5449,17 +5617,17 @@ var TEXT_MIME_TYPES = /* @__PURE__ */ new Set([
5449
5617
  "image/svg+xml"
5450
5618
  ]);
5451
5619
  var GENERIC_MIME_TYPES = /* @__PURE__ */ new Set(["application/octet-stream", ""]);
5452
- var inputSchema16 = {
5453
- attachment_id: z16.string().min(1).describe("UUID of the attachment to read"),
5454
- offset: z16.number().int().nonnegative().optional().describe(
5620
+ var inputSchema17 = {
5621
+ attachment_id: z17.string().min(1).describe("UUID of the attachment to read"),
5622
+ offset: z17.number().int().nonnegative().optional().describe(
5455
5623
  "1-based line number to start reading from (text attachments only; default 1; 0 is an alias for 1)."
5456
5624
  ),
5457
- limit: z16.number().int().positive().optional().describe("Max lines to return (text attachments only; default 2000, capped at 2000).")
5625
+ limit: z17.number().int().positive().optional().describe("Max lines to return (text attachments only; default 2000, capped at 2000).")
5458
5626
  };
5459
5627
  var readAttachmentTool = {
5460
5628
  name: "read_attachment",
5461
5629
  description: `Return an attachment's content inline. UTF-8 text is paginated by line (offset/limit, 1 MiB per response). Images (png, jpeg, gif, webp) are returned whole as an MCP image block \u2014 never paginated \u2014 up to the ${String(MAX_ATTACHMENT_MB)} MiB attachment limit; a 10 MiB image is roughly 13.4 MiB of base64, so check file_size_bytes (from get_attachment or list_attachments) before reading a large one. Other binary types (pdf, docx, xlsx, \u2026) cannot be read here; view them in the MySpec webapp.`,
5462
- inputSchema: inputSchema16,
5630
+ inputSchema: inputSchema17,
5463
5631
  handler: async (args, ctx) => {
5464
5632
  const meta = await ctx.client.getAttachmentMetadata(args.attachment_id);
5465
5633
  if (!meta) {
@@ -5516,18 +5684,18 @@ var readAttachmentTool = {
5516
5684
  // src/server/tools/read_spec_file.ts
5517
5685
  import { promises as fs3 } from "fs";
5518
5686
  import { createHash as createHash2 } from "crypto";
5519
- import { z as z17 } from "zod";
5687
+ import { z as z18 } from "zod";
5520
5688
  var MAX_READABLE_SPEC_FILE_BYTES = 8 * 1024 * 1024;
5521
- var inputSchema17 = {
5522
- file_id: z17.string().min(1).describe("UUID of the file"),
5523
- revision: z17.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision)."),
5524
- offset: z17.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
5525
- limit: z17.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
5689
+ var inputSchema18 = {
5690
+ file_id: z18.string().min(1).describe("UUID of the file"),
5691
+ revision: z18.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision)."),
5692
+ offset: z18.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
5693
+ limit: z18.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
5526
5694
  };
5527
5695
  var readSpecFileTool = {
5528
5696
  name: "read_spec_file",
5529
5697
  description: "Return a window of a spec file's UTF-8 content, paginated by line (offset/limit, default and cap 2000 lines, 1 MiB per response). Large files are read across multiple calls using `next_offset`. 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, so paging through a file downloads it once. Files above 8 MiB and binary content are refused \u2014 use download_spec_file to save those to a local file instead.",
5530
- inputSchema: inputSchema17,
5698
+ inputSchema: inputSchema18,
5531
5699
  handler: async (args, ctx) => {
5532
5700
  const meta = await ctx.client.getFileMetadata(args.file_id);
5533
5701
  const targetRevision = args.revision ?? meta.revisionCount;
@@ -5657,17 +5825,17 @@ function sha256Prefixed2(content) {
5657
5825
  }
5658
5826
 
5659
5827
  // src/server/tools/rename_spec_session.ts
5660
- import { z as z18 } from "zod";
5661
- var inputSchema18 = {
5662
- session_id: z18.string().min(1).describe("UUID of the spec session to rename"),
5663
- session_summary: z18.string().min(1).max(SESSION_SUMMARY_MAX_LENGTH).describe(
5828
+ import { z as z19 } from "zod";
5829
+ var inputSchema19 = {
5830
+ session_id: z19.string().min(1).describe("UUID of the spec session to rename"),
5831
+ session_summary: z19.string().min(1).max(SESSION_SUMMARY_MAX_LENGTH).describe(
5664
5832
  `New human-readable session title (max ${String(SESSION_SUMMARY_MAX_LENGTH)} characters). Stored as context.sessionSummary \u2014 the field the webapp displays and searches.`
5665
5833
  )
5666
5834
  };
5667
5835
  var renameSpecSessionTool = {
5668
5836
  name: "rename_spec_session",
5669
5837
  description: "Rename a spec session by updating its context.sessionSummary \u2014 the session's human-readable title (there is no first-class name field). Permitted on both active and archived sessions. All other context keys are preserved untouched.",
5670
- inputSchema: inputSchema18,
5838
+ inputSchema: inputSchema19,
5671
5839
  handler: async (args, ctx) => {
5672
5840
  if (args.session_summary.length > SESSION_SUMMARY_MAX_LENGTH) {
5673
5841
  return errorResult(
@@ -5701,14 +5869,14 @@ var renameSpecSessionTool = {
5701
5869
  };
5702
5870
 
5703
5871
  // src/server/tools/restore_spec_file_from_trash.ts
5704
- import { z as z19 } from "zod";
5705
- var inputSchema19 = {
5706
- file_id: z19.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
5872
+ import { z as z20 } from "zod";
5873
+ var inputSchema20 = {
5874
+ file_id: z20.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
5707
5875
  };
5708
5876
  var restoreSpecFileFromTrashTool = {
5709
5877
  name: "restore_spec_file_from_trash",
5710
5878
  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.",
5711
- inputSchema: inputSchema19,
5879
+ inputSchema: inputSchema20,
5712
5880
  handler: async (args, ctx) => {
5713
5881
  await ctx.client.restoreFile(args.file_id);
5714
5882
  return jsonResult({ restored: true, file_id: args.file_id });
@@ -5716,14 +5884,14 @@ var restoreSpecFileFromTrashTool = {
5716
5884
  };
5717
5885
 
5718
5886
  // src/server/tools/unarchive_project.ts
5719
- import { z as z20 } from "zod";
5720
- var inputSchema20 = {
5721
- project_id: z20.string().min(1).describe("UUID of the project to unarchive.")
5887
+ import { z as z21 } from "zod";
5888
+ var inputSchema21 = {
5889
+ project_id: z21.string().min(1).describe("UUID of the project to unarchive.")
5722
5890
  };
5723
5891
  var unarchiveProjectTool = {
5724
5892
  name: "unarchive_project",
5725
5893
  description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
5726
- inputSchema: inputSchema20,
5894
+ inputSchema: inputSchema21,
5727
5895
  handler: async (args, ctx) => {
5728
5896
  const project = await ctx.client.unarchiveProject(args.project_id);
5729
5897
  return jsonResult({ project: projectSummary(project) });
@@ -5731,14 +5899,14 @@ var unarchiveProjectTool = {
5731
5899
  };
5732
5900
 
5733
5901
  // src/server/tools/unarchive_spec_session.ts
5734
- import { z as z21 } from "zod";
5735
- var inputSchema21 = {
5736
- session_id: z21.string().min(1).describe("UUID of the spec session to unarchive")
5902
+ import { z as z22 } from "zod";
5903
+ var inputSchema22 = {
5904
+ session_id: z22.string().min(1).describe("UUID of the spec session to unarchive")
5737
5905
  };
5738
5906
  var unarchiveSpecSessionTool = {
5739
5907
  name: "unarchive_spec_session",
5740
5908
  description: "Unarchive a spec session, returning it to the active list.",
5741
- inputSchema: inputSchema21,
5909
+ inputSchema: inputSchema22,
5742
5910
  handler: async (args, ctx) => {
5743
5911
  let session;
5744
5912
  try {
@@ -5755,19 +5923,19 @@ var unarchiveSpecSessionTool = {
5755
5923
  };
5756
5924
 
5757
5925
  // src/server/tools/update_project.ts
5758
- import { z as z22 } from "zod";
5759
- var inputSchema22 = {
5760
- project_id: z22.string().min(1).describe("UUID of the project to update."),
5761
- name: z22.string().min(1).optional().describe("New project name."),
5762
- description: z22.string().optional().describe("New project description."),
5763
- project_type: z22.enum(["greenfield", "brownfield", "auto"]).optional().describe(
5926
+ import { z as z23 } from "zod";
5927
+ var inputSchema23 = {
5928
+ project_id: z23.string().min(1).describe("UUID of the project to update."),
5929
+ name: z23.string().min(1).optional().describe("New project name."),
5930
+ description: z23.string().optional().describe("New project description."),
5931
+ project_type: z23.enum(["greenfield", "brownfield", "auto"]).optional().describe(
5764
5932
  "Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
5765
5933
  )
5766
5934
  };
5767
5935
  var updateProjectTool = {
5768
5936
  name: "update_project",
5769
5937
  description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
5770
- inputSchema: inputSchema22,
5938
+ inputSchema: inputSchema23,
5771
5939
  handler: async (args, ctx) => {
5772
5940
  if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
5773
5941
  return errorResult(
@@ -5790,7 +5958,7 @@ var updateProjectTool = {
5790
5958
  };
5791
5959
 
5792
5960
  // src/server/tools/update_spec_file.ts
5793
- import { z as z23 } from "zod";
5961
+ import { z as z24 } from "zod";
5794
5962
 
5795
5963
  // src/server/tools/spec-content-source.ts
5796
5964
  import { readFile, stat } from "fs/promises";
@@ -5846,24 +6014,24 @@ async function resolveSpecContent(input, maxBytes = MAX_LOCAL_SPEC_FILE_BYTES) {
5846
6014
  }
5847
6015
 
5848
6016
  // src/server/tools/update_spec_file.ts
5849
- var inputSchema23 = {
5850
- file_id: z23.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
5851
- project_id: z23.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
5852
- file_path: z23.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
5853
- content: z23.string().min(1).optional().describe(
6017
+ var inputSchema24 = {
6018
+ file_id: z24.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
6019
+ project_id: z24.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
6020
+ file_path: z24.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
6021
+ content: z24.string().min(1).optional().describe(
5854
6022
  "Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
5855
6023
  ),
5856
- local_file_path: z23.string().min(1).optional().describe(
6024
+ local_file_path: z24.string().min(1).optional().describe(
5857
6025
  "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."
5858
6026
  ),
5859
- expected_version: z23.number().int().nonnegative().optional().describe(
6027
+ expected_version: z24.number().int().nonnegative().optional().describe(
5860
6028
  "The content_version the new body was based on, from the get_spec_file or read_spec_file you worked from. The update is rejected with an actionable error if someone else changed the file since. Omit only when overwriting unconditionally is genuinely intended \u2014 the project may be configured to reject unversioned updates."
5861
6029
  )
5862
6030
  };
5863
6031
  var updateSpecFileTool = {
5864
6032
  name: "update_spec_file",
5865
6033
  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). Pass `expected_version` from the get_spec_file you based the new body on so a concurrent edit by someone else is reported instead of silently overwritten.",
5866
- inputSchema: inputSchema23,
6034
+ inputSchema: inputSchema24,
5867
6035
  handler: async (args, ctx) => {
5868
6036
  const resolved = await resolveSpecContent({
5869
6037
  content: args.content,
@@ -5925,7 +6093,7 @@ function conflictMessage(filePath, conflict) {
5925
6093
  }
5926
6094
 
5927
6095
  // src/server/tools/spec_file_comments.ts
5928
- import { z as z24 } from "zod";
6096
+ import { z as z25 } from "zod";
5929
6097
  var LIST_BODY_PREVIEW_CHARS = 1e3;
5930
6098
  var LIST_MAX_THREADS = 100;
5931
6099
  function commentSummary(comment, opts = {}) {
@@ -5971,7 +6139,7 @@ function threadSummary(thread, opts = {}) {
5971
6139
  };
5972
6140
  }
5973
6141
  function uuidField(what) {
5974
- return z24.uuid().describe(what);
6142
+ return z25.uuid().describe(what);
5975
6143
  }
5976
6144
  var CONFLICT_REASON = {
5977
6145
  fileTrashed: "file_trashed",
@@ -5995,7 +6163,7 @@ function conflictAdvice(err, parts) {
5995
6163
  }
5996
6164
  var listInputSchema = {
5997
6165
  file_id: uuidField("UUID of the spec file whose comments to list."),
5998
- status: z24.enum(["open", "resolved", "orphaned"]).optional().describe(
6166
+ status: z25.enum(["open", "resolved", "orphaned"]).optional().describe(
5999
6167
  "Narrow to open (unresolved) threads, resolved ones, or orphaned ones whose passage can no longer be located. Omit for all of them."
6000
6168
  )
6001
6169
  };
@@ -6019,19 +6187,19 @@ var listSpecFileCommentsTool = {
6019
6187
  };
6020
6188
  var createInputSchema = {
6021
6189
  file_id: uuidField("UUID of the spec file to comment on."),
6022
- body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The comment text, in markdown."),
6190
+ body: z25.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The comment text, in markdown."),
6023
6191
  // The three anchor limits are exported by the contract for exactly this, and
6024
6192
  // the context one is SHORT (64). A description reading "the text immediately
6025
6193
  // BEFORE the quote" invites a whole preceding sentence, which is the default
6026
6194
  // reading rather than a corner case — so the ceiling is enforced in the
6027
6195
  // schema, where the model sees it, rather than discovered as a 400.
6028
- anchor_quote: z24.string().min(1).max(MAX_ANCHOR_QUOTE_LENGTH).describe(
6196
+ anchor_quote: z25.string().min(1).max(MAX_ANCHOR_QUOTE_LENGTH).describe(
6029
6197
  `The exact passage this comment is about, copied verbatim from the file. The server searches the current content for it, so it must match the text as written. Read the file with read_spec_file first. At most ${String(MAX_ANCHOR_QUOTE_LENGTH)} characters.`
6030
6198
  ),
6031
- anchor_prefix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
6199
+ anchor_prefix: z25.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
6032
6200
  `The text immediately BEFORE the quote, which disambiguates a passage appearing more than once. At most ${String(MAX_ANCHOR_CONTEXT_LENGTH)} characters \u2014 a few words, not a sentence. Send an empty string when the quote starts the file.`
6033
6201
  ),
6034
- anchor_suffix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
6202
+ anchor_suffix: z25.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
6035
6203
  `The text immediately AFTER the quote. At most ${String(MAX_ANCHOR_CONTEXT_LENGTH)} characters. Empty when the quote ends the file.`
6036
6204
  )
6037
6205
  };
@@ -6072,7 +6240,7 @@ var createSpecFileCommentThreadTool = {
6072
6240
  var replyInputSchema = {
6073
6241
  file_id: uuidField("UUID of the spec file the thread belongs to."),
6074
6242
  thread_id: uuidField("UUID of the thread to reply to, from list_spec_file_comments."),
6075
- body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The reply text, in markdown.")
6243
+ body: z25.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The reply text, in markdown.")
6076
6244
  };
6077
6245
  var replySpecFileCommentThreadTool = {
6078
6246
  name: "reply_spec_file_comment_thread",
@@ -6102,8 +6270,8 @@ var replySpecFileCommentThreadTool = {
6102
6270
  var updateInputSchema = {
6103
6271
  file_id: uuidField("UUID of the spec file the comment belongs to."),
6104
6272
  comment_id: uuidField("UUID of the comment to edit. Must be your own."),
6105
- body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The replacement text, in markdown. Replaces the comment entirely."),
6106
- expected_version: z24.number().int().nonnegative().optional().describe(
6273
+ body: z25.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The replacement text, in markdown. Replaces the comment entirely."),
6274
+ expected_version: z25.number().int().nonnegative().optional().describe(
6107
6275
  "The COMMENT's own content_version as last read by list_spec_file_comments \u2014 not the file's. Sending it makes the edit fail rather than silently overwrite one made since."
6108
6276
  )
6109
6277
  };
@@ -6162,7 +6330,7 @@ var deleteSpecFileCommentTool = {
6162
6330
  import { createHash as createHash3 } from "crypto";
6163
6331
  import { lstat, readFile as readFile2 } from "fs/promises";
6164
6332
  import { basename, extname, isAbsolute as isAbsolute2 } from "path";
6165
- import { z as z25 } from "zod";
6333
+ import { z as z26 } from "zod";
6166
6334
  var DEFAULT_MIME = "application/octet-stream";
6167
6335
  var MAX_ATTACHMENT_MB2 = MAX_ATTACHMENT_BYTES / (1024 * 1024);
6168
6336
  var STRUCTURED_EXTENSION_MIME = {
@@ -6267,23 +6435,23 @@ function inferMimeFromName(name) {
6267
6435
  }
6268
6436
  return DEFAULT_MIME;
6269
6437
  }
6270
- var inputSchema24 = {
6271
- project_id: z25.string().min(1).describe("UUID of the project to attach the file to."),
6272
- file_path: z25.string().min(1).describe(
6438
+ var inputSchema25 = {
6439
+ project_id: z26.string().min(1).describe("UUID of the project to attach the file to."),
6440
+ file_path: z26.string().min(1).describe(
6273
6441
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
6274
6442
  ),
6275
- file_name: z25.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
6276
- mime_type: z25.string().min(1).optional().describe(
6443
+ file_name: z26.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
6444
+ mime_type: z26.string().min(1).optional().describe(
6277
6445
  "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."
6278
6446
  ),
6279
- override: z25.boolean().optional().describe(
6447
+ override: z26.boolean().optional().describe(
6280
6448
  "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)."
6281
6449
  )
6282
6450
  };
6283
6451
  var uploadAttachmentTool = {
6284
6452
  name: "upload_attachment",
6285
6453
  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_MB2)} MB. Supported content: PDF, DOCX, XLSX, images (PNG, JPEG, GIF, WEBP), and any UTF-8 text file (XML, JSON, YAML, HTML, SVG, Markdown, CSV, source code, ...). Other binary files are rejected by the platform.`,
6286
- inputSchema: inputSchema24,
6454
+ inputSchema: inputSchema25,
6287
6455
  handler: async (args, ctx) => {
6288
6456
  if (!isAbsolute2(args.file_path)) {
6289
6457
  return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
@@ -6407,53 +6575,7 @@ var uploadAttachmentTool = {
6407
6575
  };
6408
6576
 
6409
6577
  // src/server/tools/upload_spec_file.ts
6410
- import { z as z26 } from "zod";
6411
-
6412
- // src/server/spec-path.ts
6413
- var MAX_SPEC_DIR_DEPTH = 3;
6414
- function validateSpecFilePath(filePath) {
6415
- if (!filePath) {
6416
- return "file_path is required.";
6417
- }
6418
- if (filePath.startsWith("/")) {
6419
- return "file_path must be a relative path, not an absolute path.";
6420
- }
6421
- const segments = filePath.split("/");
6422
- if (segments.some((s) => s === "..")) {
6423
- return "file_path must not contain '..' (parent directory) segments.";
6424
- }
6425
- if (segments.some((s) => s === "" || s === ".")) {
6426
- return 'file_path must be a normalized path (no empty, "." or trailing-slash segments).';
6427
- }
6428
- const root = segments[0];
6429
- if (root !== "specs" && root !== "openspec") {
6430
- return "file_path must be rooted at 'specs' or 'openspec' (e.g. specs/changes/add-oauth-login/proposal.md).";
6431
- }
6432
- if (segments.length < 2) {
6433
- return `file_path must include a filename under '${root}' (e.g. ${root}/proposal.md).`;
6434
- }
6435
- const dirLevels = segments.length - 2;
6436
- if (dirLevels > MAX_SPEC_DIR_DEPTH) {
6437
- return `file_path must not nest deeper than ${String(MAX_SPEC_DIR_DEPTH)} directory levels below the '${root}' root.`;
6438
- }
6439
- return null;
6440
- }
6441
- var FILE_TYPE_BY_BASENAME = {
6442
- constitution: "constitution",
6443
- requirements: "requirements",
6444
- solution: "solution",
6445
- tasks: "tasks",
6446
- // Enterprise greenfield quality plan (generate-greenfield-enterprise).
6447
- quality: "quality",
6448
- proposal: "proposal"
6449
- };
6450
- function deriveFileType(filePath) {
6451
- const filename = filePath.split("/").pop() ?? "";
6452
- const base = filename.replace(/\.[^.]+$/, "").toLowerCase();
6453
- return FILE_TYPE_BY_BASENAME[base] ?? "openspec-spec";
6454
- }
6455
-
6456
- // src/server/tools/upload_spec_file.ts
6578
+ import { z as z27 } from "zod";
6457
6579
  var SPEC_FILE_TYPES = [
6458
6580
  "constitution",
6459
6581
  "requirements",
@@ -6463,23 +6585,23 @@ var SPEC_FILE_TYPES = [
6463
6585
  "proposal",
6464
6586
  "openspec-spec"
6465
6587
  ];
6466
- var inputSchema25 = {
6467
- project_id: z26.string().min(1).describe("UUID of the project to upload the file into."),
6468
- file_path: z26.string().min(1).describe(
6588
+ var inputSchema26 = {
6589
+ project_id: z27.string().min(1).describe("UUID of the project to upload the file into."),
6590
+ file_path: z27.string().min(1).describe(
6469
6591
  '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).'
6470
6592
  ),
6471
- content: z26.string().min(1).optional().describe(
6593
+ content: z27.string().min(1).optional().describe(
6472
6594
  "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
6473
6595
  ),
6474
- local_file_path: z26.string().min(1).optional().describe(
6596
+ local_file_path: z27.string().min(1).optional().describe(
6475
6597
  "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."
6476
6598
  ),
6477
- file_type: z26.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
6599
+ file_type: z27.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
6478
6600
  };
6479
6601
  var uploadSpecFileTool = {
6480
6602
  name: "upload_spec_file",
6481
6603
  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).",
6482
- inputSchema: inputSchema25,
6604
+ inputSchema: inputSchema26,
6483
6605
  handler: async (args, ctx) => {
6484
6606
  const pathError = validateSpecFilePath(args.file_path);
6485
6607
  if (pathError) {
@@ -6510,16 +6632,16 @@ var uploadSpecFileTool = {
6510
6632
  };
6511
6633
 
6512
6634
  // src/server/tools/list_artifacts.ts
6513
- import { z as z28 } from "zod";
6635
+ import { z as z29 } from "zod";
6514
6636
 
6515
6637
  // src/server/tools/artifact-output.ts
6516
- import { z as z27 } from "zod";
6517
- var artifactFilesSchema = z27.array(
6518
- z27.object({
6519
- path: z27.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
6520
- content: z27.string().describe("The file content, in `encoding` (default utf8)."),
6521
- encoding: z27.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
6522
- content_type: z27.string().optional().describe("Hint used only for extensions the platform does not recognise.")
6638
+ import { z as z28 } from "zod";
6639
+ var artifactFilesSchema = z28.array(
6640
+ z28.object({
6641
+ path: z28.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
6642
+ content: z28.string().describe("The file content, in `encoding` (default utf8)."),
6643
+ encoding: z28.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
6644
+ content_type: z28.string().optional().describe("Hint used only for extensions the platform does not recognise.")
6523
6645
  })
6524
6646
  ).describe("Files of the bundle.");
6525
6647
  function artifactSummary(a) {
@@ -6591,15 +6713,15 @@ function artifactCreateConflictMessage(conflict) {
6591
6713
  // src/server/tools/list_artifacts.ts
6592
6714
  var LIST_DEFAULT4 = 50;
6593
6715
  var LIST_MAX4 = 100;
6594
- var inputSchema26 = {
6595
- project_id: z28.string().min(1).describe("UUID of the project whose artifacts to list."),
6596
- limit: z28.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
6597
- offset: z28.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
6716
+ var inputSchema27 = {
6717
+ project_id: z29.string().min(1).describe("UUID of the project whose artifacts to list."),
6718
+ limit: z29.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
6719
+ offset: z29.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
6598
6720
  };
6599
6721
  var listArtifactsTool = {
6600
6722
  name: "list_artifacts",
6601
6723
  description: "List a project's live artifacts (multi-file bundles such as HTML mockups, each versioned as one snapshot) with id, slug, entry path and revision_count. Returns `total` and, when more remain, `next_offset`. Use get_artifact for the file manifest and read_artifact_file for content.",
6602
- inputSchema: inputSchema26,
6724
+ inputSchema: inputSchema27,
6603
6725
  handler: async (args, ctx) => {
6604
6726
  const limit = clampLimit(args.limit, LIST_DEFAULT4, LIST_MAX4);
6605
6727
  const offset = args.offset ?? 0;
@@ -6617,15 +6739,15 @@ var listArtifactsTool = {
6617
6739
  };
6618
6740
 
6619
6741
  // src/server/tools/get_artifact.ts
6620
- import { z as z29 } from "zod";
6621
- var inputSchema27 = {
6622
- artifact_id: z29.string().min(1).describe("UUID of the artifact."),
6623
- revision: z29.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
6742
+ import { z as z30 } from "zod";
6743
+ var inputSchema28 = {
6744
+ artifact_id: z30.string().min(1).describe("UUID of the artifact."),
6745
+ revision: z30.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
6624
6746
  };
6625
6747
  var getArtifactTool = {
6626
6748
  name: "get_artifact",
6627
6749
  description: "Get an artifact's metadata plus the file manifest (path, content type, size, checksum of every file) of its head revision, or of `revision` when given. `revision_count` is the head number AND the value to pass as expected_version when writing; it only ever grows because rollback appends a revision rather than discarding history.",
6628
- inputSchema: inputSchema27,
6750
+ inputSchema: inputSchema28,
6629
6751
  handler: async (args, ctx) => {
6630
6752
  const detail = await ctx.client.getArtifact(args.artifact_id);
6631
6753
  if (args.revision === void 0) {
@@ -6642,19 +6764,19 @@ var getArtifactTool = {
6642
6764
  };
6643
6765
 
6644
6766
  // src/server/tools/read_artifact_file.ts
6645
- import { z as z30 } from "zod";
6767
+ import { z as z31 } from "zod";
6646
6768
  var MAX_READABLE_ARTIFACT_FILE_BYTES = 8 * 1024 * 1024;
6647
- var inputSchema28 = {
6648
- artifact_id: z30.string().min(1).describe("UUID of the artifact."),
6649
- path: z30.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
6650
- revision: z30.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
6651
- offset: z30.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
6652
- limit: z30.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
6769
+ var inputSchema29 = {
6770
+ artifact_id: z31.string().min(1).describe("UUID of the artifact."),
6771
+ path: z31.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
6772
+ revision: z31.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
6773
+ offset: z31.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
6774
+ limit: z31.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
6653
6775
  };
6654
6776
  var readArtifactFileTool = {
6655
6777
  name: "read_artifact_file",
6656
6778
  description: "Return one artifact file's UTF-8 content, paginated by line (offset/limit, default and cap 2000 lines, 1 MiB per response) with `total_lines`, `truncated` and `next_offset`. Reads the head revision unless `revision` is given. Binary files (images, fonts) cannot be read through this tool; get_artifact lists their size and checksum.",
6657
- inputSchema: inputSchema28,
6779
+ inputSchema: inputSchema29,
6658
6780
  handler: async (args, ctx) => {
6659
6781
  const file = await ctx.client.downloadArtifactFile(args.artifact_id, args.path, args.revision);
6660
6782
  if (file.content.byteLength > MAX_READABLE_ARTIFACT_FILE_BYTES) {
@@ -6689,20 +6811,20 @@ var readArtifactFileTool = {
6689
6811
  };
6690
6812
 
6691
6813
  // src/server/tools/create_artifact.ts
6692
- import { z as z31 } from "zod";
6693
- var inputSchema29 = {
6694
- project_id: z31.string().min(1).describe("UUID of the project to create the artifact in."),
6695
- name: z31.string().min(1).describe("Display name (max 200 characters)."),
6696
- slug: z31.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
6697
- description: z31.string().optional().describe("Optional description (max 2000 characters)."),
6698
- entry_path: z31.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
6699
- message: z31.string().optional().describe("Short summary of this first revision (max 500 characters)."),
6814
+ import { z as z32 } from "zod";
6815
+ var inputSchema30 = {
6816
+ project_id: z32.string().min(1).describe("UUID of the project to create the artifact in."),
6817
+ name: z32.string().min(1).describe("Display name (max 200 characters)."),
6818
+ slug: z32.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
6819
+ description: z32.string().optional().describe("Optional description (max 2000 characters)."),
6820
+ entry_path: z32.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
6821
+ message: z32.string().optional().describe("Short summary of this first revision (max 500 characters)."),
6700
6822
  files: artifactFilesSchema
6701
6823
  };
6702
6824
  var createArtifactTool = {
6703
6825
  name: "create_artifact",
6704
6826
  description: "Create an artifact \u2014 a bundle of files (e.g. an HTML mockup: index.html + styles.css + app.js) the platform versions AS ONE snapshot \u2014 with the given files as revision 1. At most 50 files, 2 MiB per file, 10 MiB per revision. Fails with a conflict when the slug is already taken in the project (trashed artifacts still own their slug).",
6705
- inputSchema: inputSchema29,
6827
+ inputSchema: inputSchema30,
6706
6828
  handler: async (args, ctx) => {
6707
6829
  try {
6708
6830
  const result = await ctx.client.createArtifact(args.project_id, {
@@ -6730,24 +6852,24 @@ var createArtifactTool = {
6730
6852
  };
6731
6853
 
6732
6854
  // src/server/tools/write_artifact_revision.ts
6733
- import { z as z32 } from "zod";
6734
- var inputSchema30 = {
6735
- artifact_id: z32.string().min(1).describe("UUID of the artifact."),
6855
+ import { z as z33 } from "zod";
6856
+ var inputSchema31 = {
6857
+ artifact_id: z33.string().min(1).describe("UUID of the artifact."),
6736
6858
  files: artifactFilesSchema,
6737
- base_revision: z32.number().int().positive().optional().describe(
6859
+ base_revision: z33.number().int().positive().optional().describe(
6738
6860
  "PATCH mode: start from this revision's files, upsert `files` by path and drop `remove` paths. Omit for FULL mode, where `files` is the complete new file set. Patch mode defaults expected_version to base_revision."
6739
6861
  ),
6740
- remove: z32.array(z32.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
6741
- expected_version: z32.number().int().nonnegative().optional().describe(
6862
+ remove: z33.array(z33.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
6863
+ expected_version: z33.number().int().nonnegative().optional().describe(
6742
6864
  "The revision_count this write is based on (from get_artifact). The platform refuses the write with a conflict if the head moved. Defaults to base_revision in patch mode, else the current head."
6743
6865
  ),
6744
- message: z32.string().optional().describe("Short summary of the change (max 500 characters)."),
6745
- entry_path: z32.string().optional().describe("Change the file a renderer opens first, from this revision on.")
6866
+ message: z33.string().optional().describe("Short summary of the change (max 500 characters)."),
6867
+ entry_path: z33.string().optional().describe("Change the file a renderer opens first, from this revision on.")
6746
6868
  };
6747
6869
  var writeArtifactRevisionTool = {
6748
6870
  name: "write_artifact_revision",
6749
6871
  description: "Write a new revision of an artifact. FULL mode (no base_revision): `files` becomes the whole bundle. PATCH mode (base_revision set): only the changed files are sent, plus `remove` for paths to drop \u2014 the platform stores the complete result either way. Every revision is immutable; the newest N are retained (50 by default, set by the platform; older ones are pruned); use rollback_artifact to restore an earlier one. Read the current head with get_artifact first and pass its revision_count as expected_version.",
6750
- inputSchema: inputSchema30,
6872
+ inputSchema: inputSchema31,
6751
6873
  handler: async (args, ctx) => {
6752
6874
  if (args.remove && args.remove.length > 0 && args.base_revision === void 0) {
6753
6875
  return errorResult("`remove` is only valid in patch mode: set base_revision to the revision you are editing.");
@@ -6781,17 +6903,17 @@ var writeArtifactRevisionTool = {
6781
6903
  };
6782
6904
 
6783
6905
  // src/server/tools/rollback_artifact.ts
6784
- import { z as z33 } from "zod";
6785
- var inputSchema31 = {
6786
- artifact_id: z33.string().min(1).describe("UUID of the artifact."),
6787
- revision_number: z33.number().int().positive().describe("The revision whose files to restore."),
6788
- expected_version: z33.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
6789
- message: z33.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
6906
+ import { z as z34 } from "zod";
6907
+ var inputSchema32 = {
6908
+ artifact_id: z34.string().min(1).describe("UUID of the artifact."),
6909
+ revision_number: z34.number().int().positive().describe("The revision whose files to restore."),
6910
+ expected_version: z34.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
6911
+ message: z34.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
6790
6912
  };
6791
6913
  var rollbackArtifactTool = {
6792
6914
  name: "rollback_artifact",
6793
6915
  description: "Restore an earlier revision of an artifact. Like `git revert`, this APPENDS a new revision whose files equal the chosen one \u2014 history is never rewritten, and the new head is revision_count + 1. Only the newest N revisions are retained (50 by default, set by the platform): older ones are pruned, and rolling back to a pruned revision fails with not found \u2014 list the revisions (get_artifact) first when in doubt. Rolling back to the head itself is allowed and recorded with no_change=true.",
6794
- inputSchema: inputSchema31,
6916
+ inputSchema: inputSchema32,
6795
6917
  handler: async (args, ctx) => {
6796
6918
  try {
6797
6919
  const result = await ctx.client.rollbackArtifact(
@@ -6811,10 +6933,10 @@ var rollbackArtifactTool = {
6811
6933
  };
6812
6934
 
6813
6935
  // src/server/tools/create_stream_token.ts
6814
- import { z as z35 } from "zod";
6936
+ import { z as z36 } from "zod";
6815
6937
 
6816
6938
  // src/server/tools/stream-token-output.ts
6817
- import { z as z34 } from "zod";
6939
+ import { z as z35 } from "zod";
6818
6940
  var projectStreamEventTypes = [
6819
6941
  "project.updated",
6820
6942
  "project.deleted",
@@ -6833,7 +6955,7 @@ var projectStreamEventTypes = [
6833
6955
  "spec_session.deleted",
6834
6956
  "*"
6835
6957
  ];
6836
- var streamEventTypesSchema = z34.array(z34.enum(projectStreamEventTypes)).min(1).describe(
6958
+ var streamEventTypesSchema = z35.array(z35.enum(projectStreamEventTypes)).min(1).describe(
6837
6959
  'Event types to receive. Required \u2014 pass ["*"] to follow all of them. spec_file.updated covers new revisions, rollbacks, and moves to or from the trash; spec_file.lock covers acquire, renew, release and takeover; spec_session.updated covers completion, archive, unarchive and rename; spec_file_comment.updated covers a body edit and a soft delete, thread_state_changed covers resolve and unresolve, and anchors_updated is batched across every thread one save moved.'
6838
6960
  );
6839
6961
  function streamTokenSummary(token) {
@@ -6863,19 +6985,19 @@ function streamTokenMintOutput(result) {
6863
6985
  }
6864
6986
 
6865
6987
  // src/server/tools/create_stream_token.ts
6866
- var inputSchema32 = {
6867
- resource_type: z35.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
6868
- resource_id: z35.string().min(1).describe("UUID of the project to stream events for."),
6988
+ var inputSchema33 = {
6989
+ resource_type: z36.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
6990
+ resource_id: z36.string().min(1).describe("UUID of the project to stream events for."),
6869
6991
  event_types: streamEventTypesSchema,
6870
- ttl_seconds: z35.number().int().positive().optional().describe(
6992
+ ttl_seconds: z36.number().int().positive().optional().describe(
6871
6993
  "How long the URL stays valid. Defaults to 24 hours, capped at 7 days. Out-of-range values are clamped rather than rejected \u2014 check expires_at in the result."
6872
6994
  ),
6873
- label: z35.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
6995
+ label: z36.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
6874
6996
  };
6875
6997
  var createStreamTokenTool = {
6876
6998
  name: "create_stream_token",
6877
6999
  description: "Create a short-lived WebSocket URL that streams a project's events in real time \u2014 like an S3 presigned URL, the returned URL is the whole credential and needs no auth header. Choose the project and the event types to follow; the result carries the full wss:// URL. Feed it to any WebSocket client with subprotocol 'v1' (for example Claude Code's Monitor tool). Each text frame is one JSON event carrying a `seq` cursor; reconnect with ?after=<seq> to resume without missing events. Defaults to 24 hours, 7 days maximum. TREAT THE RETURNED URL LIKE A PASSWORD: anyone holding it can read every event on this project until it expires, without authenticating. Pass it directly to the WebSocket client; never write it to a file, a commit, an issue or a chat reply, and never log or repeat it. It is shown once and cannot be retrieved again. If it may have been exposed, revoke it at once with revoke_stream_token \u2014 that is also how you end one early. Prefer the shortest ttl_seconds that covers the work.",
6878
- inputSchema: inputSchema32,
7000
+ inputSchema: inputSchema33,
6879
7001
  handler: async (args, ctx) => {
6880
7002
  const result = await ctx.client.createStreamToken({
6881
7003
  resourceType: args.resource_type,
@@ -6889,22 +7011,22 @@ var createStreamTokenTool = {
6889
7011
  };
6890
7012
 
6891
7013
  // src/server/tools/list_stream_tokens.ts
6892
- import { z as z36 } from "zod";
7014
+ import { z as z37 } from "zod";
6893
7015
  var LIST_DEFAULT5 = 50;
6894
7016
  var LIST_MAX5 = 200;
6895
- var inputSchema33 = {
6896
- resource_id: z36.string().optional().describe("Only tokens following this project."),
6897
- live_only: z36.boolean().optional().describe("Hide expired and revoked tokens."),
6898
- mine_only: z36.boolean().optional().describe(
7017
+ var inputSchema34 = {
7018
+ resource_id: z37.string().optional().describe("Only tokens following this project."),
7019
+ live_only: z37.boolean().optional().describe("Hide expired and revoked tokens."),
7020
+ mine_only: z37.boolean().optional().describe(
6899
7021
  "Only tokens you created. Off by default, so a leaked URL can be found \u2014 note that revoking one you did not mint needs the organisation owner or admin role."
6900
7022
  ),
6901
- limit: z36.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
6902
- offset: z36.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
7023
+ limit: z37.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
7024
+ offset: z37.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
6903
7025
  };
6904
7026
  var listStreamTokensTool = {
6905
7027
  name: "list_stream_tokens",
6906
7028
  description: "List the stream tokens in your organisation, with their scope, expiry and whether each is still live. Secrets are never returned \u2014 only a short prefix to tell tokens apart. Returns `total` and, when more remain, `next_offset`.",
6907
- inputSchema: inputSchema33,
7029
+ inputSchema: inputSchema34,
6908
7030
  handler: async (args, ctx) => {
6909
7031
  const limit = clampLimit(args.limit, LIST_DEFAULT5, LIST_MAX5);
6910
7032
  const offset = args.offset ?? 0;
@@ -6927,14 +7049,14 @@ var listStreamTokensTool = {
6927
7049
  };
6928
7050
 
6929
7051
  // src/server/tools/revoke_stream_token.ts
6930
- import { z as z37 } from "zod";
6931
- var inputSchema34 = {
6932
- id: z37.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
7052
+ import { z as z38 } from "zod";
7053
+ var inputSchema35 = {
7054
+ id: z38.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
6933
7055
  };
6934
7056
  var revokeStreamTokenTool = {
6935
7057
  name: "revoke_stream_token",
6936
7058
  description: "Revoke a stream token immediately, closing its URL and deleting the events buffered for it. Use this as soon as a stream is no longer needed, or if its URL may have leaked \u2014 the URL is the credential, so revoking is the only way to stop it before it expires. Revoking a token you minted yourself always works. Revoking one somebody ELSE minted additionally requires the organisation owner or admin role, and is refused with 403 otherwise \u2014 so when hunting a leaked URL you did not create, expect that refusal and ask an organisation owner or admin rather than retrying.",
6937
- inputSchema: inputSchema34,
7059
+ inputSchema: inputSchema35,
6938
7060
  handler: async (args, ctx) => {
6939
7061
  const token = await ctx.client.revokeStreamToken(args.id);
6940
7062
  return jsonResult({ ...streamTokenSummary(token), revoked: true });
@@ -6942,7 +7064,7 @@ var revokeStreamTokenTool = {
6942
7064
  };
6943
7065
 
6944
7066
  // src/server/tools/start_spec_session.ts
6945
- import { z as z38 } from "zod";
7067
+ import { z as z39 } from "zod";
6946
7068
 
6947
7069
  // src/server/tools/start-session-core.ts
6948
7070
  var START_SESSION_WAIT_MS = 75e3;
@@ -7334,28 +7456,28 @@ var START_SESSION_DESCRIPTION_BASE = "Start a NEW MySpec chat session on the use
7334
7456
 
7335
7457
  // src/server/tools/start_spec_session.ts
7336
7458
  var BRIDGE_COMMAND = "npx -y @myspec/mcp-server reverse --root <path>";
7337
- var inputSchema35 = {
7338
- project_id: z38.string().min(1).describe("UUID of the project to start the session in."),
7339
- mode: z38.enum(["generate", "edit"]).describe(
7459
+ var inputSchema36 = {
7460
+ project_id: z39.string().min(1).describe("UUID of the project to start the session in."),
7461
+ mode: z39.enum(["generate", "edit"]).describe(
7340
7462
  "'generate' writes a new spec bundle from scratch; 'edit' changes the spec files of an existing bundle."
7341
7463
  ),
7342
- prompt: z38.string().min(1).describe(
7464
+ prompt: z39.string().min(1).describe(
7343
7465
  "The opening message. Write it as the user would: what they want specced, and the repository context you have gathered that they would otherwise have to retype."
7344
7466
  ),
7345
- workflow_id: z38.string().optional().describe(
7467
+ workflow_id: z39.string().optional().describe(
7346
7468
  "Workflow variant id, e.g. myspec-greenfield-v1, enterprise-greenfield-v1, speckit-greenfield-v1, openspec-brownfield-v1, myspec-brownfield-v1, myspec-edit-v1, myspec-edit-brownfield-v1. Omit to use the default for the chosen mode. Must agree with mode \u2014 a contradiction is rejected, not silently resolved."
7347
7469
  ),
7348
- language: z38.enum(SPEC_SESSION_LANGUAGES).optional().describe(
7470
+ language: z39.enum(SPEC_SESSION_LANGUAGES).optional().describe(
7349
7471
  "Output language the session locks to on its first turn. Defaults to 'en'. A session's language is immutable once set."
7350
7472
  ),
7351
- model_id: z38.string().optional().describe("Model id to run the session with, e.g. openai:gpt-6-sol."),
7352
- attachment_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
7473
+ model_id: z39.string().optional().describe("Model id to run the session with, e.g. openai:gpt-6-sol."),
7474
+ attachment_ids: z39.array(z39.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
7353
7475
  `Project attachment ids to cite in the opening message, as the composer @-mentions do. At most ${String(MAX_REFERENCE_IDS)}.`
7354
7476
  ),
7355
- spec_file_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
7477
+ spec_file_ids: z39.array(z39.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
7356
7478
  `Existing spec file ids to cite in the opening message. Required in practice for edit sessions. At most ${String(MAX_REFERENCE_IDS)}.`
7357
7479
  ),
7358
- session_title: z38.string().min(1).max(SESSION_TITLE_MAX_LENGTH).optional().describe(
7480
+ session_title: z39.string().min(1).max(SESSION_TITLE_MAX_LENGTH).optional().describe(
7359
7481
  `Human-readable session title, stored as context.sessionSummary (max ${String(SESSION_TITLE_MAX_LENGTH)} characters). A workflow that names the session itself during its interview will overwrite it, exactly as in the webapp.`
7360
7482
  )
7361
7483
  };
@@ -7417,7 +7539,7 @@ async function resolveSpecFiles(ctx, projectId, ids) {
7417
7539
  var startSpecSessionTool = {
7418
7540
  name: "start_spec_session",
7419
7541
  description: START_SESSION_DESCRIPTION_BASE + BRIDGE_ORDERING_NOTE,
7420
- inputSchema: inputSchema35,
7542
+ inputSchema: inputSchema36,
7421
7543
  handler: async (args, ctx) => {
7422
7544
  const webappUrl = httpUrlOrNull(ctx.webappUrl);
7423
7545
  if (webappUrl === null) {
@@ -7540,7 +7662,7 @@ var startSpecSessionTool = {
7540
7662
  };
7541
7663
 
7542
7664
  // src/server/tools/tasks.ts
7543
- import { z as z39 } from "zod";
7665
+ import { z as z40 } from "zod";
7544
7666
  var LIST_DEFAULT_TASKS = 100;
7545
7667
  var LIST_MAX_TASKS = 500;
7546
7668
  var LIST_MAX_OFFSET = 1e6;
@@ -7548,8 +7670,8 @@ var DETAILS_PREVIEW_CHARS = 500;
7548
7670
  var ME = "me";
7549
7671
  var NONE = "none";
7550
7672
  var PRIORITY_NAMES = ["none", "urgent", "high", "medium", "low"];
7551
- var STATUS = z39.enum(["pending", "in_progress", "done"]);
7552
- var RELATIONSHIP_TYPE = z39.enum(["blocks", "blocked_by", "related"]);
7673
+ var STATUS = z40.enum(["pending", "in_progress", "done"]);
7674
+ var RELATIONSHIP_TYPE = z40.enum(["blocks", "blocked_by", "related"]);
7553
7675
  function toDay(date) {
7554
7676
  return date ? date.toISOString().slice(0, 10) : null;
7555
7677
  }
@@ -7584,33 +7706,33 @@ function taskSummary(task, opts = {}) {
7584
7706
  updated_at: task.updatedAt?.toISOString()
7585
7707
  };
7586
7708
  }
7587
- var userIdField = z39.string().trim().min(1);
7709
+ var userIdField = z40.string().trim().min(1);
7588
7710
  async function resolveMe(id, ctx) {
7589
7711
  return id === ME ? ctx.client.currentUserId() : id;
7590
7712
  }
7591
7713
  var UNKNOWN_ME = 'Could not tell who you are from this connection, so "me" cannot be used. Pass your user id instead: list_task_assignees returns it as self_user_id.';
7592
- var labelField = z39.string().trim().min(1).refine((label) => Array.from(label).length <= TASK_LIMITS.maxLabelLength, {
7714
+ var labelField = z40.string().trim().min(1).refine((label) => Array.from(label).length <= TASK_LIMITS.maxLabelLength, {
7593
7715
  message: `A label is at most ${String(TASK_LIMITS.maxLabelLength)} characters.`
7594
7716
  });
7595
7717
  function taskIdsField(what) {
7596
- return z39.array(z39.uuid()).min(1).max(TASK_LIMITS.maxTasksPerRequest).describe(`${what} At most ${String(TASK_LIMITS.maxTasksPerRequest)} per call.`);
7718
+ return z40.array(z40.uuid()).min(1).max(TASK_LIMITS.maxTasksPerRequest).describe(`${what} At most ${String(TASK_LIMITS.maxTasksPerRequest)} per call.`);
7597
7719
  }
7598
7720
  var listInputSchema2 = {
7599
- project_id: z39.uuid().describe("UUID of the project whose tasks to list."),
7721
+ project_id: z40.uuid().describe("UUID of the project whose tasks to list."),
7600
7722
  status: STATUS.optional().describe("Only tasks with this status."),
7601
7723
  assignee_user_id: userIdField.optional().describe(
7602
7724
  'Only tasks assigned to this user id (as returned in assignee_user_id or by list_task_assignees). "me" means you; "none" means unassigned tasks.'
7603
7725
  ),
7604
- cycle: z39.number().int().min(1).max(TASK_LIMITS.maxCycle).optional().describe("Only tasks in this cycle (sprint) number."),
7726
+ cycle: z40.number().int().min(1).max(TASK_LIMITS.maxCycle).optional().describe("Only tasks in this cycle (sprint) number."),
7605
7727
  label: labelField.optional().describe("Only tasks carrying this label."),
7606
- file_id: z39.uuid().optional().describe("Only tasks from this tasks.md (its spec file id)."),
7607
- include_details: z39.boolean().optional().describe(
7728
+ file_id: z40.uuid().optional().describe("Only tasks from this tasks.md (its spec file id)."),
7729
+ include_details: z40.boolean().optional().describe(
7608
7730
  "Also return each task's details (its sub-bullets in tasks.md), previewed to 500 characters. Off by default: they make the result several times larger."
7609
7731
  ),
7610
- limit: z39.number().int().min(1).max(LIST_MAX_TASKS).optional().describe(
7732
+ limit: z40.number().int().min(1).max(LIST_MAX_TASKS).optional().describe(
7611
7733
  `Tasks per page (default ${String(LIST_DEFAULT_TASKS)}, at most ${String(LIST_MAX_TASKS)}).`
7612
7734
  ),
7613
- offset: z39.number().int().min(0).max(LIST_MAX_OFFSET).optional().describe("How many matching tasks to skip; pass next_offset from the previous page.")
7735
+ offset: z40.number().int().min(0).max(LIST_MAX_OFFSET).optional().describe("How many matching tasks to skip; pass next_offset from the previous page.")
7614
7736
  };
7615
7737
  function pageLocally(board, query) {
7616
7738
  const assignee = query.assigneeUserId;
@@ -7678,7 +7800,7 @@ var listTasksTool = {
7678
7800
  }
7679
7801
  };
7680
7802
  var assigneesInputSchema = {
7681
- project_id: z39.uuid().describe("UUID of the project whose tasks are being assigned.")
7803
+ project_id: z40.uuid().describe("UUID of the project whose tasks are being assigned.")
7682
7804
  };
7683
7805
  var listTaskAssigneesTool = {
7684
7806
  name: "list_task_assignees",
@@ -7699,29 +7821,29 @@ var listTaskAssigneesTool = {
7699
7821
  }
7700
7822
  };
7701
7823
  var updateInputSchema2 = {
7702
- project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
7824
+ project_id: z40.uuid().describe("UUID of the project the tasks belong to."),
7703
7825
  task_ids: taskIdsField("The tasks to change (task_id values from list_tasks)."),
7704
7826
  assignee_user_id: userIdField.nullable().optional().describe(
7705
7827
  'User id to assign ("me" for yourself), or null to unassign. The person must be a member of the organisation who can open the project: list_task_assignees lists them.'
7706
7828
  ),
7707
- priority: z39.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
7708
- story_points: z39.number().min(0).max(TASK_LIMITS.maxStoryPoints).nullable().optional().describe("Estimate in story points, or null to clear it."),
7709
- cycle: z39.number().int().min(1).max(TASK_LIMITS.maxCycle).nullable().optional().describe("Cycle (sprint) number, or null to clear it."),
7710
- start_date: z39.iso.date().nullable().optional().describe("Start day as YYYY-MM-DD, or null to clear it."),
7711
- end_date: z39.iso.date().nullable().optional().describe("Due day as YYYY-MM-DD, or null to clear it. Not before the start day."),
7712
- add_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe(`Labels to add. A task carries at most ${String(TASK_LIMITS.maxLabels)}.`),
7713
- remove_labels: z39.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe("Labels to remove."),
7714
- add_relationships: z39.array(
7715
- z39.object({
7829
+ priority: z40.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low."),
7830
+ story_points: z40.number().min(0).max(TASK_LIMITS.maxStoryPoints).nullable().optional().describe("Estimate in story points, or null to clear it."),
7831
+ cycle: z40.number().int().min(1).max(TASK_LIMITS.maxCycle).nullable().optional().describe("Cycle (sprint) number, or null to clear it."),
7832
+ start_date: z40.iso.date().nullable().optional().describe("Start day as YYYY-MM-DD, or null to clear it."),
7833
+ end_date: z40.iso.date().nullable().optional().describe("Due day as YYYY-MM-DD, or null to clear it. Not before the start day."),
7834
+ add_labels: z40.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe(`Labels to add. A task carries at most ${String(TASK_LIMITS.maxLabels)}.`),
7835
+ remove_labels: z40.array(labelField).max(TASK_LIMITS.maxLabels).optional().describe("Labels to remove."),
7836
+ add_relationships: z40.array(
7837
+ z40.object({
7716
7838
  type: RELATIONSHIP_TYPE.describe(
7717
7839
  "How each changed task stands to task_id: blocks (task_id waits for it), blocked_by (it waits for task_id) or related."
7718
7840
  ),
7719
- task_id: z39.uuid().describe("The other task (a task_id from list_tasks).")
7841
+ task_id: z40.uuid().describe("The other task (a task_id from list_tasks).")
7720
7842
  })
7721
7843
  ).max(TASK_LIMITS.maxRelationships).optional().describe(
7722
7844
  `Relationships to set. Two tasks have at most one: setting one replaces whatever the pair had, and the other task sees the inverse (A blocks B is B blocked_by A). A task has at most ${String(TASK_LIMITS.maxRelationships)}.`
7723
7845
  ),
7724
- remove_relationships: z39.array(z39.uuid()).max(TASK_LIMITS.maxRelationships).optional().describe("Task ids to unlink each changed task from, whatever the relationship.")
7846
+ remove_relationships: z40.array(z40.uuid()).max(TASK_LIMITS.maxRelationships).optional().describe("Task ids to unlink each changed task from, whatever the relationship.")
7725
7847
  };
7726
7848
  var updateTasksTool = {
7727
7849
  name: "update_tasks",
@@ -7764,13 +7886,13 @@ var updateTasksTool = {
7764
7886
  }
7765
7887
  };
7766
7888
  var statusInputSchema = {
7767
- project_id: z39.uuid().describe("UUID of the project the tasks belong to."),
7889
+ project_id: z40.uuid().describe("UUID of the project the tasks belong to."),
7768
7890
  task_ids: taskIdsField("The tasks to move (task_id values from list_tasks)."),
7769
7891
  status: STATUS.describe("The new status: pending, in_progress or done."),
7770
- expected_versions: z39.array(
7771
- z39.object({
7772
- spec_file_id: z39.uuid().describe("A tasks.md file (spec_file_id from list_tasks)."),
7773
- content_version: z39.number().int().min(0).describe("The content_version list_tasks returned for that file.")
7892
+ expected_versions: z40.array(
7893
+ z40.object({
7894
+ spec_file_id: z40.uuid().describe("A tasks.md file (spec_file_id from list_tasks)."),
7895
+ content_version: z40.number().int().min(0).describe("The content_version list_tasks returned for that file.")
7774
7896
  })
7775
7897
  ).max(TASK_LIMITS.maxBoardFiles).optional().describe(
7776
7898
  "Optional. The file versions your view of these tasks came from (from list_tasks); only the files these tasks are in are checked, so passing just those is enough. A file that has changed since is not written and its tasks fail with file_changed, so a change never lands on a line you did not see; list again and retry."
@@ -7839,6 +7961,7 @@ function buildServer(deps) {
7839
7961
  registerTool(server, uploadSpecFileTool, ctx);
7840
7962
  registerTool(server, updateSpecFileTool, ctx);
7841
7963
  registerTool(server, moveSpecFileToTrashTool, ctx);
7964
+ registerTool(server, moveSpecFolderTool, ctx);
7842
7965
  registerTool(server, restoreSpecFileFromTrashTool, ctx);
7843
7966
  registerTool(server, listAttachmentsTool, ctx);
7844
7967
  registerTool(server, readAttachmentTool, ctx);
@@ -8043,7 +8166,7 @@ async function canonicaliseRoot(root) {
8043
8166
  import { promises as fs4 } from "fs";
8044
8167
  import { join as join2, relative } from "path";
8045
8168
  import { spawn } from "child_process";
8046
- import { z as z40 } from "zod";
8169
+ import { z as z41 } from "zod";
8047
8170
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
8048
8171
 
8049
8172
  // src/reverse/ignore.ts
@@ -8348,37 +8471,37 @@ var READ_FILE_MAX_LINES = 2e3;
8348
8471
  var LIST_DIR_HARD_CAP = 5e3;
8349
8472
  var LIST_DIR_DEFAULT_CAP = 1e3;
8350
8473
  var GREP_MAX_MATCHES_DEFAULT = 200;
8351
- var listDirInput = z40.object({
8352
- path: z40.string(),
8353
- recursive: z40.boolean().optional(),
8354
- maxEntries: z40.number().int().positive().optional(),
8355
- includeIgnored: z40.boolean().optional()
8474
+ var listDirInput = z41.object({
8475
+ path: z41.string(),
8476
+ recursive: z41.boolean().optional(),
8477
+ maxEntries: z41.number().int().positive().optional(),
8478
+ includeIgnored: z41.boolean().optional()
8356
8479
  });
8357
- var readFileInput = z40.object({
8358
- path: z40.string(),
8359
- offset: z40.number().int().nonnegative().optional(),
8360
- limit: z40.number().int().positive().optional(),
8361
- includeIgnored: z40.boolean().optional()
8480
+ var readFileInput = z41.object({
8481
+ path: z41.string(),
8482
+ offset: z41.number().int().nonnegative().optional(),
8483
+ limit: z41.number().int().positive().optional(),
8484
+ includeIgnored: z41.boolean().optional()
8362
8485
  });
8363
- var grepInput = z40.object({
8364
- pattern: z40.string().describe(
8486
+ var grepInput = z41.object({
8487
+ pattern: z41.string().describe(
8365
8488
  "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."
8366
8489
  ),
8367
- path: z40.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
8368
- isRegex: z40.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
8369
- caseSensitive: z40.boolean().optional(),
8370
- maxMatches: z40.number().int().positive().optional(),
8371
- contextLines: z40.number().int().nonnegative().optional(),
8372
- includeIgnored: z40.boolean().optional()
8490
+ path: z41.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
8491
+ isRegex: z41.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
8492
+ caseSensitive: z41.boolean().optional(),
8493
+ maxMatches: z41.number().int().positive().optional(),
8494
+ contextLines: z41.number().int().nonnegative().optional(),
8495
+ includeIgnored: z41.boolean().optional()
8373
8496
  });
8374
- var packCodebaseInput = z40.object({
8375
- subpath: z40.string().optional(),
8376
- includePatterns: z40.string().optional(),
8377
- ignorePatterns: z40.string().optional()
8497
+ var packCodebaseInput = z41.object({
8498
+ subpath: z41.string().optional(),
8499
+ includePatterns: z41.string().optional(),
8500
+ ignorePatterns: z41.string().optional()
8378
8501
  });
8379
- var packCodebaseReadPageInput = z40.object({
8380
- outputId: z40.string(),
8381
- page: z40.number().int().positive()
8502
+ var packCodebaseReadPageInput = z41.object({
8503
+ outputId: z41.string(),
8504
+ page: z41.number().int().positive()
8382
8505
  });
8383
8506
  function structuredError(error, message, extra) {
8384
8507
  const payload = { error, message, ...extra ?? {} };
@@ -8906,7 +9029,7 @@ function jsonSchemaFromZod(schema) {
8906
9029
  return zodToJson(schema);
8907
9030
  }
8908
9031
  function zodToJson(schema) {
8909
- if (schema instanceof z40.ZodObject) {
9032
+ if (schema instanceof z41.ZodObject) {
8910
9033
  const shape = schema.shape;
8911
9034
  const properties = {};
8912
9035
  const required = [];
@@ -8921,19 +9044,19 @@ function zodToJson(schema) {
8921
9044
  if (required.length > 0) out.required = required;
8922
9045
  return out;
8923
9046
  }
8924
- if (schema instanceof z40.ZodOptional) {
9047
+ if (schema instanceof z41.ZodOptional) {
8925
9048
  return zodToJson(schema.unwrap());
8926
9049
  }
8927
- if (schema instanceof z40.ZodString) {
9050
+ if (schema instanceof z41.ZodString) {
8928
9051
  return { type: "string" };
8929
9052
  }
8930
- if (schema instanceof z40.ZodNumber) {
9053
+ if (schema instanceof z41.ZodNumber) {
8931
9054
  return { type: "number" };
8932
9055
  }
8933
- if (schema instanceof z40.ZodBoolean) {
9056
+ if (schema instanceof z41.ZodBoolean) {
8934
9057
  return { type: "boolean" };
8935
9058
  }
8936
- if (schema instanceof z40.ZodEnum) {
9059
+ if (schema instanceof z41.ZodEnum) {
8937
9060
  return { type: "string", enum: schema.options };
8938
9061
  }
8939
9062
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-next.178",
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": {