@myspec/mcp-server 0.5.0-next.116 → 0.5.0-next.117
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.
- package/README.md +7 -0
- package/dist/index.js +615 -114
- 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
|
@@ -1425,6 +1425,9 @@ function conflictOf(err) {
|
|
|
1425
1425
|
own || fromMessage ? { ...fromMessage ?? {}, ...own ?? {} } : void 0
|
|
1426
1426
|
);
|
|
1427
1427
|
}
|
|
1428
|
+
function isPlatformConflictError(err) {
|
|
1429
|
+
return conflictOf(err) !== void 0;
|
|
1430
|
+
}
|
|
1428
1431
|
function isPlatformPreconditionRequiredError(err) {
|
|
1429
1432
|
return isPlatformHttpError(err) && err.status === 428;
|
|
1430
1433
|
}
|
|
@@ -1855,6 +1858,113 @@ function transformSpecSessionResponse(raw) {
|
|
|
1855
1858
|
// ../../packages/platform-client/project/types/lock.types.ts
|
|
1856
1859
|
var SPEC_FILE_LOCK_TTL_MS = 2 * 6e4;
|
|
1857
1860
|
|
|
1861
|
+
// ../../packages/platform-client/project/types/comment.types.ts
|
|
1862
|
+
var COMMENT_THREAD_FILTER_PARAM = "status";
|
|
1863
|
+
var MAX_COMMENT_BODY_LENGTH = 1e4;
|
|
1864
|
+
var MAX_ANCHOR_QUOTE_LENGTH = 2e3;
|
|
1865
|
+
var MAX_ANCHOR_CONTEXT_LENGTH = 64;
|
|
1866
|
+
function parseTimestamp(raw) {
|
|
1867
|
+
if (!raw) {
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
const parsed = new Date(raw);
|
|
1871
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1872
|
+
}
|
|
1873
|
+
function toCommentActorKind(raw) {
|
|
1874
|
+
return raw === "ai" ? "ai" : "human";
|
|
1875
|
+
}
|
|
1876
|
+
function toAnchorStatus(raw) {
|
|
1877
|
+
return raw === "anchored" ? "anchored" : "orphaned";
|
|
1878
|
+
}
|
|
1879
|
+
function toCreateCommentThreadRequest(input) {
|
|
1880
|
+
return {
|
|
1881
|
+
body: input.body,
|
|
1882
|
+
anchor_quote: input.anchorQuote,
|
|
1883
|
+
anchor_prefix: input.anchorPrefix,
|
|
1884
|
+
anchor_suffix: input.anchorSuffix
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
function toCreateCommentReplyRequest(body) {
|
|
1888
|
+
return { body };
|
|
1889
|
+
}
|
|
1890
|
+
function toUpdateCommentRequest(body) {
|
|
1891
|
+
return { body };
|
|
1892
|
+
}
|
|
1893
|
+
function toReanchorCommentThreadRequest(anchor) {
|
|
1894
|
+
return {
|
|
1895
|
+
anchor_quote: anchor.anchorQuote,
|
|
1896
|
+
anchor_prefix: anchor.anchorPrefix,
|
|
1897
|
+
anchor_suffix: anchor.anchorSuffix
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function transformSpecFileCommentResponse(raw) {
|
|
1901
|
+
return {
|
|
1902
|
+
id: raw.id,
|
|
1903
|
+
threadId: raw.thread_id,
|
|
1904
|
+
org: raw.org,
|
|
1905
|
+
authorId: raw.author_id,
|
|
1906
|
+
authorKind: toCommentActorKind(raw.author_kind),
|
|
1907
|
+
body: raw.body,
|
|
1908
|
+
deleted: raw.deleted,
|
|
1909
|
+
contentVersion: raw.content_version,
|
|
1910
|
+
createdAt: parseTimestamp(raw.created_at),
|
|
1911
|
+
updatedAt: parseTimestamp(raw.updated_at),
|
|
1912
|
+
deletedAt: parseTimestamp(raw.deleted_at)
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function transformSpecFileCommentThreadResponse(raw) {
|
|
1916
|
+
return {
|
|
1917
|
+
id: raw.id,
|
|
1918
|
+
specFileId: raw.spec_file_id,
|
|
1919
|
+
projectId: raw.project_id,
|
|
1920
|
+
org: raw.org,
|
|
1921
|
+
rootCommentId: raw.root_comment_id ?? null,
|
|
1922
|
+
anchorContentVersion: raw.anchor_content_version,
|
|
1923
|
+
anchorRevision: raw.anchor_revision,
|
|
1924
|
+
anchorStart: raw.anchor_start,
|
|
1925
|
+
anchorEnd: raw.anchor_end,
|
|
1926
|
+
anchorQuote: raw.anchor_quote,
|
|
1927
|
+
anchorPrefix: raw.anchor_prefix,
|
|
1928
|
+
anchorSuffix: raw.anchor_suffix,
|
|
1929
|
+
anchorStatus: toAnchorStatus(raw.anchor_status),
|
|
1930
|
+
createdBy: raw.created_by,
|
|
1931
|
+
createdByKind: toCommentActorKind(raw.created_by_kind),
|
|
1932
|
+
resolvedAt: parseTimestamp(raw.resolved_at),
|
|
1933
|
+
resolvedBy: raw.resolved_by ?? null,
|
|
1934
|
+
createdAt: parseTimestamp(raw.created_at),
|
|
1935
|
+
updatedAt: parseTimestamp(raw.updated_at),
|
|
1936
|
+
// Always an array: consumers map over it on every render, so the empty case
|
|
1937
|
+
// must not be a value each one has to guard.
|
|
1938
|
+
comments: (raw.comments ?? []).map(transformSpecFileCommentResponse)
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function transformSpecFileCommentCountResponse(raw) {
|
|
1942
|
+
return {
|
|
1943
|
+
specFileId: raw.spec_file_id,
|
|
1944
|
+
openThreadCount: raw.open_thread_count
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function transformSpecFileCommentThreadList(raw) {
|
|
1948
|
+
return (raw.threads ?? []).map(transformSpecFileCommentThreadResponse);
|
|
1949
|
+
}
|
|
1950
|
+
function transformProjectCommentCounts(raw) {
|
|
1951
|
+
return (raw.counts ?? []).map(transformSpecFileCommentCountResponse);
|
|
1952
|
+
}
|
|
1953
|
+
function transformProjectCommentActivity(raw) {
|
|
1954
|
+
return {
|
|
1955
|
+
threads: (raw.threads ?? []).map(transformSpecFileCommentThreadResponse),
|
|
1956
|
+
total: raw.total,
|
|
1957
|
+
limit: raw.limit,
|
|
1958
|
+
offset: raw.offset
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function transformDeleteCommentResult(raw) {
|
|
1962
|
+
return {
|
|
1963
|
+
comment: raw.comment ? transformSpecFileCommentResponse(raw.comment) : null,
|
|
1964
|
+
threadDeleted: raw.thread_deleted
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1858
1968
|
// ../../packages/platform-client/project/http/attachment.http-client.ts
|
|
1859
1969
|
var UPLOAD_TIMEOUT_MS = 3e4;
|
|
1860
1970
|
var HttpAttachmentClient = class {
|
|
@@ -2440,6 +2550,137 @@ var HttpSpecSessionClient = class {
|
|
|
2440
2550
|
}
|
|
2441
2551
|
};
|
|
2442
2552
|
|
|
2553
|
+
// ../../packages/platform-client/project/http/comment.http-client.ts
|
|
2554
|
+
var HEADER_EXPECTED_VERSION2 = "X-Expected-Version";
|
|
2555
|
+
var HEADER_ACTOR_KIND2 = "X-Actor-Kind";
|
|
2556
|
+
function segment(value) {
|
|
2557
|
+
return encodeURIComponent(value);
|
|
2558
|
+
}
|
|
2559
|
+
var HttpSpecFileCommentClient = class {
|
|
2560
|
+
httpClient;
|
|
2561
|
+
constructor(httpClient) {
|
|
2562
|
+
this.httpClient = httpClient;
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Turns the shared write options into request headers.
|
|
2566
|
+
*
|
|
2567
|
+
* Both halves are forwarded EXACTLY as given and neither is defaulted. That
|
|
2568
|
+
* matters most for what this does NOT do: it never substitutes
|
|
2569
|
+
* `actorKind: 'human'` for an absent value. Resolve, unresolve and re-anchor
|
|
2570
|
+
* are human-only and the platform parses the header fail-closed (AR-PRJ-26),
|
|
2571
|
+
* so a client-side default would turn a 403 the caller should see into a
|
|
2572
|
+
* successful write attributed to a person who never acted.
|
|
2573
|
+
*/
|
|
2574
|
+
writeHeaders(opts) {
|
|
2575
|
+
const headers = {};
|
|
2576
|
+
if (opts?.expectedVersion !== void 0) {
|
|
2577
|
+
headers[HEADER_EXPECTED_VERSION2] = String(opts.expectedVersion);
|
|
2578
|
+
}
|
|
2579
|
+
if (opts?.actorKind) {
|
|
2580
|
+
headers[HEADER_ACTOR_KIND2] = opts.actorKind;
|
|
2581
|
+
}
|
|
2582
|
+
return headers;
|
|
2583
|
+
}
|
|
2584
|
+
async listThreads(fileId, jwtToken, opts) {
|
|
2585
|
+
const query = new URLSearchParams();
|
|
2586
|
+
if (opts?.filter) {
|
|
2587
|
+
query.set(COMMENT_THREAD_FILTER_PARAM, opts.filter);
|
|
2588
|
+
}
|
|
2589
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2590
|
+
const raw = await this.httpClient.get(
|
|
2591
|
+
`/project/v1/files/${segment(fileId)}/comments${suffix}`,
|
|
2592
|
+
jwtToken,
|
|
2593
|
+
{}
|
|
2594
|
+
);
|
|
2595
|
+
return transformSpecFileCommentThreadList(raw);
|
|
2596
|
+
}
|
|
2597
|
+
async createThread(fileId, input, jwtToken, opts) {
|
|
2598
|
+
const raw = await this.httpClient.post(
|
|
2599
|
+
`/project/v1/files/${segment(fileId)}/comments`,
|
|
2600
|
+
toCreateCommentThreadRequest(input),
|
|
2601
|
+
jwtToken,
|
|
2602
|
+
{ headers: this.writeHeaders(opts) }
|
|
2603
|
+
);
|
|
2604
|
+
return transformSpecFileCommentThreadResponse(raw);
|
|
2605
|
+
}
|
|
2606
|
+
async replyToThread(fileId, threadId, body, jwtToken, opts) {
|
|
2607
|
+
const raw = await this.httpClient.post(
|
|
2608
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/replies`,
|
|
2609
|
+
toCreateCommentReplyRequest(body),
|
|
2610
|
+
jwtToken,
|
|
2611
|
+
{ headers: this.writeHeaders(opts) }
|
|
2612
|
+
);
|
|
2613
|
+
return transformSpecFileCommentResponse(raw);
|
|
2614
|
+
}
|
|
2615
|
+
async editComment(fileId, commentId, body, jwtToken, opts) {
|
|
2616
|
+
const raw = await this.httpClient.patch(
|
|
2617
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(commentId)}`,
|
|
2618
|
+
toUpdateCommentRequest(body),
|
|
2619
|
+
jwtToken,
|
|
2620
|
+
{ headers: this.writeHeaders(opts) }
|
|
2621
|
+
);
|
|
2622
|
+
return transformSpecFileCommentResponse(raw);
|
|
2623
|
+
}
|
|
2624
|
+
async deleteComment(fileId, commentId, jwtToken, opts) {
|
|
2625
|
+
const raw = await this.httpClient.delete(
|
|
2626
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(commentId)}`,
|
|
2627
|
+
jwtToken,
|
|
2628
|
+
{ headers: this.writeHeaders(opts) }
|
|
2629
|
+
);
|
|
2630
|
+
return transformDeleteCommentResult(raw);
|
|
2631
|
+
}
|
|
2632
|
+
async resolveThread(fileId, threadId, jwtToken, opts) {
|
|
2633
|
+
const raw = await this.httpClient.put(
|
|
2634
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/resolve`,
|
|
2635
|
+
{},
|
|
2636
|
+
jwtToken,
|
|
2637
|
+
{ headers: this.writeHeaders(opts) }
|
|
2638
|
+
);
|
|
2639
|
+
return transformSpecFileCommentThreadResponse(raw);
|
|
2640
|
+
}
|
|
2641
|
+
async unresolveThread(fileId, threadId, jwtToken, opts) {
|
|
2642
|
+
const raw = await this.httpClient.delete(
|
|
2643
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/resolve`,
|
|
2644
|
+
jwtToken,
|
|
2645
|
+
{ headers: this.writeHeaders(opts) }
|
|
2646
|
+
);
|
|
2647
|
+
return transformSpecFileCommentThreadResponse(raw);
|
|
2648
|
+
}
|
|
2649
|
+
async reanchorThread(fileId, threadId, anchor, jwtToken, opts) {
|
|
2650
|
+
const raw = await this.httpClient.post(
|
|
2651
|
+
`/project/v1/files/${segment(fileId)}/comments/${segment(threadId)}/reanchor`,
|
|
2652
|
+
toReanchorCommentThreadRequest(anchor),
|
|
2653
|
+
jwtToken,
|
|
2654
|
+
{ headers: this.writeHeaders(opts) }
|
|
2655
|
+
);
|
|
2656
|
+
return transformSpecFileCommentThreadResponse(raw);
|
|
2657
|
+
}
|
|
2658
|
+
async getProjectCommentCounts(projectId, jwtToken) {
|
|
2659
|
+
const raw = await this.httpClient.get(
|
|
2660
|
+
`/project/v1/projects/${segment(projectId)}/comment-counts`,
|
|
2661
|
+
jwtToken,
|
|
2662
|
+
{}
|
|
2663
|
+
);
|
|
2664
|
+
return transformProjectCommentCounts(raw);
|
|
2665
|
+
}
|
|
2666
|
+
async listProjectCommentActivity(projectId, jwtToken, opts) {
|
|
2667
|
+
const query = new URLSearchParams();
|
|
2668
|
+
if (opts?.limit !== void 0) {
|
|
2669
|
+
query.set("limit", String(opts.limit));
|
|
2670
|
+
}
|
|
2671
|
+
if (opts?.offset !== void 0) {
|
|
2672
|
+
query.set("offset", String(opts.offset));
|
|
2673
|
+
}
|
|
2674
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2675
|
+
const raw = await this.httpClient.get(
|
|
2676
|
+
`/project/v1/projects/${segment(projectId)}/comments${suffix}`,
|
|
2677
|
+
jwtToken,
|
|
2678
|
+
{}
|
|
2679
|
+
);
|
|
2680
|
+
return transformProjectCommentActivity(raw);
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2443
2684
|
// ../../packages/platform-client/artifact/types/artifact.types.ts
|
|
2444
2685
|
function transformArtifactResponse(raw) {
|
|
2445
2686
|
return {
|
|
@@ -2570,15 +2811,15 @@ function buildUpdateArtifactBody(request) {
|
|
|
2570
2811
|
|
|
2571
2812
|
// ../../packages/platform-client/artifact/http/artifact.http-client.ts
|
|
2572
2813
|
var BASE_PATH = "/artifact/v1";
|
|
2573
|
-
var
|
|
2574
|
-
var
|
|
2814
|
+
var HEADER_EXPECTED_VERSION3 = "X-Expected-Version";
|
|
2815
|
+
var HEADER_ACTOR_KIND3 = "X-Actor-Kind";
|
|
2575
2816
|
function writeHeaders(opts) {
|
|
2576
2817
|
const headers = {};
|
|
2577
2818
|
if (opts?.expectedVersion !== void 0) {
|
|
2578
|
-
headers[
|
|
2819
|
+
headers[HEADER_EXPECTED_VERSION3] = String(opts.expectedVersion);
|
|
2579
2820
|
}
|
|
2580
2821
|
if (opts?.actorKind) {
|
|
2581
|
-
headers[
|
|
2822
|
+
headers[HEADER_ACTOR_KIND3] = opts.actorKind;
|
|
2582
2823
|
}
|
|
2583
2824
|
return headers;
|
|
2584
2825
|
}
|
|
@@ -3155,7 +3396,8 @@ var PlatformClient = class {
|
|
|
3155
3396
|
file: new HttpFileClient(httpClient),
|
|
3156
3397
|
attachment: new HttpAttachmentClient(httpClient),
|
|
3157
3398
|
artifact: new HttpArtifactClient(httpClient),
|
|
3158
|
-
streamToken: new HttpStreamTokenClient(httpClient)
|
|
3399
|
+
streamToken: new HttpStreamTokenClient(httpClient),
|
|
3400
|
+
comment: new HttpSpecFileCommentClient(httpClient)
|
|
3159
3401
|
};
|
|
3160
3402
|
}
|
|
3161
3403
|
// ------------------------------------------------------- stream tokens
|
|
@@ -3205,6 +3447,60 @@ var PlatformClient = class {
|
|
|
3205
3447
|
(jwt, c) => c.artifact.rollback(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
|
|
3206
3448
|
);
|
|
3207
3449
|
}
|
|
3450
|
+
// ------------------------------------------------------------- comments
|
|
3451
|
+
//
|
|
3452
|
+
// Review discussion on a spec file. Every WRITE below carries
|
|
3453
|
+
// `actorKind: 'ai'` (AR-MCP-3): an MCP client is an assistant acting on the
|
|
3454
|
+
// user's behalf, and it writes under the USER'S OWN JWT, so the header is the
|
|
3455
|
+
// only thing that can tell a person's comment from a model's. Omitting it
|
|
3456
|
+
// would not fail -- the platform's permissive parse on these paths defaults
|
|
3457
|
+
// to `human` -- it would just file the comment under the user's name.
|
|
3458
|
+
//
|
|
3459
|
+
// There is deliberately no resolve, unresolve or re-anchor method here, and
|
|
3460
|
+
// no tool for one (AR-MCP-2). The platform refuses `ai` on those three routes
|
|
3461
|
+
// with a 403, so a method would only ever produce an error the model then has
|
|
3462
|
+
// to interpret.
|
|
3463
|
+
/** Lists a spec file's comment threads, optionally filtered by status. */
|
|
3464
|
+
async listSpecFileComments(fileId, opts = {}) {
|
|
3465
|
+
return this.withTokenRetry(
|
|
3466
|
+
(jwt, c) => c.comment.listThreads(fileId, jwt, opts.filter ? { filter: opts.filter } : {})
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3469
|
+
/** Opens a thread on a passage. The server locates the quote and derives the offsets. */
|
|
3470
|
+
async createSpecFileCommentThread(fileId, input) {
|
|
3471
|
+
return this.withTokenRetry(
|
|
3472
|
+
(jwt, c) => c.comment.createThread(fileId, input, jwt, { actorKind: "ai" })
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
/** Replies to an existing thread. */
|
|
3476
|
+
async replyToSpecFileCommentThread(fileId, threadId, body) {
|
|
3477
|
+
return this.withTokenRetry(
|
|
3478
|
+
(jwt, c) => c.comment.replyToThread(fileId, threadId, body, jwt, { actorKind: "ai" })
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Edits one of the caller's own comments.
|
|
3483
|
+
*
|
|
3484
|
+
* `expectedVersion` is the COMMENT's own contentVersion, not the file's:
|
|
3485
|
+
* pairing it with the file would make an edit fail because somebody saved the
|
|
3486
|
+
* document, which has nothing to do with whether this comment moved. Only the
|
|
3487
|
+
* caller's own value is sent -- substituting one read a moment ago would
|
|
3488
|
+
* guard nothing, exactly as in saveSpecFileRevision.
|
|
3489
|
+
*/
|
|
3490
|
+
async updateSpecFileComment(fileId, commentId, body, opts = {}) {
|
|
3491
|
+
return this.withTokenRetry(
|
|
3492
|
+
(jwt, c) => c.comment.editComment(fileId, commentId, body, jwt, {
|
|
3493
|
+
...opts.expectedVersion === void 0 ? {} : { expectedVersion: opts.expectedVersion },
|
|
3494
|
+
actorKind: "ai"
|
|
3495
|
+
})
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
/** Soft-deletes one of the caller's own comments. */
|
|
3499
|
+
async deleteSpecFileComment(fileId, commentId) {
|
|
3500
|
+
return this.withTokenRetry(
|
|
3501
|
+
(jwt, c) => c.comment.deleteComment(fileId, commentId, jwt, { actorKind: "ai" })
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3208
3504
|
async withTokenRetry(call) {
|
|
3209
3505
|
const clients = await this.clients();
|
|
3210
3506
|
const jwt = await this.tokenManager.getValidAccessToken();
|
|
@@ -4790,11 +5086,207 @@ function conflictMessage(filePath, conflict) {
|
|
|
4790
5086
|
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
5087
|
}
|
|
4792
5088
|
|
|
5089
|
+
// src/server/tools/spec_file_comments.ts
|
|
5090
|
+
import { z as z24 } from "zod";
|
|
5091
|
+
var LIST_BODY_PREVIEW_CHARS = 1e3;
|
|
5092
|
+
var LIST_MAX_THREADS = 100;
|
|
5093
|
+
function commentSummary(comment, opts = {}) {
|
|
5094
|
+
const body = comment.body;
|
|
5095
|
+
const truncated = opts.preview === true && body !== void 0 && body.length > LIST_BODY_PREVIEW_CHARS;
|
|
5096
|
+
return {
|
|
5097
|
+
comment_id: comment.id,
|
|
5098
|
+
thread_id: comment.threadId,
|
|
5099
|
+
author_id: comment.authorId,
|
|
5100
|
+
author_kind: comment.authorKind,
|
|
5101
|
+
// Absent for a tombstone: a soft-deleted root kept so the thread still has
|
|
5102
|
+
// a head to render. `deleted` is the flag to branch on, not a missing body.
|
|
5103
|
+
body: truncated ? body.slice(0, LIST_BODY_PREVIEW_CHARS) : body,
|
|
5104
|
+
// Only ever present and true, so a caller can test it without knowing which
|
|
5105
|
+
// shape produced the comment.
|
|
5106
|
+
...truncated ? { body_truncated: true } : {},
|
|
5107
|
+
deleted: comment.deleted,
|
|
5108
|
+
content_version: comment.contentVersion,
|
|
5109
|
+
created_at: comment.createdAt?.toISOString(),
|
|
5110
|
+
updated_at: comment.updatedAt?.toISOString()
|
|
5111
|
+
};
|
|
5112
|
+
}
|
|
5113
|
+
function threadSummary(thread, opts = {}) {
|
|
5114
|
+
return {
|
|
5115
|
+
thread_id: thread.id,
|
|
5116
|
+
spec_file_id: thread.specFileId,
|
|
5117
|
+
root_comment_id: thread.rootCommentId,
|
|
5118
|
+
anchor_quote: thread.anchorQuote,
|
|
5119
|
+
// 'anchored' or 'orphaned'. An orphan's passage can no longer be located,
|
|
5120
|
+
// and only a person can repair it — there is no tool here that can.
|
|
5121
|
+
anchor_status: thread.anchorStatus,
|
|
5122
|
+
anchor_start: thread.anchorStart,
|
|
5123
|
+
anchor_end: thread.anchorEnd,
|
|
5124
|
+
// Display only: revision_count is reusable and decrements on revert.
|
|
5125
|
+
anchor_revision: thread.anchorRevision,
|
|
5126
|
+
resolved: thread.resolvedAt !== null,
|
|
5127
|
+
resolved_by: thread.resolvedBy,
|
|
5128
|
+
resolved_at: thread.resolvedAt?.toISOString(),
|
|
5129
|
+
created_by: thread.createdBy,
|
|
5130
|
+
created_by_kind: thread.createdByKind,
|
|
5131
|
+
created_at: thread.createdAt?.toISOString(),
|
|
5132
|
+
comments: thread.comments.map((c) => commentSummary(c, opts))
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
function uuidField(what) {
|
|
5136
|
+
return z24.uuid().describe(what);
|
|
5137
|
+
}
|
|
5138
|
+
var listInputSchema = {
|
|
5139
|
+
file_id: uuidField("UUID of the spec file whose comments to list."),
|
|
5140
|
+
status: z24.enum(["open", "resolved", "orphaned"]).optional().describe(
|
|
5141
|
+
"Narrow to open (unresolved) threads, resolved ones, or orphaned ones whose passage can no longer be located. Omit for all of them."
|
|
5142
|
+
)
|
|
5143
|
+
};
|
|
5144
|
+
var listSpecFileCommentsTool = {
|
|
5145
|
+
name: "list_spec_file_comments",
|
|
5146
|
+
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.",
|
|
5147
|
+
inputSchema: listInputSchema,
|
|
5148
|
+
handler: async (args, ctx) => {
|
|
5149
|
+
const threads = await ctx.client.listSpecFileComments(args.file_id, {
|
|
5150
|
+
...args.status ? { filter: args.status } : {}
|
|
5151
|
+
});
|
|
5152
|
+
const page = threads.slice(0, LIST_MAX_THREADS);
|
|
5153
|
+
return jsonResult({
|
|
5154
|
+
threads: page.map((t) => threadSummary(t, { preview: true })),
|
|
5155
|
+
total_threads: threads.length,
|
|
5156
|
+
// Only when something was actually withheld, so its absence means "you
|
|
5157
|
+
// have all of it" rather than "nobody thought about it".
|
|
5158
|
+
...threads.length > page.length ? { truncated: true } : {}
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
};
|
|
5162
|
+
var createInputSchema = {
|
|
5163
|
+
file_id: uuidField("UUID of the spec file to comment on."),
|
|
5164
|
+
body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The comment text, in markdown."),
|
|
5165
|
+
// The three anchor limits are exported by the contract for exactly this, and
|
|
5166
|
+
// the context one is SHORT (64). A description reading "the text immediately
|
|
5167
|
+
// BEFORE the quote" invites a whole preceding sentence, which is the default
|
|
5168
|
+
// reading rather than a corner case — so the ceiling is enforced in the
|
|
5169
|
+
// schema, where the model sees it, rather than discovered as a 400.
|
|
5170
|
+
anchor_quote: z24.string().min(1).max(MAX_ANCHOR_QUOTE_LENGTH).describe(
|
|
5171
|
+
`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.`
|
|
5172
|
+
),
|
|
5173
|
+
anchor_prefix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
|
|
5174
|
+
`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.`
|
|
5175
|
+
),
|
|
5176
|
+
anchor_suffix: z24.string().max(MAX_ANCHOR_CONTEXT_LENGTH).optional().describe(
|
|
5177
|
+
`The text immediately AFTER the quote. At most ${String(MAX_ANCHOR_CONTEXT_LENGTH)} characters. Empty when the quote ends the file.`
|
|
5178
|
+
)
|
|
5179
|
+
};
|
|
5180
|
+
var createSpecFileCommentThreadTool = {
|
|
5181
|
+
name: "create_spec_file_comment_thread",
|
|
5182
|
+
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.",
|
|
5183
|
+
inputSchema: createInputSchema,
|
|
5184
|
+
handler: async (args, ctx) => {
|
|
5185
|
+
try {
|
|
5186
|
+
const thread = await ctx.client.createSpecFileCommentThread(args.file_id, {
|
|
5187
|
+
body: args.body,
|
|
5188
|
+
anchorQuote: args.anchor_quote,
|
|
5189
|
+
// Always sent, even empty: an omitted key is indistinguishable from a
|
|
5190
|
+
// client that does not know about the field.
|
|
5191
|
+
anchorPrefix: args.anchor_prefix ?? "",
|
|
5192
|
+
anchorSuffix: args.anchor_suffix ?? ""
|
|
5193
|
+
});
|
|
5194
|
+
return jsonResult({ thread: threadSummary(thread) });
|
|
5195
|
+
} catch (err) {
|
|
5196
|
+
if (isPlatformConflictError(err)) {
|
|
5197
|
+
return errorResult(
|
|
5198
|
+
`No thread was created on ${args.file_id}: the file is in the Trash Bin, so it gathers no new discussion. Restore it with restore_spec_file_from_trash first.`
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
throw err;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
};
|
|
5205
|
+
var replyInputSchema = {
|
|
5206
|
+
file_id: uuidField("UUID of the spec file the thread belongs to."),
|
|
5207
|
+
thread_id: uuidField("UUID of the thread to reply to, from list_spec_file_comments."),
|
|
5208
|
+
body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The reply text, in markdown.")
|
|
5209
|
+
};
|
|
5210
|
+
var replySpecFileCommentThreadTool = {
|
|
5211
|
+
name: "reply_spec_file_comment_thread",
|
|
5212
|
+
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.",
|
|
5213
|
+
inputSchema: replyInputSchema,
|
|
5214
|
+
handler: async (args, ctx) => {
|
|
5215
|
+
try {
|
|
5216
|
+
const comment = await ctx.client.replyToSpecFileCommentThread(
|
|
5217
|
+
args.file_id,
|
|
5218
|
+
args.thread_id,
|
|
5219
|
+
args.body
|
|
5220
|
+
);
|
|
5221
|
+
return jsonResult({ comment: commentSummary(comment) });
|
|
5222
|
+
} catch (err) {
|
|
5223
|
+
if (isPlatformConflictError(err)) {
|
|
5224
|
+
return errorResult(
|
|
5225
|
+
`No reply was posted to ${args.thread_id}: the file is in the Trash Bin, so it gathers no new discussion. Restore it with restore_spec_file_from_trash first.`
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
throw err;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
};
|
|
5232
|
+
var updateInputSchema = {
|
|
5233
|
+
file_id: uuidField("UUID of the spec file the comment belongs to."),
|
|
5234
|
+
comment_id: uuidField("UUID of the comment to edit. Must be your own."),
|
|
5235
|
+
body: z24.string().min(1).max(MAX_COMMENT_BODY_LENGTH).describe("The replacement text, in markdown. Replaces the comment entirely."),
|
|
5236
|
+
expected_version: z24.number().int().nonnegative().optional().describe(
|
|
5237
|
+
"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."
|
|
5238
|
+
)
|
|
5239
|
+
};
|
|
5240
|
+
var updateSpecFileCommentTool = {
|
|
5241
|
+
name: "update_spec_file_comment",
|
|
5242
|
+
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.",
|
|
5243
|
+
inputSchema: updateInputSchema,
|
|
5244
|
+
handler: async (args, ctx) => {
|
|
5245
|
+
try {
|
|
5246
|
+
const comment = await ctx.client.updateSpecFileComment(
|
|
5247
|
+
args.file_id,
|
|
5248
|
+
args.comment_id,
|
|
5249
|
+
args.body,
|
|
5250
|
+
{
|
|
5251
|
+
// Only the caller's own value is sent. Substituting a version read a
|
|
5252
|
+
// moment ago would guard nothing — it cannot detect an edit made before
|
|
5253
|
+
// that read, which is the edit that matters.
|
|
5254
|
+
...args.expected_version === void 0 ? {} : { expectedVersion: args.expected_version }
|
|
5255
|
+
}
|
|
5256
|
+
);
|
|
5257
|
+
return jsonResult({ comment: commentSummary(comment) });
|
|
5258
|
+
} catch (err) {
|
|
5259
|
+
if (isPlatformConflictError(err)) {
|
|
5260
|
+
return errorResult(
|
|
5261
|
+
`${args.comment_id} was NOT edited: 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.`
|
|
5262
|
+
);
|
|
5263
|
+
}
|
|
5264
|
+
throw err;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
var deleteInputSchema = {
|
|
5269
|
+
file_id: uuidField("UUID of the spec file the comment belongs to."),
|
|
5270
|
+
comment_id: uuidField("UUID of the comment to delete. Must be your own.")
|
|
5271
|
+
};
|
|
5272
|
+
var deleteSpecFileCommentTool = {
|
|
5273
|
+
name: "delete_spec_file_comment",
|
|
5274
|
+
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).",
|
|
5275
|
+
inputSchema: deleteInputSchema,
|
|
5276
|
+
handler: async (args, ctx) => {
|
|
5277
|
+
const result = await ctx.client.deleteSpecFileComment(args.file_id, args.comment_id);
|
|
5278
|
+
return jsonResult({
|
|
5279
|
+
comment: result.comment ? commentSummary(result.comment) : null,
|
|
5280
|
+
thread_deleted: result.threadDeleted
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
};
|
|
5284
|
+
|
|
4793
5285
|
// src/server/tools/upload_attachment.ts
|
|
4794
5286
|
import { createHash as createHash3 } from "crypto";
|
|
4795
5287
|
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
4796
5288
|
import { basename, extname, isAbsolute as isAbsolute2 } from "path";
|
|
4797
|
-
import { z as
|
|
5289
|
+
import { z as z25 } from "zod";
|
|
4798
5290
|
var DEFAULT_MIME = "application/octet-stream";
|
|
4799
5291
|
var MAX_ATTACHMENT_MB2 = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
4800
5292
|
var STRUCTURED_EXTENSION_MIME = {
|
|
@@ -4900,15 +5392,15 @@ function inferMimeFromName(name) {
|
|
|
4900
5392
|
return DEFAULT_MIME;
|
|
4901
5393
|
}
|
|
4902
5394
|
var inputSchema24 = {
|
|
4903
|
-
project_id:
|
|
4904
|
-
file_path:
|
|
5395
|
+
project_id: z25.string().min(1).describe("UUID of the project to attach the file to."),
|
|
5396
|
+
file_path: z25.string().min(1).describe(
|
|
4905
5397
|
"Absolute path to the local file to upload. The MCP server reads this path from its own host."
|
|
4906
5398
|
),
|
|
4907
|
-
file_name:
|
|
4908
|
-
mime_type:
|
|
5399
|
+
file_name: z25.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
|
|
5400
|
+
mime_type: z25.string().min(1).optional().describe(
|
|
4909
5401
|
"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
5402
|
),
|
|
4911
|
-
override:
|
|
5403
|
+
override: z25.boolean().optional().describe(
|
|
4912
5404
|
"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
5405
|
)
|
|
4914
5406
|
};
|
|
@@ -5039,7 +5531,7 @@ var uploadAttachmentTool = {
|
|
|
5039
5531
|
};
|
|
5040
5532
|
|
|
5041
5533
|
// src/server/tools/upload_spec_file.ts
|
|
5042
|
-
import { z as
|
|
5534
|
+
import { z as z26 } from "zod";
|
|
5043
5535
|
|
|
5044
5536
|
// src/server/spec-path.ts
|
|
5045
5537
|
var MAX_SPEC_DIR_DEPTH = 3;
|
|
@@ -5096,17 +5588,17 @@ var SPEC_FILE_TYPES = [
|
|
|
5096
5588
|
"openspec-spec"
|
|
5097
5589
|
];
|
|
5098
5590
|
var inputSchema25 = {
|
|
5099
|
-
project_id:
|
|
5100
|
-
file_path:
|
|
5591
|
+
project_id: z26.string().min(1).describe("UUID of the project to upload the file into."),
|
|
5592
|
+
file_path: z26.string().min(1).describe(
|
|
5101
5593
|
'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
5594
|
),
|
|
5103
|
-
content:
|
|
5595
|
+
content: z26.string().min(1).optional().describe(
|
|
5104
5596
|
"Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
5105
5597
|
),
|
|
5106
|
-
local_file_path:
|
|
5598
|
+
local_file_path: z26.string().min(1).optional().describe(
|
|
5107
5599
|
"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
5600
|
),
|
|
5109
|
-
file_type:
|
|
5601
|
+
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
5602
|
};
|
|
5111
5603
|
var uploadSpecFileTool = {
|
|
5112
5604
|
name: "upload_spec_file",
|
|
@@ -5142,16 +5634,16 @@ var uploadSpecFileTool = {
|
|
|
5142
5634
|
};
|
|
5143
5635
|
|
|
5144
5636
|
// src/server/tools/list_artifacts.ts
|
|
5145
|
-
import { z as
|
|
5637
|
+
import { z as z28 } from "zod";
|
|
5146
5638
|
|
|
5147
5639
|
// src/server/tools/artifact-output.ts
|
|
5148
|
-
import { z as
|
|
5149
|
-
var artifactFilesSchema =
|
|
5150
|
-
|
|
5151
|
-
path:
|
|
5152
|
-
content:
|
|
5153
|
-
encoding:
|
|
5154
|
-
content_type:
|
|
5640
|
+
import { z as z27 } from "zod";
|
|
5641
|
+
var artifactFilesSchema = z27.array(
|
|
5642
|
+
z27.object({
|
|
5643
|
+
path: z27.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
|
|
5644
|
+
content: z27.string().describe("The file content, in `encoding` (default utf8)."),
|
|
5645
|
+
encoding: z27.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
|
|
5646
|
+
content_type: z27.string().optional().describe("Hint used only for extensions the platform does not recognise.")
|
|
5155
5647
|
})
|
|
5156
5648
|
).describe("Files of the bundle.");
|
|
5157
5649
|
function artifactSummary(a) {
|
|
@@ -5224,9 +5716,9 @@ function artifactCreateConflictMessage(conflict) {
|
|
|
5224
5716
|
var LIST_DEFAULT4 = 50;
|
|
5225
5717
|
var LIST_MAX4 = 100;
|
|
5226
5718
|
var inputSchema26 = {
|
|
5227
|
-
project_id:
|
|
5228
|
-
limit:
|
|
5229
|
-
offset:
|
|
5719
|
+
project_id: z28.string().min(1).describe("UUID of the project whose artifacts to list."),
|
|
5720
|
+
limit: z28.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
|
|
5721
|
+
offset: z28.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
|
|
5230
5722
|
};
|
|
5231
5723
|
var listArtifactsTool = {
|
|
5232
5724
|
name: "list_artifacts",
|
|
@@ -5249,10 +5741,10 @@ var listArtifactsTool = {
|
|
|
5249
5741
|
};
|
|
5250
5742
|
|
|
5251
5743
|
// src/server/tools/get_artifact.ts
|
|
5252
|
-
import { z as
|
|
5744
|
+
import { z as z29 } from "zod";
|
|
5253
5745
|
var inputSchema27 = {
|
|
5254
|
-
artifact_id:
|
|
5255
|
-
revision:
|
|
5746
|
+
artifact_id: z29.string().min(1).describe("UUID of the artifact."),
|
|
5747
|
+
revision: z29.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
|
|
5256
5748
|
};
|
|
5257
5749
|
var getArtifactTool = {
|
|
5258
5750
|
name: "get_artifact",
|
|
@@ -5274,14 +5766,14 @@ var getArtifactTool = {
|
|
|
5274
5766
|
};
|
|
5275
5767
|
|
|
5276
5768
|
// src/server/tools/read_artifact_file.ts
|
|
5277
|
-
import { z as
|
|
5769
|
+
import { z as z30 } from "zod";
|
|
5278
5770
|
var MAX_READABLE_ARTIFACT_FILE_BYTES = 8 * 1024 * 1024;
|
|
5279
5771
|
var inputSchema28 = {
|
|
5280
|
-
artifact_id:
|
|
5281
|
-
path:
|
|
5282
|
-
revision:
|
|
5283
|
-
offset:
|
|
5284
|
-
limit:
|
|
5772
|
+
artifact_id: z30.string().min(1).describe("UUID of the artifact."),
|
|
5773
|
+
path: z30.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
|
|
5774
|
+
revision: z30.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
|
|
5775
|
+
offset: z30.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
|
|
5776
|
+
limit: z30.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
|
|
5285
5777
|
};
|
|
5286
5778
|
var readArtifactFileTool = {
|
|
5287
5779
|
name: "read_artifact_file",
|
|
@@ -5321,14 +5813,14 @@ var readArtifactFileTool = {
|
|
|
5321
5813
|
};
|
|
5322
5814
|
|
|
5323
5815
|
// src/server/tools/create_artifact.ts
|
|
5324
|
-
import { z as
|
|
5816
|
+
import { z as z31 } from "zod";
|
|
5325
5817
|
var inputSchema29 = {
|
|
5326
|
-
project_id:
|
|
5327
|
-
name:
|
|
5328
|
-
slug:
|
|
5329
|
-
description:
|
|
5330
|
-
entry_path:
|
|
5331
|
-
message:
|
|
5818
|
+
project_id: z31.string().min(1).describe("UUID of the project to create the artifact in."),
|
|
5819
|
+
name: z31.string().min(1).describe("Display name (max 200 characters)."),
|
|
5820
|
+
slug: z31.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
|
|
5821
|
+
description: z31.string().optional().describe("Optional description (max 2000 characters)."),
|
|
5822
|
+
entry_path: z31.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
|
|
5823
|
+
message: z31.string().optional().describe("Short summary of this first revision (max 500 characters)."),
|
|
5332
5824
|
files: artifactFilesSchema
|
|
5333
5825
|
};
|
|
5334
5826
|
var createArtifactTool = {
|
|
@@ -5362,19 +5854,19 @@ var createArtifactTool = {
|
|
|
5362
5854
|
};
|
|
5363
5855
|
|
|
5364
5856
|
// src/server/tools/write_artifact_revision.ts
|
|
5365
|
-
import { z as
|
|
5857
|
+
import { z as z32 } from "zod";
|
|
5366
5858
|
var inputSchema30 = {
|
|
5367
|
-
artifact_id:
|
|
5859
|
+
artifact_id: z32.string().min(1).describe("UUID of the artifact."),
|
|
5368
5860
|
files: artifactFilesSchema,
|
|
5369
|
-
base_revision:
|
|
5861
|
+
base_revision: z32.number().int().positive().optional().describe(
|
|
5370
5862
|
"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
5863
|
),
|
|
5372
|
-
remove:
|
|
5373
|
-
expected_version:
|
|
5864
|
+
remove: z32.array(z32.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
|
|
5865
|
+
expected_version: z32.number().int().nonnegative().optional().describe(
|
|
5374
5866
|
"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
5867
|
),
|
|
5376
|
-
message:
|
|
5377
|
-
entry_path:
|
|
5868
|
+
message: z32.string().optional().describe("Short summary of the change (max 500 characters)."),
|
|
5869
|
+
entry_path: z32.string().optional().describe("Change the file a renderer opens first, from this revision on.")
|
|
5378
5870
|
};
|
|
5379
5871
|
var writeArtifactRevisionTool = {
|
|
5380
5872
|
name: "write_artifact_revision",
|
|
@@ -5413,12 +5905,12 @@ var writeArtifactRevisionTool = {
|
|
|
5413
5905
|
};
|
|
5414
5906
|
|
|
5415
5907
|
// src/server/tools/rollback_artifact.ts
|
|
5416
|
-
import { z as
|
|
5908
|
+
import { z as z33 } from "zod";
|
|
5417
5909
|
var inputSchema31 = {
|
|
5418
|
-
artifact_id:
|
|
5419
|
-
revision_number:
|
|
5420
|
-
expected_version:
|
|
5421
|
-
message:
|
|
5910
|
+
artifact_id: z33.string().min(1).describe("UUID of the artifact."),
|
|
5911
|
+
revision_number: z33.number().int().positive().describe("The revision whose files to restore."),
|
|
5912
|
+
expected_version: z33.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
|
|
5913
|
+
message: z33.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
|
|
5422
5914
|
};
|
|
5423
5915
|
var rollbackArtifactTool = {
|
|
5424
5916
|
name: "rollback_artifact",
|
|
@@ -5443,10 +5935,10 @@ var rollbackArtifactTool = {
|
|
|
5443
5935
|
};
|
|
5444
5936
|
|
|
5445
5937
|
// src/server/tools/create_stream_token.ts
|
|
5446
|
-
import { z as
|
|
5938
|
+
import { z as z35 } from "zod";
|
|
5447
5939
|
|
|
5448
5940
|
// src/server/tools/stream-token-output.ts
|
|
5449
|
-
import { z as
|
|
5941
|
+
import { z as z34 } from "zod";
|
|
5450
5942
|
var projectStreamEventTypes = [
|
|
5451
5943
|
"project.updated",
|
|
5452
5944
|
"project.deleted",
|
|
@@ -5454,6 +5946,10 @@ var projectStreamEventTypes = [
|
|
|
5454
5946
|
"spec_file.updated",
|
|
5455
5947
|
"spec_file.deleted",
|
|
5456
5948
|
"spec_file.lock",
|
|
5949
|
+
"spec_file_comment.created",
|
|
5950
|
+
"spec_file_comment.updated",
|
|
5951
|
+
"spec_file_comment.thread_state_changed",
|
|
5952
|
+
"spec_file_comment.anchors_updated",
|
|
5457
5953
|
"attachment.created",
|
|
5458
5954
|
"attachment.deleted",
|
|
5459
5955
|
"spec_session.created",
|
|
@@ -5461,8 +5957,8 @@ var projectStreamEventTypes = [
|
|
|
5461
5957
|
"spec_session.deleted",
|
|
5462
5958
|
"*"
|
|
5463
5959
|
];
|
|
5464
|
-
var streamEventTypesSchema =
|
|
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.'
|
|
5960
|
+
var streamEventTypesSchema = z34.array(z34.enum(projectStreamEventTypes)).min(1).describe(
|
|
5961
|
+
'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
5962
|
);
|
|
5467
5963
|
function streamTokenSummary(token) {
|
|
5468
5964
|
return {
|
|
@@ -5492,13 +5988,13 @@ function streamTokenMintOutput(result) {
|
|
|
5492
5988
|
|
|
5493
5989
|
// src/server/tools/create_stream_token.ts
|
|
5494
5990
|
var inputSchema32 = {
|
|
5495
|
-
resource_type:
|
|
5496
|
-
resource_id:
|
|
5991
|
+
resource_type: z35.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
|
|
5992
|
+
resource_id: z35.string().min(1).describe("UUID of the project to stream events for."),
|
|
5497
5993
|
event_types: streamEventTypesSchema,
|
|
5498
|
-
ttl_seconds:
|
|
5994
|
+
ttl_seconds: z35.number().int().positive().optional().describe(
|
|
5499
5995
|
"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
5996
|
),
|
|
5501
|
-
label:
|
|
5997
|
+
label: z35.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
|
|
5502
5998
|
};
|
|
5503
5999
|
var createStreamTokenTool = {
|
|
5504
6000
|
name: "create_stream_token",
|
|
@@ -5517,17 +6013,17 @@ var createStreamTokenTool = {
|
|
|
5517
6013
|
};
|
|
5518
6014
|
|
|
5519
6015
|
// src/server/tools/list_stream_tokens.ts
|
|
5520
|
-
import { z as
|
|
6016
|
+
import { z as z36 } from "zod";
|
|
5521
6017
|
var LIST_DEFAULT5 = 50;
|
|
5522
6018
|
var LIST_MAX5 = 200;
|
|
5523
6019
|
var inputSchema33 = {
|
|
5524
|
-
resource_id:
|
|
5525
|
-
live_only:
|
|
5526
|
-
mine_only:
|
|
6020
|
+
resource_id: z36.string().optional().describe("Only tokens following this project."),
|
|
6021
|
+
live_only: z36.boolean().optional().describe("Hide expired and revoked tokens."),
|
|
6022
|
+
mine_only: z36.boolean().optional().describe(
|
|
5527
6023
|
"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
6024
|
),
|
|
5529
|
-
limit:
|
|
5530
|
-
offset:
|
|
6025
|
+
limit: z36.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
|
|
6026
|
+
offset: z36.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
|
|
5531
6027
|
};
|
|
5532
6028
|
var listStreamTokensTool = {
|
|
5533
6029
|
name: "list_stream_tokens",
|
|
@@ -5555,9 +6051,9 @@ var listStreamTokensTool = {
|
|
|
5555
6051
|
};
|
|
5556
6052
|
|
|
5557
6053
|
// src/server/tools/revoke_stream_token.ts
|
|
5558
|
-
import { z as
|
|
6054
|
+
import { z as z37 } from "zod";
|
|
5559
6055
|
var inputSchema34 = {
|
|
5560
|
-
id:
|
|
6056
|
+
id: z37.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
|
|
5561
6057
|
};
|
|
5562
6058
|
var revokeStreamTokenTool = {
|
|
5563
6059
|
name: "revoke_stream_token",
|
|
@@ -5570,7 +6066,7 @@ var revokeStreamTokenTool = {
|
|
|
5570
6066
|
};
|
|
5571
6067
|
|
|
5572
6068
|
// src/server/tools/start_spec_session.ts
|
|
5573
|
-
import { z as
|
|
6069
|
+
import { z as z38 } from "zod";
|
|
5574
6070
|
|
|
5575
6071
|
// src/server/tools/start-session-core.ts
|
|
5576
6072
|
var START_SESSION_WAIT_MS = 75e3;
|
|
@@ -5963,27 +6459,27 @@ var START_SESSION_DESCRIPTION_BASE = "Start a NEW MySpec chat session on the use
|
|
|
5963
6459
|
// src/server/tools/start_spec_session.ts
|
|
5964
6460
|
var BRIDGE_COMMAND = "npx -y @myspec/mcp-server reverse --root <path>";
|
|
5965
6461
|
var inputSchema35 = {
|
|
5966
|
-
project_id:
|
|
5967
|
-
mode:
|
|
6462
|
+
project_id: z38.string().min(1).describe("UUID of the project to start the session in."),
|
|
6463
|
+
mode: z38.enum(["generate", "edit"]).describe(
|
|
5968
6464
|
"'generate' writes a new spec bundle from scratch; 'edit' changes the spec files of an existing bundle."
|
|
5969
6465
|
),
|
|
5970
|
-
prompt:
|
|
6466
|
+
prompt: z38.string().min(1).describe(
|
|
5971
6467
|
"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
6468
|
),
|
|
5973
|
-
workflow_id:
|
|
6469
|
+
workflow_id: z38.string().optional().describe(
|
|
5974
6470
|
"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
6471
|
),
|
|
5976
|
-
language:
|
|
6472
|
+
language: z38.enum(SPEC_SESSION_LANGUAGES).optional().describe(
|
|
5977
6473
|
"Output language the session locks to on its first turn. Defaults to 'en'. A session's language is immutable once set."
|
|
5978
6474
|
),
|
|
5979
|
-
model_id:
|
|
5980
|
-
attachment_ids:
|
|
6475
|
+
model_id: z38.string().optional().describe("Model id to run the session with, e.g. openai:gpt-6-astra."),
|
|
6476
|
+
attachment_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
|
|
5981
6477
|
`Project attachment ids to cite in the opening message, as the composer @-mentions do. At most ${String(MAX_REFERENCE_IDS)}.`
|
|
5982
6478
|
),
|
|
5983
|
-
spec_file_ids:
|
|
6479
|
+
spec_file_ids: z38.array(z38.string().min(1)).max(MAX_REFERENCE_IDS).optional().describe(
|
|
5984
6480
|
`Existing spec file ids to cite in the opening message. Required in practice for edit sessions. At most ${String(MAX_REFERENCE_IDS)}.`
|
|
5985
6481
|
),
|
|
5986
|
-
session_title:
|
|
6482
|
+
session_title: z38.string().min(1).max(SESSION_TITLE_MAX_LENGTH).optional().describe(
|
|
5987
6483
|
`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
6484
|
)
|
|
5989
6485
|
};
|
|
@@ -6213,6 +6709,11 @@ function buildServer(deps) {
|
|
|
6213
6709
|
registerTool(server, listStreamTokensTool, ctx);
|
|
6214
6710
|
registerTool(server, revokeStreamTokenTool, ctx);
|
|
6215
6711
|
registerTool(server, startSpecSessionTool, ctx);
|
|
6712
|
+
registerTool(server, listSpecFileCommentsTool, ctx);
|
|
6713
|
+
registerTool(server, createSpecFileCommentThreadTool, ctx);
|
|
6714
|
+
registerTool(server, replySpecFileCommentThreadTool, ctx);
|
|
6715
|
+
registerTool(server, updateSpecFileCommentTool, ctx);
|
|
6716
|
+
registerTool(server, deleteSpecFileCommentTool, ctx);
|
|
6216
6717
|
return server;
|
|
6217
6718
|
}
|
|
6218
6719
|
async function startStdioServer(deps) {
|
|
@@ -6366,7 +6867,7 @@ async function canonicaliseRoot(root) {
|
|
|
6366
6867
|
import { promises as fs4 } from "fs";
|
|
6367
6868
|
import { join as join2, relative } from "path";
|
|
6368
6869
|
import { spawn } from "child_process";
|
|
6369
|
-
import { z as
|
|
6870
|
+
import { z as z39 } from "zod";
|
|
6370
6871
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6371
6872
|
|
|
6372
6873
|
// src/reverse/ignore.ts
|
|
@@ -6671,37 +7172,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
6671
7172
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
6672
7173
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
6673
7174
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
6674
|
-
var listDirInput =
|
|
6675
|
-
path:
|
|
6676
|
-
recursive:
|
|
6677
|
-
maxEntries:
|
|
6678
|
-
includeIgnored:
|
|
7175
|
+
var listDirInput = z39.object({
|
|
7176
|
+
path: z39.string(),
|
|
7177
|
+
recursive: z39.boolean().optional(),
|
|
7178
|
+
maxEntries: z39.number().int().positive().optional(),
|
|
7179
|
+
includeIgnored: z39.boolean().optional()
|
|
6679
7180
|
});
|
|
6680
|
-
var readFileInput =
|
|
6681
|
-
path:
|
|
6682
|
-
offset:
|
|
6683
|
-
limit:
|
|
6684
|
-
includeIgnored:
|
|
7181
|
+
var readFileInput = z39.object({
|
|
7182
|
+
path: z39.string(),
|
|
7183
|
+
offset: z39.number().int().nonnegative().optional(),
|
|
7184
|
+
limit: z39.number().int().positive().optional(),
|
|
7185
|
+
includeIgnored: z39.boolean().optional()
|
|
6685
7186
|
});
|
|
6686
|
-
var grepInput =
|
|
6687
|
-
pattern:
|
|
7187
|
+
var grepInput = z39.object({
|
|
7188
|
+
pattern: z39.string().describe(
|
|
6688
7189
|
"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
7190
|
),
|
|
6690
|
-
path:
|
|
6691
|
-
isRegex:
|
|
6692
|
-
caseSensitive:
|
|
6693
|
-
maxMatches:
|
|
6694
|
-
contextLines:
|
|
6695
|
-
includeIgnored:
|
|
7191
|
+
path: z39.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
|
|
7192
|
+
isRegex: z39.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
|
|
7193
|
+
caseSensitive: z39.boolean().optional(),
|
|
7194
|
+
maxMatches: z39.number().int().positive().optional(),
|
|
7195
|
+
contextLines: z39.number().int().nonnegative().optional(),
|
|
7196
|
+
includeIgnored: z39.boolean().optional()
|
|
6696
7197
|
});
|
|
6697
|
-
var packCodebaseInput =
|
|
6698
|
-
subpath:
|
|
6699
|
-
includePatterns:
|
|
6700
|
-
ignorePatterns:
|
|
7198
|
+
var packCodebaseInput = z39.object({
|
|
7199
|
+
subpath: z39.string().optional(),
|
|
7200
|
+
includePatterns: z39.string().optional(),
|
|
7201
|
+
ignorePatterns: z39.string().optional()
|
|
6701
7202
|
});
|
|
6702
|
-
var packCodebaseReadPageInput =
|
|
6703
|
-
outputId:
|
|
6704
|
-
page:
|
|
7203
|
+
var packCodebaseReadPageInput = z39.object({
|
|
7204
|
+
outputId: z39.string(),
|
|
7205
|
+
page: z39.number().int().positive()
|
|
6705
7206
|
});
|
|
6706
7207
|
function structuredError(error, message, extra) {
|
|
6707
7208
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -7229,7 +7730,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
7229
7730
|
return zodToJson(schema);
|
|
7230
7731
|
}
|
|
7231
7732
|
function zodToJson(schema) {
|
|
7232
|
-
if (schema instanceof
|
|
7733
|
+
if (schema instanceof z39.ZodObject) {
|
|
7233
7734
|
const shape = schema.shape;
|
|
7234
7735
|
const properties = {};
|
|
7235
7736
|
const required = [];
|
|
@@ -7244,19 +7745,19 @@ function zodToJson(schema) {
|
|
|
7244
7745
|
if (required.length > 0) out.required = required;
|
|
7245
7746
|
return out;
|
|
7246
7747
|
}
|
|
7247
|
-
if (schema instanceof
|
|
7748
|
+
if (schema instanceof z39.ZodOptional) {
|
|
7248
7749
|
return zodToJson(schema.unwrap());
|
|
7249
7750
|
}
|
|
7250
|
-
if (schema instanceof
|
|
7751
|
+
if (schema instanceof z39.ZodString) {
|
|
7251
7752
|
return { type: "string" };
|
|
7252
7753
|
}
|
|
7253
|
-
if (schema instanceof
|
|
7754
|
+
if (schema instanceof z39.ZodNumber) {
|
|
7254
7755
|
return { type: "number" };
|
|
7255
7756
|
}
|
|
7256
|
-
if (schema instanceof
|
|
7757
|
+
if (schema instanceof z39.ZodBoolean) {
|
|
7257
7758
|
return { type: "boolean" };
|
|
7258
7759
|
}
|
|
7259
|
-
if (schema instanceof
|
|
7760
|
+
if (schema instanceof z39.ZodEnum) {
|
|
7260
7761
|
return { type: "string", enum: schema.options };
|
|
7261
7762
|
}
|
|
7262
7763
|
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.
|
|
3
|
+
"version": "0.5.0-next.117",
|
|
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": {
|