@myspec/mcp-server 0.5.0-next.116 → 0.5.0-next.118

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 -0
  2. package/dist/index.js +658 -115
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -313,6 +313,13 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
313
313
  | `create_artifact` | `project_id`, `name`, `files[]` (`path`, `content`, optional `encoding` utf8/base64, optional `content_type`), optional `slug`, `description`, `entry_path`, `message` | Creates the artifact with revision 1. Limits: 50 files, 2 MiB per file, 10 MiB per revision. A taken slug is a conflict |
314
314
  | `write_artifact_revision` | `artifact_id`, `files[]`, optional `base_revision` (patch mode), `remove[]`, `expected_version`, `message`, `entry_path` | Appends an immutable revision. Full mode replaces the whole bundle; patch mode (with `base_revision`) sends only changed files plus paths to `remove`. A stale `expected_version` is reported as a conflict naming the new head — re-read with `get_artifact` and retry |
315
315
  | `rollback_artifact` | `artifact_id`, `revision_number`, optional `expected_version`, `message` | Restores an earlier revision by APPENDING a revision with its files (`git revert` semantics); history is never rewritten, but only the newest N revisions are retained (50 by default, `ARTIFACT_MAX_REVISIONS` on the platform), so a pruned target answers not found |
316
+ | `list_spec_file_comments` | `file_id`, optional `status` (`open`/`resolved`/`orphaned`) | Lists the review comment threads on a spec file. Each thread carries `anchor_quote`, `anchor_status` (`anchored`, or `orphaned` when the passage can no longer be located), `anchor_start`/`anchor_end`, `resolved`, and its `comments` — each with `content_version`, the token `update_spec_file_comment` takes as `expected_version`. A tombstoned first comment is returned with `deleted: true` and no `body`. Works on a trashed file |
317
+ | `create_spec_file_comment_thread` | `file_id`, `body`, `anchor_quote`, optional `anchor_prefix`, `anchor_suffix` | Opens a review comment on a passage. Quote the passage verbatim — the server searches the current content for it and derives the character offsets itself, so do NOT send them. `anchor_prefix`/`anchor_suffix` are the surrounding text, which disambiguates a passage appearing more than once. Conflicts on a trashed file: a file staged for deletion does not gather new discussion |
318
+ | `reply_spec_file_comment_thread` | `file_id`, `thread_id`, `body` | Replies to an existing thread. Conflicts on a trashed file |
319
+ | `update_spec_file_comment` | `file_id`, `comment_id`, `body`, optional `expected_version` | Edits the body of one of YOUR OWN comments; the platform refuses somebody else's. `expected_version` is the **comment's** own `content_version`, not the file's — pairing it with the file would fail an edit because somebody saved the document. Works on a trashed file |
320
+ | `delete_spec_file_comment` | `file_id`, `comment_id` | Soft-deletes one of YOUR OWN comments. Three outcomes, reported in the result: a reply disappears (`comment: null`); a first comment with live replies becomes a tombstone so the thread keeps a head (`comment` is the tombstone); a first comment with no live replies takes the whole thread (`thread_deleted: true`) |
321
+
322
+ There is deliberately **no tool to resolve, unresolve or re-anchor a thread**. Those three are human-only: resolving declares a review finished, and re-anchoring is the repair a person makes after the automatic pass has already failed to locate the passage. The platform refuses them for an AI caller with a 403, so a tool would only ever return an error.
316
323
 
317
324
  ### Brownfield sessions: start the bridge first
318
325
 
package/dist/index.js CHANGED
@@ -1353,7 +1353,11 @@ var KNOWN_CONFLICT_REASONS = [
1353
1353
  "precondition_malformed",
1354
1354
  "slug_taken",
1355
1355
  "blob_reclaimed",
1356
- "lock_timeout"
1356
+ "lock_timeout",
1357
+ "content_moved",
1358
+ "file_trashed",
1359
+ "thread_not_orphaned",
1360
+ "version_conflict"
1357
1361
  ];
1358
1362
  var PlatformHttpError = class extends Error {
1359
1363
  status;
@@ -1425,6 +1429,9 @@ function conflictOf(err) {
1425
1429
  own || fromMessage ? { ...fromMessage ?? {}, ...own ?? {} } : void 0
1426
1430
  );
1427
1431
  }
1432
+ function isPlatformConflictError(err) {
1433
+ return conflictOf(err) !== void 0;
1434
+ }
1428
1435
  function isPlatformPreconditionRequiredError(err) {
1429
1436
  return isPlatformHttpError(err) && err.status === 428;
1430
1437
  }
@@ -1855,6 +1862,113 @@ function transformSpecSessionResponse(raw) {
1855
1862
  // ../../packages/platform-client/project/types/lock.types.ts
1856
1863
  var SPEC_FILE_LOCK_TTL_MS = 2 * 6e4;
1857
1864
 
1865
+ // ../../packages/platform-client/project/types/comment.types.ts
1866
+ var COMMENT_THREAD_FILTER_PARAM = "status";
1867
+ var MAX_COMMENT_BODY_LENGTH = 1e4;
1868
+ var MAX_ANCHOR_QUOTE_LENGTH = 2e3;
1869
+ var MAX_ANCHOR_CONTEXT_LENGTH = 64;
1870
+ function parseTimestamp(raw) {
1871
+ if (!raw) {
1872
+ return null;
1873
+ }
1874
+ const parsed = new Date(raw);
1875
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
1876
+ }
1877
+ function toCommentActorKind(raw) {
1878
+ return raw === "ai" ? "ai" : "human";
1879
+ }
1880
+ function toAnchorStatus(raw) {
1881
+ return raw === "anchored" ? "anchored" : "orphaned";
1882
+ }
1883
+ function toCreateCommentThreadRequest(input) {
1884
+ return {
1885
+ body: input.body,
1886
+ anchor_quote: input.anchorQuote,
1887
+ anchor_prefix: input.anchorPrefix,
1888
+ anchor_suffix: input.anchorSuffix
1889
+ };
1890
+ }
1891
+ function toCreateCommentReplyRequest(body) {
1892
+ return { body };
1893
+ }
1894
+ function toUpdateCommentRequest(body) {
1895
+ return { body };
1896
+ }
1897
+ function toReanchorCommentThreadRequest(anchor) {
1898
+ return {
1899
+ anchor_quote: anchor.anchorQuote,
1900
+ anchor_prefix: anchor.anchorPrefix,
1901
+ anchor_suffix: anchor.anchorSuffix
1902
+ };
1903
+ }
1904
+ function transformSpecFileCommentResponse(raw) {
1905
+ return {
1906
+ id: raw.id,
1907
+ threadId: raw.thread_id,
1908
+ org: raw.org,
1909
+ authorId: raw.author_id,
1910
+ authorKind: toCommentActorKind(raw.author_kind),
1911
+ body: raw.body,
1912
+ deleted: raw.deleted,
1913
+ contentVersion: raw.content_version,
1914
+ createdAt: parseTimestamp(raw.created_at),
1915
+ updatedAt: parseTimestamp(raw.updated_at),
1916
+ deletedAt: parseTimestamp(raw.deleted_at)
1917
+ };
1918
+ }
1919
+ function transformSpecFileCommentThreadResponse(raw) {
1920
+ return {
1921
+ id: raw.id,
1922
+ specFileId: raw.spec_file_id,
1923
+ projectId: raw.project_id,
1924
+ org: raw.org,
1925
+ rootCommentId: raw.root_comment_id ?? null,
1926
+ anchorContentVersion: raw.anchor_content_version,
1927
+ anchorRevision: raw.anchor_revision,
1928
+ anchorStart: raw.anchor_start,
1929
+ anchorEnd: raw.anchor_end,
1930
+ anchorQuote: raw.anchor_quote,
1931
+ anchorPrefix: raw.anchor_prefix,
1932
+ anchorSuffix: raw.anchor_suffix,
1933
+ anchorStatus: toAnchorStatus(raw.anchor_status),
1934
+ createdBy: raw.created_by,
1935
+ createdByKind: toCommentActorKind(raw.created_by_kind),
1936
+ resolvedAt: parseTimestamp(raw.resolved_at),
1937
+ resolvedBy: raw.resolved_by ?? null,
1938
+ createdAt: parseTimestamp(raw.created_at),
1939
+ updatedAt: parseTimestamp(raw.updated_at),
1940
+ // Always an array: consumers map over it on every render, so the empty case
1941
+ // must not be a value each one has to guard.
1942
+ comments: (raw.comments ?? []).map(transformSpecFileCommentResponse)
1943
+ };
1944
+ }
1945
+ function transformSpecFileCommentCountResponse(raw) {
1946
+ return {
1947
+ specFileId: raw.spec_file_id,
1948
+ openThreadCount: raw.open_thread_count
1949
+ };
1950
+ }
1951
+ function transformSpecFileCommentThreadList(raw) {
1952
+ return (raw.threads ?? []).map(transformSpecFileCommentThreadResponse);
1953
+ }
1954
+ function transformProjectCommentCounts(raw) {
1955
+ return (raw.counts ?? []).map(transformSpecFileCommentCountResponse);
1956
+ }
1957
+ function transformProjectCommentActivity(raw) {
1958
+ return {
1959
+ threads: (raw.threads ?? []).map(transformSpecFileCommentThreadResponse),
1960
+ total: raw.total,
1961
+ limit: raw.limit,
1962
+ offset: raw.offset
1963
+ };
1964
+ }
1965
+ function transformDeleteCommentResult(raw) {
1966
+ return {
1967
+ comment: raw.comment ? transformSpecFileCommentResponse(raw.comment) : null,
1968
+ threadDeleted: raw.thread_deleted
1969
+ };
1970
+ }
1971
+
1858
1972
  // ../../packages/platform-client/project/http/attachment.http-client.ts
1859
1973
  var UPLOAD_TIMEOUT_MS = 3e4;
1860
1974
  var HttpAttachmentClient = class {
@@ -2440,6 +2554,137 @@ var HttpSpecSessionClient = class {
2440
2554
  }
2441
2555
  };
2442
2556
 
2557
+ // ../../packages/platform-client/project/http/comment.http-client.ts
2558
+ var HEADER_EXPECTED_VERSION2 = "X-Expected-Version";
2559
+ var HEADER_ACTOR_KIND2 = "X-Actor-Kind";
2560
+ function segment(value) {
2561
+ return encodeURIComponent(value);
2562
+ }
2563
+ var HttpSpecFileCommentClient = class {
2564
+ httpClient;
2565
+ constructor(httpClient) {
2566
+ this.httpClient = httpClient;
2567
+ }
2568
+ /**
2569
+ * Turns the shared write options into request headers.
2570
+ *
2571
+ * Both halves are forwarded EXACTLY as given and neither is defaulted. That
2572
+ * matters most for what this does NOT do: it never substitutes
2573
+ * `actorKind: 'human'` for an absent value. Resolve, unresolve and re-anchor
2574
+ * are human-only and the platform parses the header fail-closed (AR-PRJ-26),
2575
+ * so a client-side default would turn a 403 the caller should see into a
2576
+ * successful write attributed to a person who never acted.
2577
+ */
2578
+ writeHeaders(opts) {
2579
+ const headers = {};
2580
+ if (opts?.expectedVersion !== void 0) {
2581
+ headers[HEADER_EXPECTED_VERSION2] = String(opts.expectedVersion);
2582
+ }
2583
+ if (opts?.actorKind) {
2584
+ headers[HEADER_ACTOR_KIND2] = opts.actorKind;
2585
+ }
2586
+ return headers;
2587
+ }
2588
+ async listThreads(fileId, jwtToken, opts) {
2589
+ const query = new URLSearchParams();
2590
+ if (opts?.filter) {
2591
+ query.set(COMMENT_THREAD_FILTER_PARAM, opts.filter);
2592
+ }
2593
+ const suffix = query.toString() ? `?${query.toString()}` : "";
2594
+ const raw = await this.httpClient.get(
2595
+ `/project/v1/files/${segment(fileId)}/comments${suffix}`,
2596
+ jwtToken,
2597
+ {}
2598
+ );
2599
+ return transformSpecFileCommentThreadList(raw);
2600
+ }
2601
+ async createThread(fileId, input, jwtToken, opts) {
2602
+ const raw = await this.httpClient.post(
2603
+ `/project/v1/files/${segment(fileId)}/comments`,
2604
+ toCreateCommentThreadRequest(input),
2605
+ jwtToken,
2606
+ { headers: this.writeHeaders(opts) }
2607
+ );
2608
+ return transformSpecFileCommentThreadResponse(raw);
2609
+ }
2610
+ async replyToThread(fileId, threadId, body, jwtToken, opts) {
2611
+ const raw = await this.httpClient.post(
2612
+ `/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/replies`,
2613
+ toCreateCommentReplyRequest(body),
2614
+ jwtToken,
2615
+ { headers: this.writeHeaders(opts) }
2616
+ );
2617
+ return transformSpecFileCommentResponse(raw);
2618
+ }
2619
+ async editComment(fileId, commentId, body, jwtToken, opts) {
2620
+ const raw = await this.httpClient.patch(
2621
+ `/project/v1/files/${segment(fileId)}/comments/${segment(commentId)}`,
2622
+ toUpdateCommentRequest(body),
2623
+ jwtToken,
2624
+ { headers: this.writeHeaders(opts) }
2625
+ );
2626
+ return transformSpecFileCommentResponse(raw);
2627
+ }
2628
+ async deleteComment(fileId, commentId, jwtToken, opts) {
2629
+ const raw = await this.httpClient.delete(
2630
+ `/project/v1/files/${segment(fileId)}/comments/${segment(commentId)}`,
2631
+ jwtToken,
2632
+ { headers: this.writeHeaders(opts) }
2633
+ );
2634
+ return transformDeleteCommentResult(raw);
2635
+ }
2636
+ async resolveThread(fileId, threadId, jwtToken, opts) {
2637
+ const raw = await this.httpClient.put(
2638
+ `/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/resolve`,
2639
+ {},
2640
+ jwtToken,
2641
+ { headers: this.writeHeaders(opts) }
2642
+ );
2643
+ return transformSpecFileCommentThreadResponse(raw);
2644
+ }
2645
+ async unresolveThread(fileId, threadId, jwtToken, opts) {
2646
+ const raw = await this.httpClient.delete(
2647
+ `/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/resolve`,
2648
+ jwtToken,
2649
+ { headers: this.writeHeaders(opts) }
2650
+ );
2651
+ return transformSpecFileCommentThreadResponse(raw);
2652
+ }
2653
+ async reanchorThread(fileId, threadId, anchor, jwtToken, opts) {
2654
+ const raw = await this.httpClient.post(
2655
+ `/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/reanchor`,
2656
+ toReanchorCommentThreadRequest(anchor),
2657
+ jwtToken,
2658
+ { headers: this.writeHeaders(opts) }
2659
+ );
2660
+ return transformSpecFileCommentThreadResponse(raw);
2661
+ }
2662
+ async getProjectCommentCounts(projectId, jwtToken) {
2663
+ const raw = await this.httpClient.get(
2664
+ `/project/v1/projects/${segment(projectId)}/comment-counts`,
2665
+ jwtToken,
2666
+ {}
2667
+ );
2668
+ return transformProjectCommentCounts(raw);
2669
+ }
2670
+ async listProjectCommentActivity(projectId, jwtToken, opts) {
2671
+ const query = new URLSearchParams();
2672
+ if (opts?.limit !== void 0) {
2673
+ query.set("limit", String(opts.limit));
2674
+ }
2675
+ if (opts?.offset !== void 0) {
2676
+ query.set("offset", String(opts.offset));
2677
+ }
2678
+ const suffix = query.toString() ? `?${query.toString()}` : "";
2679
+ const raw = await this.httpClient.get(
2680
+ `/project/v1/projects/${segment(projectId)}/comments${suffix}`,
2681
+ jwtToken,
2682
+ {}
2683
+ );
2684
+ return transformProjectCommentActivity(raw);
2685
+ }
2686
+ };
2687
+
2443
2688
  // ../../packages/platform-client/artifact/types/artifact.types.ts
2444
2689
  function transformArtifactResponse(raw) {
2445
2690
  return {
@@ -2570,15 +2815,15 @@ function buildUpdateArtifactBody(request) {
2570
2815
 
2571
2816
  // ../../packages/platform-client/artifact/http/artifact.http-client.ts
2572
2817
  var BASE_PATH = "/artifact/v1";
2573
- var HEADER_EXPECTED_VERSION2 = "X-Expected-Version";
2574
- var HEADER_ACTOR_KIND2 = "X-Actor-Kind";
2818
+ var HEADER_EXPECTED_VERSION3 = "X-Expected-Version";
2819
+ var HEADER_ACTOR_KIND3 = "X-Actor-Kind";
2575
2820
  function writeHeaders(opts) {
2576
2821
  const headers = {};
2577
2822
  if (opts?.expectedVersion !== void 0) {
2578
- headers[HEADER_EXPECTED_VERSION2] = String(opts.expectedVersion);
2823
+ headers[HEADER_EXPECTED_VERSION3] = String(opts.expectedVersion);
2579
2824
  }
2580
2825
  if (opts?.actorKind) {
2581
- headers[HEADER_ACTOR_KIND2] = opts.actorKind;
2826
+ headers[HEADER_ACTOR_KIND3] = opts.actorKind;
2582
2827
  }
2583
2828
  return headers;
2584
2829
  }
@@ -3155,7 +3400,8 @@ var PlatformClient = class {
3155
3400
  file: new HttpFileClient(httpClient),
3156
3401
  attachment: new HttpAttachmentClient(httpClient),
3157
3402
  artifact: new HttpArtifactClient(httpClient),
3158
- streamToken: new HttpStreamTokenClient(httpClient)
3403
+ streamToken: new HttpStreamTokenClient(httpClient),
3404
+ comment: new HttpSpecFileCommentClient(httpClient)
3159
3405
  };
3160
3406
  }
3161
3407
  // ------------------------------------------------------- stream tokens
@@ -3205,6 +3451,60 @@ var PlatformClient = class {
3205
3451
  (jwt, c) => c.artifact.rollback(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
3206
3452
  );
3207
3453
  }
3454
+ // ------------------------------------------------------------- comments
3455
+ //
3456
+ // Review discussion on a spec file. Every WRITE below carries
3457
+ // `actorKind: 'ai'` (AR-MCP-3): an MCP client is an assistant acting on the
3458
+ // user's behalf, and it writes under the USER'S OWN JWT, so the header is the
3459
+ // only thing that can tell a person's comment from a model's. Omitting it
3460
+ // would not fail -- the platform's permissive parse on these paths defaults
3461
+ // to `human` -- it would just file the comment under the user's name.
3462
+ //
3463
+ // There is deliberately no resolve, unresolve or re-anchor method here, and
3464
+ // no tool for one (AR-MCP-2). The platform refuses `ai` on those three routes
3465
+ // with a 403, so a method would only ever produce an error the model then has
3466
+ // to interpret.
3467
+ /** Lists a spec file's comment threads, optionally filtered by status. */
3468
+ async listSpecFileComments(fileId, opts = {}) {
3469
+ return this.withTokenRetry(
3470
+ (jwt, c) => c.comment.listThreads(fileId, jwt, opts.filter ? { filter: opts.filter } : {})
3471
+ );
3472
+ }
3473
+ /** Opens a thread on a passage. The server locates the quote and derives the offsets. */
3474
+ async createSpecFileCommentThread(fileId, input) {
3475
+ return this.withTokenRetry(
3476
+ (jwt, c) => c.comment.createThread(fileId, input, jwt, { actorKind: "ai" })
3477
+ );
3478
+ }
3479
+ /** Replies to an existing thread. */
3480
+ async replyToSpecFileCommentThread(fileId, threadId, body) {
3481
+ return this.withTokenRetry(
3482
+ (jwt, c) => c.comment.replyToThread(fileId, threadId, body, jwt, { actorKind: "ai" })
3483
+ );
3484
+ }
3485
+ /**
3486
+ * Edits one of the caller's own comments.
3487
+ *
3488
+ * `expectedVersion` is the COMMENT's own contentVersion, not the file's:
3489
+ * pairing it with the file would make an edit fail because somebody saved the
3490
+ * document, which has nothing to do with whether this comment moved. Only the
3491
+ * caller's own value is sent -- substituting one read a moment ago would
3492
+ * guard nothing, exactly as in saveSpecFileRevision.
3493
+ */
3494
+ async updateSpecFileComment(fileId, commentId, body, opts = {}) {
3495
+ return this.withTokenRetry(
3496
+ (jwt, c) => c.comment.editComment(fileId, commentId, body, jwt, {
3497
+ ...opts.expectedVersion === void 0 ? {} : { expectedVersion: opts.expectedVersion },
3498
+ actorKind: "ai"
3499
+ })
3500
+ );
3501
+ }
3502
+ /** Soft-deletes one of the caller's own comments. */
3503
+ async deleteSpecFileComment(fileId, commentId) {
3504
+ return this.withTokenRetry(
3505
+ (jwt, c) => c.comment.deleteComment(fileId, commentId, jwt, { actorKind: "ai" })
3506
+ );
3507
+ }
3208
3508
  async withTokenRetry(call) {
3209
3509
  const clients = await this.clients();
3210
3510
  const jwt = await this.tokenManager.getValidAccessToken();
@@ -4790,11 +5090,245 @@ function conflictMessage(filePath, conflict) {
4790
5090
  return `${filePath} was NOT updated: ${who} changed it${when} since you read it.${rev} Their version was kept \u2014 nothing of theirs was overwritten. Re-read the file with get_spec_file (or read_spec_file), decide how your change should combine with theirs, then update again passing the content_version that re-read returns as expected_version.${version} Do not simply resend the same content \u2014 you would undo their work.`;
4791
5091
  }
4792
5092
 
5093
+ // src/server/tools/spec_file_comments.ts
5094
+ import { z as z24 } from "zod";
5095
+ var LIST_BODY_PREVIEW_CHARS = 1e3;
5096
+ var LIST_MAX_THREADS = 100;
5097
+ function commentSummary(comment, opts = {}) {
5098
+ const body = comment.body;
5099
+ const truncated = opts.preview === true && body !== void 0 && body.length > LIST_BODY_PREVIEW_CHARS;
5100
+ return {
5101
+ comment_id: comment.id,
5102
+ thread_id: comment.threadId,
5103
+ author_id: comment.authorId,
5104
+ author_kind: comment.authorKind,
5105
+ // Absent for a tombstone: a soft-deleted root kept so the thread still has
5106
+ // a head to render. `deleted` is the flag to branch on, not a missing body.
5107
+ body: truncated ? body.slice(0, LIST_BODY_PREVIEW_CHARS) : body,
5108
+ // Only ever present and true, so a caller can test it without knowing which
5109
+ // shape produced the comment.
5110
+ ...truncated ? { body_truncated: true } : {},
5111
+ deleted: comment.deleted,
5112
+ content_version: comment.contentVersion,
5113
+ created_at: comment.createdAt?.toISOString(),
5114
+ updated_at: comment.updatedAt?.toISOString()
5115
+ };
5116
+ }
5117
+ function threadSummary(thread, opts = {}) {
5118
+ return {
5119
+ thread_id: thread.id,
5120
+ spec_file_id: thread.specFileId,
5121
+ root_comment_id: thread.rootCommentId,
5122
+ anchor_quote: thread.anchorQuote,
5123
+ // 'anchored' or 'orphaned'. An orphan's passage can no longer be located,
5124
+ // and only a person can repair it — there is no tool here that can.
5125
+ anchor_status: thread.anchorStatus,
5126
+ anchor_start: thread.anchorStart,
5127
+ anchor_end: thread.anchorEnd,
5128
+ // Display only: revision_count is reusable and decrements on revert.
5129
+ anchor_revision: thread.anchorRevision,
5130
+ resolved: thread.resolvedAt !== null,
5131
+ resolved_by: thread.resolvedBy,
5132
+ resolved_at: thread.resolvedAt?.toISOString(),
5133
+ created_by: thread.createdBy,
5134
+ created_by_kind: thread.createdByKind,
5135
+ created_at: thread.createdAt?.toISOString(),
5136
+ comments: thread.comments.map((c) => commentSummary(c, opts))
5137
+ };
5138
+ }
5139
+ function uuidField(what) {
5140
+ return z24.uuid().describe(what);
5141
+ }
5142
+ var CONFLICT_REASON = {
5143
+ fileTrashed: "file_trashed",
5144
+ lockTimeout: "lock_timeout",
5145
+ contentMoved: "content_moved",
5146
+ threadNotOrphaned: "thread_not_orphaned",
5147
+ versionConflict: "version_conflict"
5148
+ };
5149
+ function conflictAdvice(err, parts) {
5150
+ const reason = conflictOf(err)?.reason;
5151
+ switch (reason) {
5152
+ case CONFLICT_REASON.lockTimeout:
5153
+ return "Nothing was written: another write held the file briefly and this one timed out waiting. Retry the same call unchanged.";
5154
+ case CONFLICT_REASON.contentMoved:
5155
+ return "Nothing was written: the file changed while the passage was being located, so the anchor would have pointed at text that is no longer current. Re-read the file, find the passage again, and retry.";
5156
+ case CONFLICT_REASON.fileTrashed:
5157
+ return parts.whenTrashed;
5158
+ default:
5159
+ return parts.otherwise;
5160
+ }
5161
+ }
5162
+ var listInputSchema = {
5163
+ file_id: uuidField("UUID of the spec file whose comments to list."),
5164
+ status: z24.enum(["open", "resolved", "orphaned"]).optional().describe(
5165
+ "Narrow to open (unresolved) threads, resolved ones, or orphaned ones whose passage can no longer be located. Omit for all of them."
5166
+ )
5167
+ };
5168
+ var listSpecFileCommentsTool = {
5169
+ name: "list_spec_file_comments",
5170
+ description: "List the review comment threads on a spec file. Each thread carries the passage it is anchored to, whether it is resolved, its anchor status, and its comments. Works on a trashed file as well as a live one. Read this before editing a file under review. Returns at most 100 threads (with total_threads and truncated) and previews each comment body to 1000 characters, flagging it with body_truncated; narrow with `status` to see fewer threads in full.",
5171
+ inputSchema: listInputSchema,
5172
+ handler: async (args, ctx) => {
5173
+ const threads = await ctx.client.listSpecFileComments(args.file_id, {
5174
+ ...args.status ? { filter: args.status } : {}
5175
+ });
5176
+ const page = threads.slice(0, LIST_MAX_THREADS);
5177
+ return jsonResult({
5178
+ threads: page.map((t) => threadSummary(t, { preview: true })),
5179
+ total_threads: threads.length,
5180
+ // Only when something was actually withheld, so its absence means "you
5181
+ // have all of it" rather than "nobody thought about it".
5182
+ ...threads.length > page.length ? { truncated: true } : {}
5183
+ });
5184
+ }
5185
+ };
5186
+ var createInputSchema = {
5187
+ file_id: uuidField("UUID of the spec file to comment on."),
5188
+ body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The comment text, in markdown."),
5189
+ // The three anchor limits are exported by the contract for exactly this, and
5190
+ // the context one is SHORT (64). A description reading "the text immediately
5191
+ // BEFORE the quote" invites a whole preceding sentence, which is the default
5192
+ // reading rather than a corner case — so the ceiling is enforced in the
5193
+ // schema, where the model sees it, rather than discovered as a 400.
5194
+ anchor_quote: z24.string().min(1).max(MAX_ANCHOR_QUOTE_LENGTH).describe(
5195
+ `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.`
5196
+ ),
5197
+ anchor_prefix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
5198
+ `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.`
5199
+ ),
5200
+ anchor_suffix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
5201
+ `The text immediately AFTER the quote. At most ${String(MAX_ANCHOR_CONTEXT_LENGTH)} characters. Empty when the quote ends the file.`
5202
+ )
5203
+ };
5204
+ var createSpecFileCommentThreadTool = {
5205
+ name: "create_spec_file_comment_thread",
5206
+ description: "Open a review comment on a specific passage of a spec file. Quote the passage exactly as it appears. Do NOT send character offsets \u2014 the server locates the quote itself. Refused with a conflict on a trashed file: a file staged for deletion does not gather new discussion.",
5207
+ inputSchema: createInputSchema,
5208
+ handler: async (args, ctx) => {
5209
+ try {
5210
+ const thread = await ctx.client.createSpecFileCommentThread(args.file_id, {
5211
+ body: args.body,
5212
+ anchorQuote: args.anchor_quote,
5213
+ // Always sent, even empty: an omitted key is indistinguishable from a
5214
+ // client that does not know about the field.
5215
+ anchorPrefix: args.anchor_prefix ?? "",
5216
+ anchorSuffix: args.anchor_suffix ?? ""
5217
+ });
5218
+ return jsonResult({ thread: threadSummary(thread) });
5219
+ } catch (err) {
5220
+ if (isPlatformConflictError(err)) {
5221
+ return errorResult(
5222
+ `No thread was created on ${args.file_id}. ` + conflictAdvice(err, {
5223
+ // CONFIRMED trashed: the platform said so, so this states it.
5224
+ whenTrashed: "The file is in the Trash Bin, so it gathers no new discussion. Restore it with restore_spec_file_from_trash first.",
5225
+ // NOT confirmed — an older platform, or a reason this build does
5226
+ // not know. Worded as a hypothesis with a way to check it, which
5227
+ // is also what keeps the two branches distinguishable in a test:
5228
+ // asserting the trashed wording alone would pass against this one
5229
+ // too, so deleting the branch above would go unnoticed.
5230
+ otherwise: "The write was refused as a conflict, and the platform gave no reason. The usual cause is a trashed file \u2014 confirm with get_spec_file before doing anything else."
5231
+ })
5232
+ );
5233
+ }
5234
+ throw err;
5235
+ }
5236
+ }
5237
+ };
5238
+ var replyInputSchema = {
5239
+ file_id: uuidField("UUID of the spec file the thread belongs to."),
5240
+ thread_id: uuidField("UUID of the thread to reply to, from list_spec_file_comments."),
5241
+ body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The reply text, in markdown.")
5242
+ };
5243
+ var replySpecFileCommentThreadTool = {
5244
+ name: "reply_spec_file_comment_thread",
5245
+ description: "Reply to an existing review comment thread on a spec file. Use this to answer a reviewer's question or report what you changed. Refused with a conflict on a trashed file.",
5246
+ inputSchema: replyInputSchema,
5247
+ handler: async (args, ctx) => {
5248
+ try {
5249
+ const comment = await ctx.client.replyToSpecFileCommentThread(
5250
+ args.file_id,
5251
+ args.thread_id,
5252
+ args.body
5253
+ );
5254
+ return jsonResult({ comment: commentSummary(comment) });
5255
+ } catch (err) {
5256
+ if (isPlatformConflictError(err)) {
5257
+ return errorResult(
5258
+ `No reply was posted to ${args.thread_id}. ` + conflictAdvice(err, {
5259
+ whenTrashed: "The file is in the Trash Bin, so it gathers no new discussion. Restore it with restore_spec_file_from_trash first.",
5260
+ otherwise: "The reply was refused as a conflict, and the platform gave no reason. The usual cause is a trashed file \u2014 confirm with get_spec_file before doing anything else."
5261
+ })
5262
+ );
5263
+ }
5264
+ throw err;
5265
+ }
5266
+ }
5267
+ };
5268
+ var updateInputSchema = {
5269
+ file_id: uuidField("UUID of the spec file the comment belongs to."),
5270
+ comment_id: uuidField("UUID of the comment to edit. Must be your own."),
5271
+ body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The replacement text, in markdown. Replaces the comment entirely."),
5272
+ expected_version: z24.number().int().nonnegative().optional().describe(
5273
+ "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."
5274
+ )
5275
+ };
5276
+ var updateSpecFileCommentTool = {
5277
+ name: "update_spec_file_comment",
5278
+ description: "Edit the body of one of YOUR OWN comments on a spec file. The platform refuses a comment written by somebody else. Pass the comment's own content_version as expected_version to avoid overwriting an edit made since you read it. Works on a trashed file.",
5279
+ inputSchema: updateInputSchema,
5280
+ handler: async (args, ctx) => {
5281
+ try {
5282
+ const comment = await ctx.client.updateSpecFileComment(
5283
+ args.file_id,
5284
+ args.comment_id,
5285
+ args.body,
5286
+ {
5287
+ // Only the caller's own value is sent. Substituting a version read a
5288
+ // moment ago would guard nothing — it cannot detect an edit made before
5289
+ // that read, which is the edit that matters.
5290
+ ...args.expected_version === void 0 ? {} : { expectedVersion: args.expected_version }
5291
+ }
5292
+ );
5293
+ return jsonResult({ comment: commentSummary(comment) });
5294
+ } catch (err) {
5295
+ if (isPlatformConflictError(err)) {
5296
+ return errorResult(
5297
+ `${args.comment_id} was NOT edited. ` + conflictAdvice(err, {
5298
+ // An edit is allowed on a trashed file (AR-PRJ-45), so the
5299
+ // trashed reason cannot reach this route — both branches below
5300
+ // describe the version race, which is the only conflict it emits.
5301
+ whenTrashed: "It has changed since the version you passed. Re-read it with list_spec_file_comments, re-apply your change, and retry with the new content_version.",
5302
+ otherwise: "It has changed since the version you passed. Re-read it with list_spec_file_comments, re-apply your change, and retry with the new content_version."
5303
+ })
5304
+ );
5305
+ }
5306
+ throw err;
5307
+ }
5308
+ }
5309
+ };
5310
+ var deleteInputSchema = {
5311
+ file_id: uuidField("UUID of the spec file the comment belongs to."),
5312
+ comment_id: uuidField("UUID of the comment to delete. Must be your own.")
5313
+ };
5314
+ var deleteSpecFileCommentTool = {
5315
+ name: "delete_spec_file_comment",
5316
+ description: "Soft-delete one of YOUR OWN comments on a spec file. Three outcomes, reported in the result: a reply simply disappears (comment: null); a first comment with live replies becomes a tombstone so the thread keeps a head (comment: the tombstone); and a first comment with no live replies takes the whole thread (thread_deleted: true).",
5317
+ inputSchema: deleteInputSchema,
5318
+ handler: async (args, ctx) => {
5319
+ const result = await ctx.client.deleteSpecFileComment(args.file_id, args.comment_id);
5320
+ return jsonResult({
5321
+ comment: result.comment ? commentSummary(result.comment) : null,
5322
+ thread_deleted: result.threadDeleted
5323
+ });
5324
+ }
5325
+ };
5326
+
4793
5327
  // src/server/tools/upload_attachment.ts
4794
5328
  import { createHash as createHash3 } from "crypto";
4795
5329
  import { lstat, readFile as readFile2 } from "fs/promises";
4796
5330
  import { basename, extname, isAbsolute as isAbsolute2 } from "path";
4797
- import { z as z24 } from "zod";
5331
+ import { z as z25 } from "zod";
4798
5332
  var DEFAULT_MIME = "application/octet-stream";
4799
5333
  var MAX_ATTACHMENT_MB2 = MAX_ATTACHMENT_BYTES / (1024 * 1024);
4800
5334
  var STRUCTURED_EXTENSION_MIME = {
@@ -4900,15 +5434,15 @@ function inferMimeFromName(name) {
4900
5434
  return DEFAULT_MIME;
4901
5435
  }
4902
5436
  var inputSchema24 = {
4903
- project_id: z24.string().min(1).describe("UUID of the project to attach the file to."),
4904
- file_path: z24.string().min(1).describe(
5437
+ project_id: z25.string().min(1).describe("UUID of the project to attach the file to."),
5438
+ file_path: z25.string().min(1).describe(
4905
5439
  "Absolute path to the local file to upload. The MCP server reads this path from its own host."
4906
5440
  ),
4907
- file_name: z24.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
4908
- mime_type: z24.string().min(1).optional().describe(
5441
+ file_name: z25.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
5442
+ mime_type: z25.string().min(1).optional().describe(
4909
5443
  "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."
4910
5444
  ),
4911
- override: z24.boolean().optional().describe(
5445
+ override: z25.boolean().optional().describe(
4912
5446
  "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)."
4913
5447
  )
4914
5448
  };
@@ -5039,7 +5573,7 @@ var uploadAttachmentTool = {
5039
5573
  };
5040
5574
 
5041
5575
  // src/server/tools/upload_spec_file.ts
5042
- import { z as z25 } from "zod";
5576
+ import { z as z26 } from "zod";
5043
5577
 
5044
5578
  // src/server/spec-path.ts
5045
5579
  var MAX_SPEC_DIR_DEPTH = 3;
@@ -5096,17 +5630,17 @@ var SPEC_FILE_TYPES = [
5096
5630
  "openspec-spec"
5097
5631
  ];
5098
5632
  var inputSchema25 = {
5099
- project_id: z25.string().min(1).describe("UUID of the project to upload the file into."),
5100
- file_path: z25.string().min(1).describe(
5633
+ project_id: z26.string().min(1).describe("UUID of the project to upload the file into."),
5634
+ file_path: z26.string().min(1).describe(
5101
5635
  '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).'
5102
5636
  ),
5103
- content: z25.string().min(1).optional().describe(
5637
+ content: z26.string().min(1).optional().describe(
5104
5638
  "Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
5105
5639
  ),
5106
- local_file_path: z25.string().min(1).optional().describe(
5640
+ local_file_path: z26.string().min(1).optional().describe(
5107
5641
  "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."
5108
5642
  ),
5109
- file_type: z25.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
5643
+ file_type: z26.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
5110
5644
  };
5111
5645
  var uploadSpecFileTool = {
5112
5646
  name: "upload_spec_file",
@@ -5142,16 +5676,16 @@ var uploadSpecFileTool = {
5142
5676
  };
5143
5677
 
5144
5678
  // src/server/tools/list_artifacts.ts
5145
- import { z as z27 } from "zod";
5679
+ import { z as z28 } from "zod";
5146
5680
 
5147
5681
  // src/server/tools/artifact-output.ts
5148
- import { z as z26 } from "zod";
5149
- var artifactFilesSchema = z26.array(
5150
- z26.object({
5151
- path: z26.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
5152
- content: z26.string().describe("The file content, in `encoding` (default utf8)."),
5153
- encoding: z26.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
5154
- content_type: z26.string().optional().describe("Hint used only for extensions the platform does not recognise.")
5682
+ import { z as z27 } from "zod";
5683
+ var artifactFilesSchema = z27.array(
5684
+ z27.object({
5685
+ path: z27.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
5686
+ content: z27.string().describe("The file content, in `encoding` (default utf8)."),
5687
+ encoding: z27.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
5688
+ content_type: z27.string().optional().describe("Hint used only for extensions the platform does not recognise.")
5155
5689
  })
5156
5690
  ).describe("Files of the bundle.");
5157
5691
  function artifactSummary(a) {
@@ -5224,9 +5758,9 @@ function artifactCreateConflictMessage(conflict) {
5224
5758
  var LIST_DEFAULT4 = 50;
5225
5759
  var LIST_MAX4 = 100;
5226
5760
  var inputSchema26 = {
5227
- project_id: z27.string().min(1).describe("UUID of the project whose artifacts to list."),
5228
- limit: z27.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
5229
- offset: z27.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
5761
+ project_id: z28.string().min(1).describe("UUID of the project whose artifacts to list."),
5762
+ limit: z28.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
5763
+ offset: z28.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
5230
5764
  };
5231
5765
  var listArtifactsTool = {
5232
5766
  name: "list_artifacts",
@@ -5249,10 +5783,10 @@ var listArtifactsTool = {
5249
5783
  };
5250
5784
 
5251
5785
  // src/server/tools/get_artifact.ts
5252
- import { z as z28 } from "zod";
5786
+ import { z as z29 } from "zod";
5253
5787
  var inputSchema27 = {
5254
- artifact_id: z28.string().min(1).describe("UUID of the artifact."),
5255
- revision: z28.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
5788
+ artifact_id: z29.string().min(1).describe("UUID of the artifact."),
5789
+ revision: z29.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
5256
5790
  };
5257
5791
  var getArtifactTool = {
5258
5792
  name: "get_artifact",
@@ -5274,14 +5808,14 @@ var getArtifactTool = {
5274
5808
  };
5275
5809
 
5276
5810
  // src/server/tools/read_artifact_file.ts
5277
- import { z as z29 } from "zod";
5811
+ import { z as z30 } from "zod";
5278
5812
  var MAX_READABLE_ARTIFACT_FILE_BYTES = 8 * 1024 * 1024;
5279
5813
  var inputSchema28 = {
5280
- artifact_id: z29.string().min(1).describe("UUID of the artifact."),
5281
- path: z29.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
5282
- revision: z29.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
5283
- offset: z29.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
5284
- limit: z29.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
5814
+ artifact_id: z30.string().min(1).describe("UUID of the artifact."),
5815
+ path: z30.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
5816
+ revision: z30.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
5817
+ offset: z30.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
5818
+ limit: z30.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
5285
5819
  };
5286
5820
  var readArtifactFileTool = {
5287
5821
  name: "read_artifact_file",
@@ -5321,14 +5855,14 @@ var readArtifactFileTool = {
5321
5855
  };
5322
5856
 
5323
5857
  // src/server/tools/create_artifact.ts
5324
- import { z as z30 } from "zod";
5858
+ import { z as z31 } from "zod";
5325
5859
  var inputSchema29 = {
5326
- project_id: z30.string().min(1).describe("UUID of the project to create the artifact in."),
5327
- name: z30.string().min(1).describe("Display name (max 200 characters)."),
5328
- slug: z30.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
5329
- description: z30.string().optional().describe("Optional description (max 2000 characters)."),
5330
- entry_path: z30.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
5331
- message: z30.string().optional().describe("Short summary of this first revision (max 500 characters)."),
5860
+ project_id: z31.string().min(1).describe("UUID of the project to create the artifact in."),
5861
+ name: z31.string().min(1).describe("Display name (max 200 characters)."),
5862
+ slug: z31.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
5863
+ description: z31.string().optional().describe("Optional description (max 2000 characters)."),
5864
+ entry_path: z31.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
5865
+ message: z31.string().optional().describe("Short summary of this first revision (max 500 characters)."),
5332
5866
  files: artifactFilesSchema
5333
5867
  };
5334
5868
  var createArtifactTool = {
@@ -5362,19 +5896,19 @@ var createArtifactTool = {
5362
5896
  };
5363
5897
 
5364
5898
  // src/server/tools/write_artifact_revision.ts
5365
- import { z as z31 } from "zod";
5899
+ import { z as z32 } from "zod";
5366
5900
  var inputSchema30 = {
5367
- artifact_id: z31.string().min(1).describe("UUID of the artifact."),
5901
+ artifact_id: z32.string().min(1).describe("UUID of the artifact."),
5368
5902
  files: artifactFilesSchema,
5369
- base_revision: z31.number().int().positive().optional().describe(
5903
+ base_revision: z32.number().int().positive().optional().describe(
5370
5904
  "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."
5371
5905
  ),
5372
- remove: z31.array(z31.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
5373
- expected_version: z31.number().int().nonnegative().optional().describe(
5906
+ remove: z32.array(z32.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
5907
+ expected_version: z32.number().int().nonnegative().optional().describe(
5374
5908
  "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."
5375
5909
  ),
5376
- message: z31.string().optional().describe("Short summary of the change (max 500 characters)."),
5377
- entry_path: z31.string().optional().describe("Change the file a renderer opens first, from this revision on.")
5910
+ message: z32.string().optional().describe("Short summary of the change (max 500 characters)."),
5911
+ entry_path: z32.string().optional().describe("Change the file a renderer opens first, from this revision on.")
5378
5912
  };
5379
5913
  var writeArtifactRevisionTool = {
5380
5914
  name: "write_artifact_revision",
@@ -5413,12 +5947,12 @@ var writeArtifactRevisionTool = {
5413
5947
  };
5414
5948
 
5415
5949
  // src/server/tools/rollback_artifact.ts
5416
- import { z as z32 } from "zod";
5950
+ import { z as z33 } from "zod";
5417
5951
  var inputSchema31 = {
5418
- artifact_id: z32.string().min(1).describe("UUID of the artifact."),
5419
- revision_number: z32.number().int().positive().describe("The revision whose files to restore."),
5420
- expected_version: z32.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
5421
- message: z32.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
5952
+ artifact_id: z33.string().min(1).describe("UUID of the artifact."),
5953
+ revision_number: z33.number().int().positive().describe("The revision whose files to restore."),
5954
+ expected_version: z33.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
5955
+ message: z33.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
5422
5956
  };
5423
5957
  var rollbackArtifactTool = {
5424
5958
  name: "rollback_artifact",
@@ -5443,10 +5977,10 @@ var rollbackArtifactTool = {
5443
5977
  };
5444
5978
 
5445
5979
  // src/server/tools/create_stream_token.ts
5446
- import { z as z34 } from "zod";
5980
+ import { z as z35 } from "zod";
5447
5981
 
5448
5982
  // src/server/tools/stream-token-output.ts
5449
- import { z as z33 } from "zod";
5983
+ import { z as z34 } from "zod";
5450
5984
  var projectStreamEventTypes = [
5451
5985
  "project.updated",
5452
5986
  "project.deleted",
@@ -5454,6 +5988,10 @@ var projectStreamEventTypes = [
5454
5988
  "spec_file.updated",
5455
5989
  "spec_file.deleted",
5456
5990
  "spec_file.lock",
5991
+ "spec_file_comment.created",
5992
+ "spec_file_comment.updated",
5993
+ "spec_file_comment.thread_state_changed",
5994
+ "spec_file_comment.anchors_updated",
5457
5995
  "attachment.created",
5458
5996
  "attachment.deleted",
5459
5997
  "spec_session.created",
@@ -5461,8 +5999,8 @@ var projectStreamEventTypes = [
5461
5999
  "spec_session.deleted",
5462
6000
  "*"
5463
6001
  ];
5464
- var streamEventTypesSchema = z33.array(z33.enum(projectStreamEventTypes)).min(1).describe(
5465
- '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.'
6002
+ var streamEventTypesSchema = z34.array(z34.enum(projectStreamEventTypes)).min(1).describe(
6003
+ '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.'
5466
6004
  );
5467
6005
  function streamTokenSummary(token) {
5468
6006
  return {
@@ -5492,13 +6030,13 @@ function streamTokenMintOutput(result) {
5492
6030
 
5493
6031
  // src/server/tools/create_stream_token.ts
5494
6032
  var inputSchema32 = {
5495
- resource_type: z34.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
5496
- resource_id: z34.string().min(1).describe("UUID of the project to stream events for."),
6033
+ resource_type: z35.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
6034
+ resource_id: z35.string().min(1).describe("UUID of the project to stream events for."),
5497
6035
  event_types: streamEventTypesSchema,
5498
- ttl_seconds: z34.number().int().positive().optional().describe(
6036
+ ttl_seconds: z35.number().int().positive().optional().describe(
5499
6037
  "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."
5500
6038
  ),
5501
- label: z34.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
6039
+ label: z35.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
5502
6040
  };
5503
6041
  var createStreamTokenTool = {
5504
6042
  name: "create_stream_token",
@@ -5517,17 +6055,17 @@ var createStreamTokenTool = {
5517
6055
  };
5518
6056
 
5519
6057
  // src/server/tools/list_stream_tokens.ts
5520
- import { z as z35 } from "zod";
6058
+ import { z as z36 } from "zod";
5521
6059
  var LIST_DEFAULT5 = 50;
5522
6060
  var LIST_MAX5 = 200;
5523
6061
  var inputSchema33 = {
5524
- resource_id: z35.string().optional().describe("Only tokens following this project."),
5525
- live_only: z35.boolean().optional().describe("Hide expired and revoked tokens."),
5526
- mine_only: z35.boolean().optional().describe(
6062
+ resource_id: z36.string().optional().describe("Only tokens following this project."),
6063
+ live_only: z36.boolean().optional().describe("Hide expired and revoked tokens."),
6064
+ mine_only: z36.boolean().optional().describe(
5527
6065
  "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."
5528
6066
  ),
5529
- limit: z35.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
5530
- offset: z35.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
6067
+ limit: z36.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
6068
+ offset: z36.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
5531
6069
  };
5532
6070
  var listStreamTokensTool = {
5533
6071
  name: "list_stream_tokens",
@@ -5555,9 +6093,9 @@ var listStreamTokensTool = {
5555
6093
  };
5556
6094
 
5557
6095
  // src/server/tools/revoke_stream_token.ts
5558
- import { z as z36 } from "zod";
6096
+ import { z as z37 } from "zod";
5559
6097
  var inputSchema34 = {
5560
- id: z36.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
6098
+ id: z37.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
5561
6099
  };
5562
6100
  var revokeStreamTokenTool = {
5563
6101
  name: "revoke_stream_token",
@@ -5570,7 +6108,7 @@ var revokeStreamTokenTool = {
5570
6108
  };
5571
6109
 
5572
6110
  // src/server/tools/start_spec_session.ts
5573
- import { z as z37 } from "zod";
6111
+ import { z as z38 } from "zod";
5574
6112
 
5575
6113
  // src/server/tools/start-session-core.ts
5576
6114
  var START_SESSION_WAIT_MS = 75e3;
@@ -5963,27 +6501,27 @@ var START_SESSION_DESCRIPTION_BASE = "Start a NEW MySpec chat session on the use
5963
6501
  // src/server/tools/start_spec_session.ts
5964
6502
  var BRIDGE_COMMAND = "npx -y @myspec/mcp-server reverse --root <path>";
5965
6503
  var inputSchema35 = {
5966
- project_id: z37.string().min(1).describe("UUID of the project to start the session in."),
5967
- mode: z37.enum(["generate", "edit"]).describe(
6504
+ project_id: z38.string().min(1).describe("UUID of the project to start the session in."),
6505
+ mode: z38.enum(["generate", "edit"]).describe(
5968
6506
  "'generate' writes a new spec bundle from scratch; 'edit' changes the spec files of an existing bundle."
5969
6507
  ),
5970
- prompt: z37.string().min(1).describe(
6508
+ prompt: z38.string().min(1).describe(
5971
6509
  "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."
5972
6510
  ),
5973
- workflow_id: z37.string().optional().describe(
6511
+ workflow_id: z38.string().optional().describe(
5974
6512
  "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."
5975
6513
  ),
5976
- language: z37.enum(SPEC_SESSION_LANGUAGES).optional().describe(
6514
+ language: z38.enum(SPEC_SESSION_LANGUAGES).optional().describe(
5977
6515
  "Output language the session locks to on its first turn. Defaults to 'en'. A session's language is immutable once set."
5978
6516
  ),
5979
- model_id: z37.string().optional().describe("Model id to run the session with, e.g. openai:gpt-6-astra."),
5980
- attachment_ids: z37.array(z37.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
6517
+ model_id: z38.string().optional().describe("Model id to run the session with, e.g. openai:gpt-6-astra."),
6518
+ attachment_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
5981
6519
  `Project attachment ids to cite in the opening message, as the composer @-mentions do. At most ${String(MAX_REFERENCE_IDS)}.`
5982
6520
  ),
5983
- spec_file_ids: z37.array(z37.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
6521
+ spec_file_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
5984
6522
  `Existing spec file ids to cite in the opening message. Required in practice for edit sessions. At most ${String(MAX_REFERENCE_IDS)}.`
5985
6523
  ),
5986
- session_title: z37.string().min(1).max(SESSION_TITLE_MAX_LENGTH).optional().describe(
6524
+ session_title: z38.string().min(1).max(SESSION_TITLE_MAX_LENGTH).optional().describe(
5987
6525
  `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.`
5988
6526
  )
5989
6527
  };
@@ -6213,6 +6751,11 @@ function buildServer(deps) {
6213
6751
  registerTool(server, listStreamTokensTool, ctx);
6214
6752
  registerTool(server, revokeStreamTokenTool, ctx);
6215
6753
  registerTool(server, startSpecSessionTool, ctx);
6754
+ registerTool(server, listSpecFileCommentsTool, ctx);
6755
+ registerTool(server, createSpecFileCommentThreadTool, ctx);
6756
+ registerTool(server, replySpecFileCommentThreadTool, ctx);
6757
+ registerTool(server, updateSpecFileCommentTool, ctx);
6758
+ registerTool(server, deleteSpecFileCommentTool, ctx);
6216
6759
  return server;
6217
6760
  }
6218
6761
  async function startStdioServer(deps) {
@@ -6366,7 +6909,7 @@ async function canonicaliseRoot(root) {
6366
6909
  import { promises as fs4 } from "fs";
6367
6910
  import { join as join2, relative } from "path";
6368
6911
  import { spawn } from "child_process";
6369
- import { z as z38 } from "zod";
6912
+ import { z as z39 } from "zod";
6370
6913
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6371
6914
 
6372
6915
  // src/reverse/ignore.ts
@@ -6671,37 +7214,37 @@ var READ_FILE_MAX_LINES = 2e3;
6671
7214
  var LIST_DIR_HARD_CAP = 5e3;
6672
7215
  var LIST_DIR_DEFAULT_CAP = 1e3;
6673
7216
  var GREP_MAX_MATCHES_DEFAULT = 200;
6674
- var listDirInput = z38.object({
6675
- path: z38.string(),
6676
- recursive: z38.boolean().optional(),
6677
- maxEntries: z38.number().int().positive().optional(),
6678
- includeIgnored: z38.boolean().optional()
7217
+ var listDirInput = z39.object({
7218
+ path: z39.string(),
7219
+ recursive: z39.boolean().optional(),
7220
+ maxEntries: z39.number().int().positive().optional(),
7221
+ includeIgnored: z39.boolean().optional()
6679
7222
  });
6680
- var readFileInput = z38.object({
6681
- path: z38.string(),
6682
- offset: z38.number().int().nonnegative().optional(),
6683
- limit: z38.number().int().positive().optional(),
6684
- includeIgnored: z38.boolean().optional()
7223
+ var readFileInput = z39.object({
7224
+ path: z39.string(),
7225
+ offset: z39.number().int().nonnegative().optional(),
7226
+ limit: z39.number().int().positive().optional(),
7227
+ includeIgnored: z39.boolean().optional()
6685
7228
  });
6686
- var grepInput = z38.object({
6687
- pattern: z38.string().describe(
7229
+ var grepInput = z39.object({
7230
+ pattern: z39.string().describe(
6688
7231
  "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."
6689
7232
  ),
6690
- path: z38.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
6691
- isRegex: z38.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
6692
- caseSensitive: z38.boolean().optional(),
6693
- maxMatches: z38.number().int().positive().optional(),
6694
- contextLines: z38.number().int().nonnegative().optional(),
6695
- includeIgnored: z38.boolean().optional()
7233
+ path: z39.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
7234
+ isRegex: z39.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
7235
+ caseSensitive: z39.boolean().optional(),
7236
+ maxMatches: z39.number().int().positive().optional(),
7237
+ contextLines: z39.number().int().nonnegative().optional(),
7238
+ includeIgnored: z39.boolean().optional()
6696
7239
  });
6697
- var packCodebaseInput = z38.object({
6698
- subpath: z38.string().optional(),
6699
- includePatterns: z38.string().optional(),
6700
- ignorePatterns: z38.string().optional()
7240
+ var packCodebaseInput = z39.object({
7241
+ subpath: z39.string().optional(),
7242
+ includePatterns: z39.string().optional(),
7243
+ ignorePatterns: z39.string().optional()
6701
7244
  });
6702
- var packCodebaseReadPageInput = z38.object({
6703
- outputId: z38.string(),
6704
- page: z38.number().int().positive()
7245
+ var packCodebaseReadPageInput = z39.object({
7246
+ outputId: z39.string(),
7247
+ page: z39.number().int().positive()
6705
7248
  });
6706
7249
  function structuredError(error, message, extra) {
6707
7250
  const payload = { error, message, ...extra ?? {} };
@@ -7229,7 +7772,7 @@ function jsonSchemaFromZod(schema) {
7229
7772
  return zodToJson(schema);
7230
7773
  }
7231
7774
  function zodToJson(schema) {
7232
- if (schema instanceof z38.ZodObject) {
7775
+ if (schema instanceof z39.ZodObject) {
7233
7776
  const shape = schema.shape;
7234
7777
  const properties = {};
7235
7778
  const required = [];
@@ -7244,19 +7787,19 @@ function zodToJson(schema) {
7244
7787
  if (required.length > 0) out.required = required;
7245
7788
  return out;
7246
7789
  }
7247
- if (schema instanceof z38.ZodOptional) {
7790
+ if (schema instanceof z39.ZodOptional) {
7248
7791
  return zodToJson(schema.unwrap());
7249
7792
  }
7250
- if (schema instanceof z38.ZodString) {
7793
+ if (schema instanceof z39.ZodString) {
7251
7794
  return { type: "string" };
7252
7795
  }
7253
- if (schema instanceof z38.ZodNumber) {
7796
+ if (schema instanceof z39.ZodNumber) {
7254
7797
  return { type: "number" };
7255
7798
  }
7256
- if (schema instanceof z38.ZodBoolean) {
7799
+ if (schema instanceof z39.ZodBoolean) {
7257
7800
  return { type: "boolean" };
7258
7801
  }
7259
- if (schema instanceof z38.ZodEnum) {
7802
+ if (schema instanceof z39.ZodEnum) {
7260
7803
  return { type: "string", enum: schema.options };
7261
7804
  }
7262
7805
  return { type: "string" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myspec/mcp-server",
3
- "version": "0.5.0-next.116",
3
+ "version": "0.5.0-next.118",
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": {