@myspec/mcp-server 0.4.0-next.99 → 0.5.0-next.112
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 +8 -2
- package/dist/index.js +1040 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -291,9 +291,9 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
291
291
|
| `list_projects` | `query?`, `limit?`, `offset?` | Paginated list of projects |
|
|
292
292
|
| `get_project` | `project_id` | Project + active sessions + files + attachments |
|
|
293
293
|
| `get_spec_file` | `file_id`, optional `include_download_url` | File metadata, including `content_version` (see [Concurrent edits](#concurrent-edits-content_version)); with `include_download_url=N` also returns a download URL targeting revision `N` |
|
|
294
|
-
| `read_spec_file` | `file_id`, optional `revision` | UTF-8 content
|
|
294
|
+
| `read_spec_file` | `file_id`, optional `revision`, optional `offset`, optional `limit` | UTF-8 content paginated by line (`offset` is 1-based, default/cap 2000 lines, 1 MiB per response) with `content`, `start_line`, `end_line`, `read_lines`, `total_lines`, `truncated`, `next_offset`; plus `content_version` when reading the latest revision (errors for binary or files > 8 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT`, so paging through a file downloads it once |
|
|
295
295
|
| `download_spec_file` | `file_id`, optional `destination_path`, optional `revision`, optional `overwrite` | Downloads the file's raw bytes to a local file — by default under `.specs/` in the working directory (see [Download destinations](#download-destinations)). Handles binary files and files up to 50 MiB. Refuses to replace an existing file unless `overwrite=true`. Returns only metadata (`saved_path`, `bytes_written`, `revision_number`, `destination_source`) — **not** the content, so a large file can be saved locally and read in chunks |
|
|
296
|
-
| `list_spec_file` | `project_id`, optional `path`, optional `trashed` | Lists a project's spec files (`file_id`, `file_path`, `file_type`, `file_size_bytes`, `revision_count`, `session_id`, `updated_at`). Filter to a directory with `path` (exact file or everything under it; `""`/`"/"`/omit = all). `trashed=true` lists the Trash Bin instead of live files |
|
|
296
|
+
| `list_spec_file` | `project_id`, optional `path`, optional `trashed`, optional `limit`, optional `offset` | Lists a project's spec files (`file_id`, `file_path`, `file_type`, `file_size_bytes`, `revision_count`, `session_id`, `updated_at`, plus `trashed_at` on Trash Bin entries). Filter to a directory with `path` (exact file or everything under it; `""`/`"/"`/omit = all). `trashed=true` lists the Trash Bin instead of live files. Returns `total`, `limit`, `offset` and, when more records remain, `next_offset` (default 50, capped at 200). Ordered by `file_path` when `path` or `trashed` is given, most-recently-created first otherwise |
|
|
297
297
|
| `move_spec_file_to_trash` | `file_id` | Moves a spec file to the Trash Bin (recoverable — hidden from listings and AI-agent tools, restorable indefinitely). Not a permanent delete |
|
|
298
298
|
| `restore_spec_file_from_trash` | `file_id` | Restores a trashed spec file back to the active file list, reversing `move_spec_file_to_trash` |
|
|
299
299
|
| `get_attachment` | `attachment_id`, optional `include_download_url` | Attachment metadata (looked up org-scoped by id); with `include_download_url>0` also returns a signed download URL |
|
|
@@ -306,6 +306,12 @@ Local state is stored under `~/.myspec/` (alongside the on-disk cache root), spl
|
|
|
306
306
|
| `archive_spec_session` | `session_id` | Archives the session (idempotent — re-archiving keeps the original `archived_at`); the required confirmation step before `delete_spec_session` |
|
|
307
307
|
| `unarchive_spec_session` | `session_id` | Returns an archived session to the active list |
|
|
308
308
|
| `delete_spec_session` | `session_id` | Soft-deletes an ARCHIVED session. Refuses non-archived sessions with an archive-first error and never archives on the caller's behalf |
|
|
309
|
+
| `list_artifacts` | `project_id`, optional `limit`, optional `offset` | Paginated list of a project's live artifacts — multi-file bundles (an HTML mockup: `index.html` + `styles.css` + `app.js` + assets) the platform versions as ONE snapshot. Each entry carries `revision_count`, the head number and the `expected_version` to pass when writing |
|
|
310
|
+
| `get_artifact` | `artifact_id`, optional `revision` | Artifact metadata plus the file manifest (path, content type, size, checksum) of the head revision, or of `revision` |
|
|
311
|
+
| `read_artifact_file` | `artifact_id`, `path`, optional `revision`, optional `offset`, optional `limit` | One file's UTF-8 content paginated by line (same `content`/`total_lines`/`truncated`/`next_offset` shape as `read_attachment`); binary files are refused |
|
|
312
|
+
| `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 |
|
|
313
|
+
| `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 |
|
|
314
|
+
| `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 |
|
|
309
315
|
|
|
310
316
|
### Download destinations
|
|
311
317
|
|
package/dist/index.js
CHANGED
|
@@ -32,10 +32,11 @@ var REFRESH_TOKEN_PATTERN = /"refreshToken"\s*:\s*"[^"]+"/g;
|
|
|
32
32
|
var API_TOKEN_PATTERN = /"apiToken"\s*:\s*"[^"]+"/g;
|
|
33
33
|
var API_TOKEN_VALUE_PATTERN = /\bmsp_pat_[A-Za-z0-9]+/g;
|
|
34
34
|
var ACCESS_TOKEN_PATTERN = /"accessToken"\s*:\s*"[^"]+"/g;
|
|
35
|
-
var SNAKE_TOKEN_PATTERN = /"(access_token|refresh_token|id_token)"\s*:\s*"[^"]+"/g;
|
|
35
|
+
var SNAKE_TOKEN_PATTERN = /"(access_token|refresh_token|id_token|token)"\s*:\s*"[^"]+"/g;
|
|
36
36
|
var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
37
|
+
var STREAM_URL_PATTERN = /\/realtime\/v1\/stream\/[A-Za-z0-9_-]{20,}/g;
|
|
37
38
|
function redactSensitive(value) {
|
|
38
|
-
return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(API_TOKEN_PATTERN, '"apiToken":"[REDACTED]"').replace(API_TOKEN_VALUE_PATTERN, "[REDACTED_API_TOKEN]").replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]");
|
|
39
|
+
return value.replace(BEARER_PATTERN, "Bearer [REDACTED]").replace(REFRESH_TOKEN_PATTERN, '"refreshToken":"[REDACTED]"').replace(API_TOKEN_PATTERN, '"apiToken":"[REDACTED]"').replace(API_TOKEN_VALUE_PATTERN, "[REDACTED_API_TOKEN]").replace(ACCESS_TOKEN_PATTERN, '"accessToken":"[REDACTED]"').replace(SNAKE_TOKEN_PATTERN, (_match, key) => `"${key}":"[REDACTED]"`).replace(JWT_PATTERN, "[REDACTED_JWT]").replace(STREAM_URL_PATTERN, "/realtime/v1/stream/[REDACTED_STREAM_TOKEN]");
|
|
39
40
|
}
|
|
40
41
|
var ConfigError = class extends Error {
|
|
41
42
|
constructor(message) {
|
|
@@ -1349,7 +1350,10 @@ var KNOWN_CONFLICT_REASONS = [
|
|
|
1349
1350
|
"revision_mismatch",
|
|
1350
1351
|
"path_taken",
|
|
1351
1352
|
"precondition_required",
|
|
1352
|
-
"precondition_malformed"
|
|
1353
|
+
"precondition_malformed",
|
|
1354
|
+
"slug_taken",
|
|
1355
|
+
"blob_reclaimed",
|
|
1356
|
+
"lock_timeout"
|
|
1353
1357
|
];
|
|
1354
1358
|
var PlatformHttpError = class extends Error {
|
|
1355
1359
|
status;
|
|
@@ -2436,6 +2440,404 @@ var HttpSpecSessionClient = class {
|
|
|
2436
2440
|
}
|
|
2437
2441
|
};
|
|
2438
2442
|
|
|
2443
|
+
// ../../packages/platform-client/artifact/types/artifact.types.ts
|
|
2444
|
+
function transformArtifactResponse(raw) {
|
|
2445
|
+
return {
|
|
2446
|
+
id: raw.id,
|
|
2447
|
+
projectId: raw.project_id,
|
|
2448
|
+
org: raw.org,
|
|
2449
|
+
sessionId: raw.session_id ?? null,
|
|
2450
|
+
kind: raw.kind,
|
|
2451
|
+
name: raw.name,
|
|
2452
|
+
slug: raw.slug,
|
|
2453
|
+
description: raw.description ?? "",
|
|
2454
|
+
entryPath: raw.entry_path,
|
|
2455
|
+
revisionCount: raw.revision_count,
|
|
2456
|
+
createdBy: raw.created_by,
|
|
2457
|
+
createdAt: new Date(raw.created_at),
|
|
2458
|
+
updatedAt: new Date(raw.updated_at),
|
|
2459
|
+
trashedAt: raw.trashed_at ? new Date(raw.trashed_at) : void 0,
|
|
2460
|
+
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function transformArtifactRevisionResponse(raw) {
|
|
2464
|
+
return {
|
|
2465
|
+
id: raw.id,
|
|
2466
|
+
artifactId: raw.artifact_id,
|
|
2467
|
+
revisionNumber: raw.revision_number,
|
|
2468
|
+
source: raw.source,
|
|
2469
|
+
restoredFromRevision: raw.restored_from_revision,
|
|
2470
|
+
message: raw.message ?? "",
|
|
2471
|
+
changedBy: raw.changed_by,
|
|
2472
|
+
changedByKind: raw.changed_by_kind === "ai" ? "ai" : "human",
|
|
2473
|
+
sessionId: raw.session_id ?? null,
|
|
2474
|
+
fileCount: raw.file_count,
|
|
2475
|
+
totalBytes: raw.total_bytes,
|
|
2476
|
+
manifestChecksum: raw.manifest_checksum,
|
|
2477
|
+
createdAt: new Date(raw.created_at)
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
function transformArtifactFileEntry(raw) {
|
|
2481
|
+
return {
|
|
2482
|
+
path: raw.path,
|
|
2483
|
+
contentType: raw.content_type,
|
|
2484
|
+
sizeBytes: raw.size_bytes,
|
|
2485
|
+
checksum: raw.checksum
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
function transformArtifactRevisionDetail(raw) {
|
|
2489
|
+
return {
|
|
2490
|
+
...transformArtifactRevisionResponse(raw),
|
|
2491
|
+
files: (raw.files ?? []).map(transformArtifactFileEntry)
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
function transformArtifactDetail(raw) {
|
|
2495
|
+
return {
|
|
2496
|
+
artifact: transformArtifactResponse(raw.artifact),
|
|
2497
|
+
head: transformArtifactRevisionDetail(raw.head)
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function transformArtifactWriteResult(raw) {
|
|
2501
|
+
return {
|
|
2502
|
+
artifact: transformArtifactResponse(raw.artifact),
|
|
2503
|
+
revision: transformArtifactRevisionDetail(raw.revision),
|
|
2504
|
+
noChange: raw.no_change === true
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
function transformArtifactListResult(raw) {
|
|
2508
|
+
return {
|
|
2509
|
+
items: (raw.items ?? []).map(transformArtifactResponse),
|
|
2510
|
+
total: raw.total,
|
|
2511
|
+
limit: raw.limit,
|
|
2512
|
+
offset: raw.offset
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function transformArtifactRevisionListResult(raw) {
|
|
2516
|
+
return {
|
|
2517
|
+
items: (raw.items ?? []).map(transformArtifactRevisionResponse),
|
|
2518
|
+
total: raw.total,
|
|
2519
|
+
limit: raw.limit,
|
|
2520
|
+
offset: raw.offset
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function buildFilesBody(files) {
|
|
2524
|
+
return files.map((f) => {
|
|
2525
|
+
const body = { path: f.path, content: f.content };
|
|
2526
|
+
if (f.encoding) {
|
|
2527
|
+
body.encoding = f.encoding;
|
|
2528
|
+
}
|
|
2529
|
+
if (f.contentType) {
|
|
2530
|
+
body.content_type = f.contentType;
|
|
2531
|
+
}
|
|
2532
|
+
return body;
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
function buildCreateArtifactBody(request) {
|
|
2536
|
+
const body = {
|
|
2537
|
+
name: request.name,
|
|
2538
|
+
files: buildFilesBody(request.files)
|
|
2539
|
+
};
|
|
2540
|
+
if (request.slug) body.slug = request.slug;
|
|
2541
|
+
if (request.kind) body.kind = request.kind;
|
|
2542
|
+
if (request.description !== void 0) body.description = request.description;
|
|
2543
|
+
if (request.entryPath) body.entry_path = request.entryPath;
|
|
2544
|
+
if (request.message) body.message = request.message;
|
|
2545
|
+
if (request.sessionId) body.session_id = request.sessionId;
|
|
2546
|
+
return body;
|
|
2547
|
+
}
|
|
2548
|
+
function buildCreateArtifactRevisionBody(request) {
|
|
2549
|
+
const body = { files: buildFilesBody(request.files) };
|
|
2550
|
+
if (request.message) body.message = request.message;
|
|
2551
|
+
if (request.baseRevision !== void 0) body.base_revision = request.baseRevision;
|
|
2552
|
+
if (request.remove && request.remove.length > 0) body.remove = request.remove;
|
|
2553
|
+
if (request.entryPath) body.entry_path = request.entryPath;
|
|
2554
|
+
if (request.sessionId) body.session_id = request.sessionId;
|
|
2555
|
+
return body;
|
|
2556
|
+
}
|
|
2557
|
+
function buildRollbackArtifactBody(request) {
|
|
2558
|
+
const body = { revision_number: request.revisionNumber };
|
|
2559
|
+
if (request.message) body.message = request.message;
|
|
2560
|
+
if (request.sessionId) body.session_id = request.sessionId;
|
|
2561
|
+
return body;
|
|
2562
|
+
}
|
|
2563
|
+
function buildUpdateArtifactBody(request) {
|
|
2564
|
+
const body = {};
|
|
2565
|
+
if (request.name !== void 0) body.name = request.name;
|
|
2566
|
+
if (request.description !== void 0) body.description = request.description;
|
|
2567
|
+
if (request.entryPath !== void 0) body.entry_path = request.entryPath;
|
|
2568
|
+
return body;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// ../../packages/platform-client/artifact/http/artifact.http-client.ts
|
|
2572
|
+
var BASE_PATH = "/artifact/v1";
|
|
2573
|
+
var HEADER_EXPECTED_VERSION2 = "X-Expected-Version";
|
|
2574
|
+
var HEADER_ACTOR_KIND2 = "X-Actor-Kind";
|
|
2575
|
+
function writeHeaders(opts) {
|
|
2576
|
+
const headers = {};
|
|
2577
|
+
if (opts?.expectedVersion !== void 0) {
|
|
2578
|
+
headers[HEADER_EXPECTED_VERSION2] = String(opts.expectedVersion);
|
|
2579
|
+
}
|
|
2580
|
+
if (opts?.actorKind) {
|
|
2581
|
+
headers[HEADER_ACTOR_KIND2] = opts.actorKind;
|
|
2582
|
+
}
|
|
2583
|
+
return headers;
|
|
2584
|
+
}
|
|
2585
|
+
function requestOptions(headers) {
|
|
2586
|
+
return Object.keys(headers).length > 0 ? { headers } : {};
|
|
2587
|
+
}
|
|
2588
|
+
function encodeArtifactPath(path4) {
|
|
2589
|
+
return path4.split("/").map(encodeURIComponent).join("/");
|
|
2590
|
+
}
|
|
2591
|
+
function paginationQuery(opts) {
|
|
2592
|
+
const params = new URLSearchParams();
|
|
2593
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
2594
|
+
if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
|
|
2595
|
+
if (opts?.kind) params.set("kind", opts.kind);
|
|
2596
|
+
const query = params.toString();
|
|
2597
|
+
return query ? `?${query}` : "";
|
|
2598
|
+
}
|
|
2599
|
+
var HttpArtifactClient = class {
|
|
2600
|
+
httpClient;
|
|
2601
|
+
constructor(httpClient) {
|
|
2602
|
+
this.httpClient = httpClient;
|
|
2603
|
+
}
|
|
2604
|
+
async createArtifact(projectId, request, jwtToken, opts) {
|
|
2605
|
+
const raw = await this.httpClient.post(
|
|
2606
|
+
`${BASE_PATH}/projects/${projectId}/artifacts`,
|
|
2607
|
+
buildCreateArtifactBody(request),
|
|
2608
|
+
jwtToken,
|
|
2609
|
+
requestOptions(writeHeaders(opts))
|
|
2610
|
+
);
|
|
2611
|
+
return transformArtifactWriteResult(raw);
|
|
2612
|
+
}
|
|
2613
|
+
async listArtifacts(projectId, jwtToken, opts) {
|
|
2614
|
+
const raw = await this.httpClient.get(
|
|
2615
|
+
`${BASE_PATH}/projects/${projectId}/artifacts${paginationQuery(opts)}`,
|
|
2616
|
+
jwtToken,
|
|
2617
|
+
{}
|
|
2618
|
+
);
|
|
2619
|
+
return transformArtifactListResult(raw);
|
|
2620
|
+
}
|
|
2621
|
+
async listTrashedArtifacts(projectId, jwtToken) {
|
|
2622
|
+
const raw = await this.httpClient.get(
|
|
2623
|
+
`${BASE_PATH}/projects/${projectId}/artifacts/trashed`,
|
|
2624
|
+
jwtToken,
|
|
2625
|
+
{}
|
|
2626
|
+
);
|
|
2627
|
+
return (raw.items ?? []).map(transformArtifactResponse);
|
|
2628
|
+
}
|
|
2629
|
+
async getArtifact(artifactId, jwtToken) {
|
|
2630
|
+
const raw = await this.httpClient.get(
|
|
2631
|
+
`${BASE_PATH}/artifacts/${artifactId}`,
|
|
2632
|
+
jwtToken,
|
|
2633
|
+
{}
|
|
2634
|
+
);
|
|
2635
|
+
return transformArtifactDetail(raw);
|
|
2636
|
+
}
|
|
2637
|
+
async updateArtifact(artifactId, request, jwtToken) {
|
|
2638
|
+
const raw = await this.httpClient.patch(
|
|
2639
|
+
`${BASE_PATH}/artifacts/${artifactId}`,
|
|
2640
|
+
buildUpdateArtifactBody(request),
|
|
2641
|
+
jwtToken,
|
|
2642
|
+
{}
|
|
2643
|
+
);
|
|
2644
|
+
return transformArtifactResponse(raw);
|
|
2645
|
+
}
|
|
2646
|
+
async trashArtifact(artifactId, jwtToken) {
|
|
2647
|
+
await this.httpClient.delete(`${BASE_PATH}/artifacts/${artifactId}`, jwtToken, {});
|
|
2648
|
+
}
|
|
2649
|
+
async restoreArtifact(artifactId, jwtToken) {
|
|
2650
|
+
await this.httpClient.post(
|
|
2651
|
+
`${BASE_PATH}/artifacts/${artifactId}/restore`,
|
|
2652
|
+
{},
|
|
2653
|
+
jwtToken,
|
|
2654
|
+
{}
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
async deleteArtifactPermanently(artifactId, jwtToken) {
|
|
2658
|
+
await this.httpClient.delete(
|
|
2659
|
+
`${BASE_PATH}/artifacts/${artifactId}/permanent`,
|
|
2660
|
+
jwtToken,
|
|
2661
|
+
{}
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
async createRevision(artifactId, request, jwtToken, opts) {
|
|
2665
|
+
const raw = await this.httpClient.post(
|
|
2666
|
+
`${BASE_PATH}/artifacts/${artifactId}/revisions`,
|
|
2667
|
+
buildCreateArtifactRevisionBody(request),
|
|
2668
|
+
jwtToken,
|
|
2669
|
+
requestOptions(writeHeaders(opts))
|
|
2670
|
+
);
|
|
2671
|
+
return transformArtifactWriteResult(raw);
|
|
2672
|
+
}
|
|
2673
|
+
async rollback(artifactId, request, jwtToken, opts) {
|
|
2674
|
+
const raw = await this.httpClient.post(
|
|
2675
|
+
`${BASE_PATH}/artifacts/${artifactId}/rollback`,
|
|
2676
|
+
buildRollbackArtifactBody(request),
|
|
2677
|
+
jwtToken,
|
|
2678
|
+
requestOptions(writeHeaders(opts))
|
|
2679
|
+
);
|
|
2680
|
+
return transformArtifactWriteResult(raw);
|
|
2681
|
+
}
|
|
2682
|
+
async listRevisions(artifactId, jwtToken, opts) {
|
|
2683
|
+
const raw = await this.httpClient.get(
|
|
2684
|
+
`${BASE_PATH}/artifacts/${artifactId}/revisions${paginationQuery(opts)}`,
|
|
2685
|
+
jwtToken,
|
|
2686
|
+
{}
|
|
2687
|
+
);
|
|
2688
|
+
return transformArtifactRevisionListResult(raw);
|
|
2689
|
+
}
|
|
2690
|
+
async getRevision(artifactId, revisionNumber, jwtToken) {
|
|
2691
|
+
const raw = await this.httpClient.get(
|
|
2692
|
+
`${BASE_PATH}/artifacts/${artifactId}/revisions/${revisionNumber}`,
|
|
2693
|
+
jwtToken,
|
|
2694
|
+
{}
|
|
2695
|
+
);
|
|
2696
|
+
return transformArtifactRevisionDetail(raw);
|
|
2697
|
+
}
|
|
2698
|
+
fileUrl(artifactId, path4, revision) {
|
|
2699
|
+
const baseUrl = this.httpClient.getBaseUrl().replace(/\/$/, "");
|
|
2700
|
+
const encoded = encodeArtifactPath(path4);
|
|
2701
|
+
return revision === void 0 ? `${baseUrl}${BASE_PATH}/artifacts/${artifactId}/files/${encoded}` : `${baseUrl}${BASE_PATH}/artifacts/${artifactId}/revisions/${revision}/files/${encoded}`;
|
|
2702
|
+
}
|
|
2703
|
+
async downloadFile(artifactId, path4, jwtToken, opts) {
|
|
2704
|
+
const response = await fetch(this.fileUrl(artifactId, path4, opts?.revision), {
|
|
2705
|
+
method: "GET",
|
|
2706
|
+
headers: { Authorization: `Bearer ${jwtToken}` }
|
|
2707
|
+
});
|
|
2708
|
+
if (!response.ok) {
|
|
2709
|
+
await throwFromResponse(response, "Artifact file download failed");
|
|
2710
|
+
}
|
|
2711
|
+
const buffer = await response.arrayBuffer();
|
|
2712
|
+
return {
|
|
2713
|
+
content: new Uint8Array(buffer),
|
|
2714
|
+
contentType: response.headers.get("Content-Type") ?? "application/octet-stream",
|
|
2715
|
+
checksum: (response.headers.get("ETag") ?? "").replace(/"/g, "").trim()
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
// ../../packages/platform-client/stream/types/stream-token.types.ts
|
|
2721
|
+
function optionalDate(value) {
|
|
2722
|
+
return value ? new Date(value) : null;
|
|
2723
|
+
}
|
|
2724
|
+
function transformStreamToken(raw) {
|
|
2725
|
+
return {
|
|
2726
|
+
id: raw.id,
|
|
2727
|
+
tokenPrefix: raw.token_prefix,
|
|
2728
|
+
label: raw.label ?? "",
|
|
2729
|
+
resourceType: raw.resource_type,
|
|
2730
|
+
resourceId: raw.resource_id,
|
|
2731
|
+
eventTypes: raw.event_types ?? [],
|
|
2732
|
+
createdBy: raw.created_by,
|
|
2733
|
+
createdAt: new Date(raw.created_at),
|
|
2734
|
+
expiresAt: new Date(raw.expires_at),
|
|
2735
|
+
revokedAt: optionalDate(raw.revoked_at),
|
|
2736
|
+
resourceDeletedAt: optionalDate(raw.resource_deleted_at),
|
|
2737
|
+
lastConnectedAt: optionalDate(raw.last_connected_at),
|
|
2738
|
+
connectCount: raw.connect_count,
|
|
2739
|
+
live: raw.live
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function transformStreamTokenMintResult(raw) {
|
|
2743
|
+
if (!raw.token || !raw.url) {
|
|
2744
|
+
throw new Error("stream token mint response carried no token or url");
|
|
2745
|
+
}
|
|
2746
|
+
return { ...transformStreamToken(raw), token: raw.token, url: raw.url };
|
|
2747
|
+
}
|
|
2748
|
+
function transformStreamTokenListResult(raw) {
|
|
2749
|
+
return {
|
|
2750
|
+
tokens: (raw.tokens ?? []).map(transformStreamToken),
|
|
2751
|
+
total: raw.total ?? 0
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function transformStreamCatalogue(raw) {
|
|
2755
|
+
return {
|
|
2756
|
+
resources: (raw.resources ?? []).map((resource) => ({
|
|
2757
|
+
resourceType: resource.resource_type,
|
|
2758
|
+
eventTypes: resource.event_types ?? []
|
|
2759
|
+
})),
|
|
2760
|
+
maxTtlSeconds: raw.max_ttl_seconds,
|
|
2761
|
+
defaultTtlSeconds: raw.default_ttl_seconds
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function buildCreateStreamTokenBody(request) {
|
|
2765
|
+
const body = {
|
|
2766
|
+
resource_type: request.resourceType,
|
|
2767
|
+
resource_id: request.resourceId
|
|
2768
|
+
};
|
|
2769
|
+
body.event_types = request.eventTypes;
|
|
2770
|
+
if (request.ttlSeconds !== void 0) {
|
|
2771
|
+
body.ttl_seconds = request.ttlSeconds;
|
|
2772
|
+
}
|
|
2773
|
+
if (request.label) {
|
|
2774
|
+
body.label = request.label;
|
|
2775
|
+
}
|
|
2776
|
+
return body;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// ../../packages/platform-client/stream/http/stream-token.http-client.ts
|
|
2780
|
+
var BASE_PATH2 = "/realtime/v1";
|
|
2781
|
+
function requestOptions2(options) {
|
|
2782
|
+
return options ?? {};
|
|
2783
|
+
}
|
|
2784
|
+
var HttpStreamTokenClient = class {
|
|
2785
|
+
constructor(httpClient) {
|
|
2786
|
+
this.httpClient = httpClient;
|
|
2787
|
+
}
|
|
2788
|
+
httpClient;
|
|
2789
|
+
async createStreamToken(request, jwtToken, options) {
|
|
2790
|
+
const raw = await this.httpClient.post(
|
|
2791
|
+
`${BASE_PATH2}/stream-tokens`,
|
|
2792
|
+
buildCreateStreamTokenBody(request),
|
|
2793
|
+
jwtToken,
|
|
2794
|
+
requestOptions2(options)
|
|
2795
|
+
);
|
|
2796
|
+
return transformStreamTokenMintResult(raw);
|
|
2797
|
+
}
|
|
2798
|
+
async listStreamTokens(options, jwtToken, httpOptions) {
|
|
2799
|
+
const query = new URLSearchParams();
|
|
2800
|
+
if (options?.resourceId) {
|
|
2801
|
+
query.set("resource_id", options.resourceId);
|
|
2802
|
+
}
|
|
2803
|
+
if (options?.liveOnly) {
|
|
2804
|
+
query.set("live", "true");
|
|
2805
|
+
}
|
|
2806
|
+
if (options?.mineOnly) {
|
|
2807
|
+
query.set("mine", "true");
|
|
2808
|
+
}
|
|
2809
|
+
if (options?.limit !== void 0) {
|
|
2810
|
+
query.set("limit", String(options.limit));
|
|
2811
|
+
}
|
|
2812
|
+
if (options?.offset !== void 0) {
|
|
2813
|
+
query.set("offset", String(options.offset));
|
|
2814
|
+
}
|
|
2815
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
2816
|
+
const raw = await this.httpClient.get(
|
|
2817
|
+
`${BASE_PATH2}/stream-tokens${suffix}`,
|
|
2818
|
+
jwtToken,
|
|
2819
|
+
requestOptions2(httpOptions)
|
|
2820
|
+
);
|
|
2821
|
+
return transformStreamTokenListResult(raw);
|
|
2822
|
+
}
|
|
2823
|
+
async revokeStreamToken(id, jwtToken, options) {
|
|
2824
|
+
const raw = await this.httpClient.delete(
|
|
2825
|
+
`${BASE_PATH2}/stream-tokens/${encodeURIComponent(id)}`,
|
|
2826
|
+
jwtToken,
|
|
2827
|
+
requestOptions2(options)
|
|
2828
|
+
);
|
|
2829
|
+
return transformStreamToken(raw);
|
|
2830
|
+
}
|
|
2831
|
+
async getStreamCatalogue(jwtToken, options) {
|
|
2832
|
+
const raw = await this.httpClient.get(
|
|
2833
|
+
`${BASE_PATH2}/stream-catalogue`,
|
|
2834
|
+
jwtToken,
|
|
2835
|
+
requestOptions2(options)
|
|
2836
|
+
);
|
|
2837
|
+
return transformStreamCatalogue(raw);
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2439
2841
|
// src/platform/client.ts
|
|
2440
2842
|
var silentLogger = {
|
|
2441
2843
|
debug: () => void 0,
|
|
@@ -2722,9 +3124,58 @@ var PlatformClient = class {
|
|
|
2722
3124
|
project: new HttpProjectClient(httpClient),
|
|
2723
3125
|
session: new HttpSpecSessionClient(httpClient),
|
|
2724
3126
|
file: new HttpFileClient(httpClient),
|
|
2725
|
-
attachment: new HttpAttachmentClient(httpClient)
|
|
3127
|
+
attachment: new HttpAttachmentClient(httpClient),
|
|
3128
|
+
artifact: new HttpArtifactClient(httpClient),
|
|
3129
|
+
streamToken: new HttpStreamTokenClient(httpClient)
|
|
2726
3130
|
};
|
|
2727
3131
|
}
|
|
3132
|
+
// ------------------------------------------------------- stream tokens
|
|
3133
|
+
//
|
|
3134
|
+
// A stream token is a presigned WebSocket URL for a project's event feed —
|
|
3135
|
+
// the URL is the whole credential, which is what lets a tool that takes one
|
|
3136
|
+
// string and nothing else consume the feed.
|
|
3137
|
+
async createStreamToken(request) {
|
|
3138
|
+
return this.withTokenRetry((jwt, c) => c.streamToken.createStreamToken(request, jwt));
|
|
3139
|
+
}
|
|
3140
|
+
async listStreamTokens(opts = {}) {
|
|
3141
|
+
return this.withTokenRetry((jwt, c) => c.streamToken.listStreamTokens(opts, jwt));
|
|
3142
|
+
}
|
|
3143
|
+
async revokeStreamToken(id) {
|
|
3144
|
+
return this.withTokenRetry((jwt, c) => c.streamToken.revokeStreamToken(id, jwt));
|
|
3145
|
+
}
|
|
3146
|
+
// ----------------------------------------------------------- artifacts
|
|
3147
|
+
//
|
|
3148
|
+
// Every write is attributed `actorKind: 'ai'`: an MCP client is an
|
|
3149
|
+
// assistant acting on the user's behalf, exactly as for spec files.
|
|
3150
|
+
async listArtifacts(projectId, opts = {}) {
|
|
3151
|
+
return this.withTokenRetry((jwt, c) => c.artifact.listArtifacts(projectId, jwt, opts));
|
|
3152
|
+
}
|
|
3153
|
+
async getArtifact(artifactId) {
|
|
3154
|
+
return this.withTokenRetry((jwt, c) => c.artifact.getArtifact(artifactId, jwt));
|
|
3155
|
+
}
|
|
3156
|
+
async getArtifactRevision(artifactId, revisionNumber) {
|
|
3157
|
+
return this.withTokenRetry((jwt, c) => c.artifact.getRevision(artifactId, revisionNumber, jwt));
|
|
3158
|
+
}
|
|
3159
|
+
async downloadArtifactFile(artifactId, path4, revision) {
|
|
3160
|
+
return this.withTokenRetry(
|
|
3161
|
+
(jwt, c) => c.artifact.downloadFile(artifactId, path4, jwt, revision === void 0 ? void 0 : { revision })
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
async createArtifact(projectId, request) {
|
|
3165
|
+
return this.withTokenRetry(
|
|
3166
|
+
(jwt, c) => c.artifact.createArtifact(projectId, request, jwt, { actorKind: "ai" })
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
async createArtifactRevision(artifactId, request, expectedVersion) {
|
|
3170
|
+
return this.withTokenRetry(
|
|
3171
|
+
(jwt, c) => c.artifact.createRevision(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
async rollbackArtifact(artifactId, request, expectedVersion) {
|
|
3175
|
+
return this.withTokenRetry(
|
|
3176
|
+
(jwt, c) => c.artifact.rollback(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
2728
3179
|
async withTokenRetry(call) {
|
|
2729
3180
|
const clients = await this.clients();
|
|
2730
3181
|
const jwt = await this.tokenManager.getValidAccessToken();
|
|
@@ -3479,6 +3930,9 @@ function sliceWindow(items, limit, offset) {
|
|
|
3479
3930
|
};
|
|
3480
3931
|
}
|
|
3481
3932
|
function nextOffsetOf(offset, returned, total) {
|
|
3933
|
+
if (returned === 0) {
|
|
3934
|
+
return void 0;
|
|
3935
|
+
}
|
|
3482
3936
|
const end = offset + returned;
|
|
3483
3937
|
return end < total ? end : void 0;
|
|
3484
3938
|
}
|
|
@@ -3556,6 +4010,8 @@ var listProjectsTool = {
|
|
|
3556
4010
|
// src/server/tools/list_spec_file.ts
|
|
3557
4011
|
import { z as z13 } from "zod";
|
|
3558
4012
|
var PAGE_SIZE = 100;
|
|
4013
|
+
var LIST_DEFAULT2 = 50;
|
|
4014
|
+
var LIST_MAX2 = 200;
|
|
3559
4015
|
var inputSchema13 = {
|
|
3560
4016
|
project_id: z13.string().min(1).describe("UUID of the project whose spec files to list."),
|
|
3561
4017
|
path: z13.string().optional().describe(
|
|
@@ -3563,26 +4019,57 @@ var inputSchema13 = {
|
|
|
3563
4019
|
),
|
|
3564
4020
|
trashed: z13.boolean().optional().describe(
|
|
3565
4021
|
"When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
|
|
3566
|
-
)
|
|
4022
|
+
),
|
|
4023
|
+
limit: z13.number().int().positive().optional().describe(
|
|
4024
|
+
`Max files to return (default ${String(LIST_DEFAULT2)}, capped at ${String(LIST_MAX2)}).`
|
|
4025
|
+
),
|
|
4026
|
+
offset: z13.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
3567
4027
|
};
|
|
3568
4028
|
var listSpecFileTool = {
|
|
3569
4029
|
name: "list_spec_file",
|
|
3570
|
-
description: 'List a project\'s spec files with id, path, and metadata (revision count, file size of the latest revision, file type, owning session, last update). Filter to a directory with `path` (exact file or everything under it; pass "", "/", or omit for all). Set `trashed=true` to list files in the Trash Bin instead of live files.',
|
|
4030
|
+
description: 'List a project\'s spec files with id, path, and metadata (revision count, file size of the latest revision, file type, owning session, last update). Filter to a directory with `path` (exact file or everything under it; pass "", "/", or omit for all). Set `trashed=true` to list files in the Trash Bin instead of live files. Ordered by `file_path` ascending whenever `path` or `trashed` is given, most-recently-created first otherwise. Returns `total` and, when more records remain, `next_offset`.',
|
|
3571
4031
|
inputSchema: inputSchema13,
|
|
3572
4032
|
handler: async (args, ctx) => {
|
|
3573
4033
|
const trashed = args.trashed ?? false;
|
|
3574
|
-
const
|
|
4034
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT2, LIST_MAX2);
|
|
4035
|
+
const offset = args.offset ?? 0;
|
|
3575
4036
|
const prefix = normalizePrefix(args.path);
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
files
|
|
3583
|
-
|
|
4037
|
+
if (trashed) {
|
|
4038
|
+
const all2 = await ctx.client.listTrashedFiles(args.project_id);
|
|
4039
|
+
return windowedListResult(args.project_id, prefix, true, all2, limit, offset);
|
|
4040
|
+
}
|
|
4041
|
+
if (prefix === "") {
|
|
4042
|
+
const page = await ctx.client.listFiles(args.project_id, { limit, offset });
|
|
4043
|
+
const next = nextOffsetOf(offset, page.files.length, page.total);
|
|
4044
|
+
return jsonResult({
|
|
4045
|
+
project_id: args.project_id,
|
|
4046
|
+
path: prefix,
|
|
4047
|
+
trashed: false,
|
|
4048
|
+
files: page.files.map(toEntry),
|
|
4049
|
+
total: page.total,
|
|
4050
|
+
limit,
|
|
4051
|
+
offset,
|
|
4052
|
+
...next === void 0 ? {} : { next_offset: next }
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
const all = await listAllLiveFiles(ctx.client, args.project_id);
|
|
4056
|
+
return windowedListResult(args.project_id, prefix, false, all, limit, offset);
|
|
3584
4057
|
}
|
|
3585
4058
|
};
|
|
4059
|
+
function windowedListResult(projectId, prefix, trashed, files, limit, offset) {
|
|
4060
|
+
const matches = files.filter((f) => isUnderPath(f.filePath, prefix)).map(toEntry).sort((a, b) => a.file_path.localeCompare(b.file_path));
|
|
4061
|
+
const window = sliceWindow(matches, limit, offset);
|
|
4062
|
+
return jsonResult({
|
|
4063
|
+
project_id: projectId,
|
|
4064
|
+
path: prefix,
|
|
4065
|
+
trashed,
|
|
4066
|
+
files: window.items,
|
|
4067
|
+
total: window.total,
|
|
4068
|
+
limit: window.limit,
|
|
4069
|
+
offset,
|
|
4070
|
+
...window.next_offset === void 0 ? {} : { next_offset: window.next_offset }
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
3586
4073
|
async function listAllLiveFiles(client, projectId) {
|
|
3587
4074
|
const all = [];
|
|
3588
4075
|
let offset = 0;
|
|
@@ -3626,24 +4113,24 @@ function isUnderPath(filePath, prefix) {
|
|
|
3626
4113
|
|
|
3627
4114
|
// src/server/tools/list_spec_sessions.ts
|
|
3628
4115
|
import { z as z14 } from "zod";
|
|
3629
|
-
var
|
|
3630
|
-
var
|
|
4116
|
+
var LIST_DEFAULT3 = 10;
|
|
4117
|
+
var LIST_MAX3 = 20;
|
|
3631
4118
|
var inputSchema14 = {
|
|
3632
4119
|
project_id: z14.string().min(1).describe("UUID of the project whose spec sessions to list."),
|
|
3633
4120
|
archive_filter: z14.enum(["active", "archived", "all"]).optional().describe("Which sessions to list: 'active' (default), 'archived', or 'all'."),
|
|
3634
4121
|
status: z14.enum(["generating", "pending_input", "completed"]).optional().describe("Filter by session status."),
|
|
3635
4122
|
query: z14.string().optional().describe("Search term matched against the session summary."),
|
|
3636
4123
|
limit: z14.number().int().positive().optional().describe(
|
|
3637
|
-
`Max sessions to return (default ${String(
|
|
4124
|
+
`Max sessions to return (default ${String(LIST_DEFAULT3)}, capped at ${String(LIST_MAX3)}).`
|
|
3638
4125
|
),
|
|
3639
4126
|
offset: z14.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
3640
4127
|
};
|
|
3641
4128
|
var listSpecSessionsTool = {
|
|
3642
4129
|
name: "list_spec_sessions",
|
|
3643
|
-
description: `List a project's spec sessions (metadata plus session_summary and context_size_bytes \u2014 never the context itself or the chat transcript). Filter with archive_filter ('active'/'archived'/'all'), status, and query (searches the session summary). Capped at ${String(
|
|
4130
|
+
description: `List a project's spec sessions (metadata plus session_summary and context_size_bytes \u2014 never the context itself or the chat transcript). Filter with archive_filter ('active'/'archived'/'all'), status, and query (searches the session summary). Capped at ${String(LIST_MAX3)} per page; returns \`total\` and \`next_offset\`.`,
|
|
3644
4131
|
inputSchema: inputSchema14,
|
|
3645
4132
|
handler: async (args, ctx) => {
|
|
3646
|
-
const limit = clampLimit(args.limit,
|
|
4133
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT3, LIST_MAX3);
|
|
3647
4134
|
const offset = args.offset ?? 0;
|
|
3648
4135
|
const result = await ctx.client.listProjectSessions(args.project_id, {
|
|
3649
4136
|
archiveFilter: args.archive_filter ?? "active",
|
|
@@ -3694,6 +4181,9 @@ var MAX_LINES_PER_CALL = 2e3;
|
|
|
3694
4181
|
var MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
|
|
3695
4182
|
function normalizeNewlines(text) {
|
|
3696
4183
|
const withoutBom = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
4184
|
+
if (!withoutBom.includes("\r")) {
|
|
4185
|
+
return withoutBom;
|
|
4186
|
+
}
|
|
3697
4187
|
return withoutBom.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3698
4188
|
}
|
|
3699
4189
|
function splitIntoLines(text) {
|
|
@@ -3864,20 +4354,27 @@ var readAttachmentTool = {
|
|
|
3864
4354
|
import { promises as fs3 } from "fs";
|
|
3865
4355
|
import { createHash as createHash2 } from "crypto";
|
|
3866
4356
|
import { z as z17 } from "zod";
|
|
3867
|
-
var
|
|
4357
|
+
var MAX_READABLE_SPEC_FILE_BYTES = 8 * 1024 * 1024;
|
|
3868
4358
|
var inputSchema17 = {
|
|
3869
4359
|
file_id: z17.string().min(1).describe("UUID of the file"),
|
|
3870
|
-
revision: z17.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
|
|
4360
|
+
revision: z17.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision)."),
|
|
4361
|
+
offset: z17.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
|
|
4362
|
+
limit: z17.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
|
|
3871
4363
|
};
|
|
3872
4364
|
var readSpecFileTool = {
|
|
3873
4365
|
name: "read_spec_file",
|
|
3874
|
-
description: "Return a spec file's UTF-8 content. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision
|
|
4366
|
+
description: "Return a window of a spec file's UTF-8 content, paginated by line (offset/limit, default and cap 2000 lines, 1 MiB per response). Large files are read across multiple calls using `next_offset`. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision, so paging through a file downloads it once. Files above 8 MiB and binary content are refused \u2014 use download_spec_file to save those to a local file instead.",
|
|
3875
4367
|
inputSchema: inputSchema17,
|
|
3876
4368
|
handler: async (args, ctx) => {
|
|
3877
4369
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
3878
4370
|
const targetRevision = args.revision ?? meta.revisionCount;
|
|
3879
4371
|
const isLatestRevision = targetRevision === meta.revisionCount;
|
|
3880
|
-
if (
|
|
4372
|
+
if (args.revision !== void 0 && args.revision > meta.revisionCount) {
|
|
4373
|
+
return errorResult(
|
|
4374
|
+
`revision=${String(args.revision)} is greater than revision_count=${String(meta.revisionCount)} for file ${args.file_id}.`
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
if (isLatestRevision && meta.fileSizeBytes > MAX_READABLE_SPEC_FILE_BYTES) {
|
|
3881
4378
|
return errorResult(buildOversizeMessage2(args.file_id, meta.fileSizeBytes));
|
|
3882
4379
|
}
|
|
3883
4380
|
const cacheRoot = resolveDownloadRoot();
|
|
@@ -3894,20 +4391,42 @@ var readSpecFileTool = {
|
|
|
3894
4391
|
`read_spec_file refused to write cache: ${escape}. This indicates an unexpected platform response shape (projectId/fileId/filePath).`
|
|
3895
4392
|
);
|
|
3896
4393
|
}
|
|
3897
|
-
const
|
|
4394
|
+
const cachedSize = await fileSizeOrNull(cachePath);
|
|
4395
|
+
if (cachedSize !== null && cachedSize > MAX_READABLE_SPEC_FILE_BYTES) {
|
|
4396
|
+
return errorResult(buildOversizeMessage2(args.file_id, cachedSize));
|
|
4397
|
+
}
|
|
4398
|
+
const cached = cachedSize === null ? null : await fs3.readFile(cachePath);
|
|
3898
4399
|
const cacheHit = cached !== null && (!isLatestRevision || sha256Prefixed2(cached) === meta.checksum);
|
|
3899
4400
|
let bytes;
|
|
4401
|
+
let effectiveRevision = targetRevision;
|
|
4402
|
+
let effectiveCachePath = cachePath;
|
|
3900
4403
|
if (cacheHit) {
|
|
3901
4404
|
bytes = cached;
|
|
3902
4405
|
} else {
|
|
3903
4406
|
const downloaded = await ctx.client.downloadFile(args.file_id, args.revision);
|
|
3904
4407
|
bytes = downloaded.bytes;
|
|
3905
|
-
if (bytes.byteLength >
|
|
4408
|
+
if (bytes.byteLength > MAX_READABLE_SPEC_FILE_BYTES) {
|
|
3906
4409
|
return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
|
|
3907
4410
|
}
|
|
3908
|
-
|
|
4411
|
+
if (downloaded.revisionNumber !== targetRevision) {
|
|
4412
|
+
effectiveRevision = downloaded.revisionNumber;
|
|
4413
|
+
effectiveCachePath = computeFileCachePath({
|
|
4414
|
+
cacheRoot,
|
|
4415
|
+
projectId: meta.projectId,
|
|
4416
|
+
fileId: meta.id,
|
|
4417
|
+
revision: effectiveRevision,
|
|
4418
|
+
filePath: meta.filePath
|
|
4419
|
+
});
|
|
4420
|
+
const raceEscape = assertWithinRoot(effectiveCachePath, cacheRoot);
|
|
4421
|
+
if (raceEscape) {
|
|
4422
|
+
return errorResult(
|
|
4423
|
+
`read_spec_file refused to write cache: ${raceEscape}. This indicates an unexpected platform response shape (projectId/fileId/filePath).`
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
await writeBytesAtomically(effectiveCachePath, bytes);
|
|
3909
4428
|
}
|
|
3910
|
-
if (bytes.byteLength >
|
|
4429
|
+
if (bytes.byteLength > MAX_READABLE_SPEC_FILE_BYTES) {
|
|
3911
4430
|
return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
|
|
3912
4431
|
}
|
|
3913
4432
|
if (isBinary2(bytes)) {
|
|
@@ -3923,25 +4442,44 @@ var readSpecFileTool = {
|
|
|
3923
4442
|
`File ${args.file_id} is not valid UTF-8 text. Use download_spec_file to save it to a local file.`
|
|
3924
4443
|
);
|
|
3925
4444
|
}
|
|
4445
|
+
const paginated = paginateText(text, args.offset, args.limit);
|
|
4446
|
+
if (!paginated.ok) {
|
|
4447
|
+
return errorResult(paginated.error);
|
|
4448
|
+
}
|
|
3926
4449
|
return jsonResult({
|
|
3927
4450
|
file_id: meta.id,
|
|
3928
|
-
revision_number:
|
|
4451
|
+
revision_number: effectiveRevision,
|
|
3929
4452
|
// Pass this back as update_spec_file's expected_version so an edit made by
|
|
3930
4453
|
// someone else since this read is reported instead of overwritten. It
|
|
3931
4454
|
// describes the file's CURRENT content, so it is emitted only when that is
|
|
3932
4455
|
// what was read — beside a historical revision it would invite an update
|
|
3933
4456
|
// that overwrites everything written since.
|
|
3934
|
-
...
|
|
4457
|
+
// ...and only when it describes THESE bytes. If a concurrent save moved
|
|
4458
|
+
// the revision under us, meta.contentVersion belongs to the older one:
|
|
4459
|
+
// a stale token is fail-safe (the platform answers 409) but it would
|
|
4460
|
+
// cost the model a round trip it cannot diagnose, so omit it and let the
|
|
4461
|
+
// re-read supply a current one.
|
|
4462
|
+
...isLatestRevision && effectiveRevision === targetRevision && meta.contentVersion !== void 0 ? { content_version: meta.contentVersion } : {},
|
|
3935
4463
|
file_path: meta.filePath,
|
|
3936
4464
|
file_type: meta.fileType,
|
|
3937
|
-
cache_path: maskHomedir(
|
|
4465
|
+
cache_path: maskHomedir(effectiveCachePath),
|
|
3938
4466
|
cache_hit: cacheHit,
|
|
3939
|
-
content
|
|
4467
|
+
// `content` plus the window fields (start_line/end_line/read_lines/
|
|
4468
|
+
// total_lines/truncated/next_offset) — the same shape read_attachment
|
|
4469
|
+
// and mcp-server-http's read_spec_file return.
|
|
4470
|
+
...paginated.window
|
|
3940
4471
|
});
|
|
3941
4472
|
}
|
|
3942
4473
|
};
|
|
3943
4474
|
function buildOversizeMessage2(fileId, byteLength) {
|
|
3944
|
-
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(
|
|
4475
|
+
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READABLE_SPEC_FILE_BYTES)} bytes. Use download_spec_file to save it to a local file and read it in chunks.`;
|
|
4476
|
+
}
|
|
4477
|
+
async function fileSizeOrNull(filePath) {
|
|
4478
|
+
try {
|
|
4479
|
+
return (await fs3.stat(filePath)).size;
|
|
4480
|
+
} catch {
|
|
4481
|
+
return null;
|
|
4482
|
+
}
|
|
3945
4483
|
}
|
|
3946
4484
|
function isBinary2(bytes) {
|
|
3947
4485
|
for (const byte of bytes) {
|
|
@@ -4571,6 +5109,434 @@ var uploadSpecFileTool = {
|
|
|
4571
5109
|
}
|
|
4572
5110
|
};
|
|
4573
5111
|
|
|
5112
|
+
// src/server/tools/list_artifacts.ts
|
|
5113
|
+
import { z as z27 } from "zod";
|
|
5114
|
+
|
|
5115
|
+
// src/server/tools/artifact-output.ts
|
|
5116
|
+
import { z as z26 } from "zod";
|
|
5117
|
+
var artifactFilesSchema = z26.array(
|
|
5118
|
+
z26.object({
|
|
5119
|
+
path: z26.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
|
|
5120
|
+
content: z26.string().describe("The file content, in `encoding` (default utf8)."),
|
|
5121
|
+
encoding: z26.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
|
|
5122
|
+
content_type: z26.string().optional().describe("Hint used only for extensions the platform does not recognise.")
|
|
5123
|
+
})
|
|
5124
|
+
).describe("Files of the bundle.");
|
|
5125
|
+
function artifactSummary(a) {
|
|
5126
|
+
return {
|
|
5127
|
+
id: a.id,
|
|
5128
|
+
project_id: a.projectId,
|
|
5129
|
+
name: a.name,
|
|
5130
|
+
slug: a.slug,
|
|
5131
|
+
kind: a.kind,
|
|
5132
|
+
description: a.description,
|
|
5133
|
+
entry_path: a.entryPath,
|
|
5134
|
+
revision_count: a.revisionCount,
|
|
5135
|
+
session_id: a.sessionId,
|
|
5136
|
+
created_by: a.createdBy,
|
|
5137
|
+
created_at: a.createdAt,
|
|
5138
|
+
updated_at: a.updatedAt,
|
|
5139
|
+
...a.trashedAt ? { trashed_at: a.trashedAt } : {}
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
function fileEntry(f) {
|
|
5143
|
+
return { path: f.path, content_type: f.contentType, size_bytes: f.sizeBytes, checksum: f.checksum };
|
|
5144
|
+
}
|
|
5145
|
+
function revisionSummary(r) {
|
|
5146
|
+
return {
|
|
5147
|
+
id: r.id,
|
|
5148
|
+
revision_number: r.revisionNumber,
|
|
5149
|
+
source: r.source,
|
|
5150
|
+
...r.restoredFromRevision === void 0 ? {} : { restored_from_revision: r.restoredFromRevision },
|
|
5151
|
+
message: r.message,
|
|
5152
|
+
changed_by: r.changedBy,
|
|
5153
|
+
changed_by_kind: r.changedByKind,
|
|
5154
|
+
file_count: r.fileCount,
|
|
5155
|
+
total_bytes: r.totalBytes,
|
|
5156
|
+
manifest_checksum: r.manifestChecksum,
|
|
5157
|
+
created_at: r.createdAt,
|
|
5158
|
+
..."files" in r ? { files: r.files.map(fileEntry) } : {}
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
function writeResultOutput(result) {
|
|
5162
|
+
return {
|
|
5163
|
+
artifact: artifactSummary(result.artifact),
|
|
5164
|
+
revision: revisionSummary(result.revision),
|
|
5165
|
+
no_change: result.noChange
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
function isArtifactWriteConflict(conflict) {
|
|
5169
|
+
return conflict.reason === "revision_mismatch" || conflict.reason === "blob_reclaimed" || conflict.reason === "lock_timeout" || conflict.reason === "unknown";
|
|
5170
|
+
}
|
|
5171
|
+
function artifactConflictMessage(artifactId, conflict) {
|
|
5172
|
+
if (conflict.reason === "lock_timeout") {
|
|
5173
|
+
return `Artifact ${artifactId} was NOT written: another write or a storage collection held its lock and this call timed out waiting. Nothing changed \u2014 retry the same call as-is.`;
|
|
5174
|
+
}
|
|
5175
|
+
if (conflict.reason === "blob_reclaimed") {
|
|
5176
|
+
return `Artifact ${artifactId} was NOT written: a file this write relied on from an older revision is no longer stored (that revision was pruned). Call get_artifact to read the current head, then resend the full content of every file (a full snapshot, or a patch based on the head).`;
|
|
5177
|
+
}
|
|
5178
|
+
const head = conflict.currentVersion === void 0 ? "" : ` The head is now revision ${String(conflict.currentVersion)}.`;
|
|
5179
|
+
return `Artifact ${artifactId} was NOT written: it changed since the revision this write was based on.${head} Call get_artifact to read the current head, reapply your change to it, and retry with expected_version (or base_revision) set to that head.`;
|
|
5180
|
+
}
|
|
5181
|
+
function artifactCreateConflictMessage(conflict) {
|
|
5182
|
+
if (conflict.reason === "slug_taken") {
|
|
5183
|
+
return "Artifact was NOT created: the slug is already taken in this project (trashed artifacts keep their slug). Pick another slug, or call list_artifacts to find the existing artifact and write a new revision to it instead.";
|
|
5184
|
+
}
|
|
5185
|
+
if (conflict.reason === "lock_timeout") {
|
|
5186
|
+
return "Artifact was NOT created: the platform timed out waiting for a lock. Nothing changed \u2014 retry the same call as-is.";
|
|
5187
|
+
}
|
|
5188
|
+
return `Artifact was NOT created: the platform reported a conflict (${conflict.reason}). Re-read the project's artifacts with list_artifacts before retrying.`;
|
|
5189
|
+
}
|
|
5190
|
+
|
|
5191
|
+
// src/server/tools/list_artifacts.ts
|
|
5192
|
+
var LIST_DEFAULT4 = 50;
|
|
5193
|
+
var LIST_MAX4 = 100;
|
|
5194
|
+
var inputSchema26 = {
|
|
5195
|
+
project_id: z27.string().min(1).describe("UUID of the project whose artifacts to list."),
|
|
5196
|
+
limit: z27.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT4)}, capped at ${String(LIST_MAX4)}).`),
|
|
5197
|
+
offset: z27.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
|
|
5198
|
+
};
|
|
5199
|
+
var listArtifactsTool = {
|
|
5200
|
+
name: "list_artifacts",
|
|
5201
|
+
description: "List a project's live artifacts (multi-file bundles such as HTML mockups, each versioned as one snapshot) with id, slug, entry path and revision_count. Returns `total` and, when more remain, `next_offset`. Use get_artifact for the file manifest and read_artifact_file for content.",
|
|
5202
|
+
inputSchema: inputSchema26,
|
|
5203
|
+
handler: async (args, ctx) => {
|
|
5204
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT4, LIST_MAX4);
|
|
5205
|
+
const offset = args.offset ?? 0;
|
|
5206
|
+
const page = await ctx.client.listArtifacts(args.project_id, { limit, offset });
|
|
5207
|
+
const next = nextOffsetOf(offset, page.items.length, page.total);
|
|
5208
|
+
return jsonResult({
|
|
5209
|
+
project_id: args.project_id,
|
|
5210
|
+
artifacts: page.items.map(artifactSummary),
|
|
5211
|
+
total: page.total,
|
|
5212
|
+
limit,
|
|
5213
|
+
offset,
|
|
5214
|
+
...next === void 0 ? {} : { next_offset: next }
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
};
|
|
5218
|
+
|
|
5219
|
+
// src/server/tools/get_artifact.ts
|
|
5220
|
+
import { z as z28 } from "zod";
|
|
5221
|
+
var inputSchema27 = {
|
|
5222
|
+
artifact_id: z28.string().min(1).describe("UUID of the artifact."),
|
|
5223
|
+
revision: z28.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
|
|
5224
|
+
};
|
|
5225
|
+
var getArtifactTool = {
|
|
5226
|
+
name: "get_artifact",
|
|
5227
|
+
description: "Get an artifact's metadata plus the file manifest (path, content type, size, checksum of every file) of its head revision, or of `revision` when given. `revision_count` is the head number AND the value to pass as expected_version when writing; it only ever grows because rollback appends a revision rather than discarding history.",
|
|
5228
|
+
inputSchema: inputSchema27,
|
|
5229
|
+
handler: async (args, ctx) => {
|
|
5230
|
+
const detail = await ctx.client.getArtifact(args.artifact_id);
|
|
5231
|
+
if (args.revision === void 0) {
|
|
5232
|
+
return jsonResult({ artifact: artifactSummary(detail.artifact), revision: revisionSummary(detail.head) });
|
|
5233
|
+
}
|
|
5234
|
+
if (args.revision > detail.artifact.revisionCount) {
|
|
5235
|
+
return errorResult(
|
|
5236
|
+
`revision=${String(args.revision)} is greater than revision_count=${String(detail.artifact.revisionCount)} for artifact ${args.artifact_id}.`
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
const revision = await ctx.client.getArtifactRevision(args.artifact_id, args.revision);
|
|
5240
|
+
return jsonResult({ artifact: artifactSummary(detail.artifact), revision: revisionSummary(revision) });
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
|
|
5244
|
+
// src/server/tools/read_artifact_file.ts
|
|
5245
|
+
import { z as z29 } from "zod";
|
|
5246
|
+
var MAX_READABLE_ARTIFACT_FILE_BYTES = 8 * 1024 * 1024;
|
|
5247
|
+
var inputSchema28 = {
|
|
5248
|
+
artifact_id: z29.string().min(1).describe("UUID of the artifact."),
|
|
5249
|
+
path: z29.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
|
|
5250
|
+
revision: z29.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
|
|
5251
|
+
offset: z29.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
|
|
5252
|
+
limit: z29.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
|
|
5253
|
+
};
|
|
5254
|
+
var readArtifactFileTool = {
|
|
5255
|
+
name: "read_artifact_file",
|
|
5256
|
+
description: "Return one artifact file's UTF-8 content, paginated by line (offset/limit, default and cap 2000 lines, 1 MiB per response) with `total_lines`, `truncated` and `next_offset`. Reads the head revision unless `revision` is given. Binary files (images, fonts) cannot be read through this tool; get_artifact lists their size and checksum.",
|
|
5257
|
+
inputSchema: inputSchema28,
|
|
5258
|
+
handler: async (args, ctx) => {
|
|
5259
|
+
const file = await ctx.client.downloadArtifactFile(args.artifact_id, args.path, args.revision);
|
|
5260
|
+
if (file.content.byteLength > MAX_READABLE_ARTIFACT_FILE_BYTES) {
|
|
5261
|
+
return errorResult(
|
|
5262
|
+
`File ${args.path} of artifact ${args.artifact_id} is ${String(file.content.byteLength)} bytes, above the ${String(MAX_READABLE_ARTIFACT_FILE_BYTES)}-byte read ceiling.`
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
if (isBinary(file.content)) {
|
|
5266
|
+
return errorResult(
|
|
5267
|
+
`File ${args.path} of artifact ${args.artifact_id} appears to be binary (content_type=${file.contentType}, size=${String(file.content.byteLength)} bytes) and cannot be read through this tool.`
|
|
5268
|
+
);
|
|
5269
|
+
}
|
|
5270
|
+
const text = decodeUtf8Strict(file.content);
|
|
5271
|
+
if (text === null) {
|
|
5272
|
+
return errorResult(
|
|
5273
|
+
`File ${args.path} of artifact ${args.artifact_id} is not valid UTF-8 text (content_type=${file.contentType}).`
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
const paginated = paginateText(text, args.offset, args.limit);
|
|
5277
|
+
if (!paginated.ok) {
|
|
5278
|
+
return errorResult(paginated.error);
|
|
5279
|
+
}
|
|
5280
|
+
return jsonResult({
|
|
5281
|
+
artifact_id: args.artifact_id,
|
|
5282
|
+
path: args.path,
|
|
5283
|
+
...args.revision === void 0 ? {} : { revision_number: args.revision },
|
|
5284
|
+
content_type: file.contentType,
|
|
5285
|
+
checksum: file.checksum,
|
|
5286
|
+
...paginated.window
|
|
5287
|
+
});
|
|
5288
|
+
}
|
|
5289
|
+
};
|
|
5290
|
+
|
|
5291
|
+
// src/server/tools/create_artifact.ts
|
|
5292
|
+
import { z as z30 } from "zod";
|
|
5293
|
+
var inputSchema29 = {
|
|
5294
|
+
project_id: z30.string().min(1).describe("UUID of the project to create the artifact in."),
|
|
5295
|
+
name: z30.string().min(1).describe("Display name (max 200 characters)."),
|
|
5296
|
+
slug: z30.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
|
|
5297
|
+
description: z30.string().optional().describe("Optional description (max 2000 characters)."),
|
|
5298
|
+
entry_path: z30.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
|
|
5299
|
+
message: z30.string().optional().describe("Short summary of this first revision (max 500 characters)."),
|
|
5300
|
+
files: artifactFilesSchema
|
|
5301
|
+
};
|
|
5302
|
+
var createArtifactTool = {
|
|
5303
|
+
name: "create_artifact",
|
|
5304
|
+
description: "Create an artifact \u2014 a bundle of files (e.g. an HTML mockup: index.html + styles.css + app.js) the platform versions AS ONE snapshot \u2014 with the given files as revision 1. At most 50 files, 2 MiB per file, 10 MiB per revision. Fails with a conflict when the slug is already taken in the project (trashed artifacts still own their slug).",
|
|
5305
|
+
inputSchema: inputSchema29,
|
|
5306
|
+
handler: async (args, ctx) => {
|
|
5307
|
+
try {
|
|
5308
|
+
const result = await ctx.client.createArtifact(args.project_id, {
|
|
5309
|
+
name: args.name,
|
|
5310
|
+
slug: args.slug,
|
|
5311
|
+
description: args.description,
|
|
5312
|
+
entryPath: args.entry_path,
|
|
5313
|
+
message: args.message,
|
|
5314
|
+
files: args.files.map((f) => ({
|
|
5315
|
+
path: f.path,
|
|
5316
|
+
content: f.content,
|
|
5317
|
+
encoding: f.encoding,
|
|
5318
|
+
contentType: f.content_type
|
|
5319
|
+
}))
|
|
5320
|
+
});
|
|
5321
|
+
return jsonResult(writeResultOutput(result));
|
|
5322
|
+
} catch (err) {
|
|
5323
|
+
const conflict = conflictOf(err);
|
|
5324
|
+
if (conflict) {
|
|
5325
|
+
return errorResult(artifactCreateConflictMessage(conflict));
|
|
5326
|
+
}
|
|
5327
|
+
throw err;
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
|
|
5332
|
+
// src/server/tools/write_artifact_revision.ts
|
|
5333
|
+
import { z as z31 } from "zod";
|
|
5334
|
+
var inputSchema30 = {
|
|
5335
|
+
artifact_id: z31.string().min(1).describe("UUID of the artifact."),
|
|
5336
|
+
files: artifactFilesSchema,
|
|
5337
|
+
base_revision: z31.number().int().positive().optional().describe(
|
|
5338
|
+
"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."
|
|
5339
|
+
),
|
|
5340
|
+
remove: z31.array(z31.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
|
|
5341
|
+
expected_version: z31.number().int().nonnegative().optional().describe(
|
|
5342
|
+
"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."
|
|
5343
|
+
),
|
|
5344
|
+
message: z31.string().optional().describe("Short summary of the change (max 500 characters)."),
|
|
5345
|
+
entry_path: z31.string().optional().describe("Change the file a renderer opens first, from this revision on.")
|
|
5346
|
+
};
|
|
5347
|
+
var writeArtifactRevisionTool = {
|
|
5348
|
+
name: "write_artifact_revision",
|
|
5349
|
+
description: "Write a new revision of an artifact. FULL mode (no base_revision): `files` becomes the whole bundle. PATCH mode (base_revision set): only the changed files are sent, plus `remove` for paths to drop \u2014 the platform stores the complete result either way. Every revision is immutable; the newest N are retained (50 by default, set by the platform; older ones are pruned); use rollback_artifact to restore an earlier one. Read the current head with get_artifact first and pass its revision_count as expected_version.",
|
|
5350
|
+
inputSchema: inputSchema30,
|
|
5351
|
+
handler: async (args, ctx) => {
|
|
5352
|
+
if (args.remove && args.remove.length > 0 && args.base_revision === void 0) {
|
|
5353
|
+
return errorResult("`remove` is only valid in patch mode: set base_revision to the revision you are editing.");
|
|
5354
|
+
}
|
|
5355
|
+
try {
|
|
5356
|
+
const result = await ctx.client.createArtifactRevision(
|
|
5357
|
+
args.artifact_id,
|
|
5358
|
+
{
|
|
5359
|
+
files: args.files.map((f) => ({
|
|
5360
|
+
path: f.path,
|
|
5361
|
+
content: f.content,
|
|
5362
|
+
encoding: f.encoding,
|
|
5363
|
+
contentType: f.content_type
|
|
5364
|
+
})),
|
|
5365
|
+
remove: args.remove,
|
|
5366
|
+
baseRevision: args.base_revision,
|
|
5367
|
+
message: args.message,
|
|
5368
|
+
entryPath: args.entry_path
|
|
5369
|
+
},
|
|
5370
|
+
args.expected_version
|
|
5371
|
+
);
|
|
5372
|
+
return jsonResult(writeResultOutput(result));
|
|
5373
|
+
} catch (err) {
|
|
5374
|
+
const conflict = conflictOf(err);
|
|
5375
|
+
if (!conflict || !isArtifactWriteConflict(conflict)) {
|
|
5376
|
+
throw err;
|
|
5377
|
+
}
|
|
5378
|
+
return errorResult(artifactConflictMessage(args.artifact_id, conflict));
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
};
|
|
5382
|
+
|
|
5383
|
+
// src/server/tools/rollback_artifact.ts
|
|
5384
|
+
import { z as z32 } from "zod";
|
|
5385
|
+
var inputSchema31 = {
|
|
5386
|
+
artifact_id: z32.string().min(1).describe("UUID of the artifact."),
|
|
5387
|
+
revision_number: z32.number().int().positive().describe("The revision whose files to restore."),
|
|
5388
|
+
expected_version: z32.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
|
|
5389
|
+
message: z32.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
|
|
5390
|
+
};
|
|
5391
|
+
var rollbackArtifactTool = {
|
|
5392
|
+
name: "rollback_artifact",
|
|
5393
|
+
description: "Restore an earlier revision of an artifact. Like `git revert`, this APPENDS a new revision whose files equal the chosen one \u2014 history is never rewritten, and the new head is revision_count + 1. Only the newest N revisions are retained (50 by default, set by the platform): older ones are pruned, and rolling back to a pruned revision fails with not found \u2014 list the revisions (get_artifact) first when in doubt. Rolling back to the head itself is allowed and recorded with no_change=true.",
|
|
5394
|
+
inputSchema: inputSchema31,
|
|
5395
|
+
handler: async (args, ctx) => {
|
|
5396
|
+
try {
|
|
5397
|
+
const result = await ctx.client.rollbackArtifact(
|
|
5398
|
+
args.artifact_id,
|
|
5399
|
+
{ revisionNumber: args.revision_number, message: args.message },
|
|
5400
|
+
args.expected_version
|
|
5401
|
+
);
|
|
5402
|
+
return jsonResult(writeResultOutput(result));
|
|
5403
|
+
} catch (err) {
|
|
5404
|
+
const conflict = conflictOf(err);
|
|
5405
|
+
if (!conflict || !isArtifactWriteConflict(conflict)) {
|
|
5406
|
+
throw err;
|
|
5407
|
+
}
|
|
5408
|
+
return errorResult(artifactConflictMessage(args.artifact_id, conflict));
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
};
|
|
5412
|
+
|
|
5413
|
+
// src/server/tools/create_stream_token.ts
|
|
5414
|
+
import { z as z34 } from "zod";
|
|
5415
|
+
|
|
5416
|
+
// src/server/tools/stream-token-output.ts
|
|
5417
|
+
import { z as z33 } from "zod";
|
|
5418
|
+
var projectStreamEventTypes = [
|
|
5419
|
+
"project.updated",
|
|
5420
|
+
"project.deleted",
|
|
5421
|
+
"spec_file.created",
|
|
5422
|
+
"spec_file.updated",
|
|
5423
|
+
"spec_file.deleted",
|
|
5424
|
+
"spec_file.lock",
|
|
5425
|
+
"attachment.created",
|
|
5426
|
+
"attachment.deleted",
|
|
5427
|
+
"spec_session.created",
|
|
5428
|
+
"spec_session.updated",
|
|
5429
|
+
"spec_session.deleted",
|
|
5430
|
+
"*"
|
|
5431
|
+
];
|
|
5432
|
+
var streamEventTypesSchema = z33.array(z33.enum(projectStreamEventTypes)).min(1).describe(
|
|
5433
|
+
'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.'
|
|
5434
|
+
);
|
|
5435
|
+
function streamTokenSummary(token) {
|
|
5436
|
+
return {
|
|
5437
|
+
id: token.id,
|
|
5438
|
+
token_prefix: token.tokenPrefix,
|
|
5439
|
+
label: token.label,
|
|
5440
|
+
resource_type: token.resourceType,
|
|
5441
|
+
resource_id: token.resourceId,
|
|
5442
|
+
event_types: token.eventTypes,
|
|
5443
|
+
created_by: token.createdBy,
|
|
5444
|
+
created_at: token.createdAt.toISOString(),
|
|
5445
|
+
expires_at: token.expiresAt.toISOString(),
|
|
5446
|
+
live: token.live,
|
|
5447
|
+
connect_count: token.connectCount,
|
|
5448
|
+
...token.revokedAt ? { revoked_at: token.revokedAt.toISOString() } : {},
|
|
5449
|
+
...token.resourceDeletedAt ? { resource_deleted_at: token.resourceDeletedAt.toISOString() } : {},
|
|
5450
|
+
...token.lastConnectedAt ? { last_connected_at: token.lastConnectedAt.toISOString() } : {}
|
|
5451
|
+
};
|
|
5452
|
+
}
|
|
5453
|
+
function streamTokenMintOutput(result) {
|
|
5454
|
+
return {
|
|
5455
|
+
...streamTokenSummary(result),
|
|
5456
|
+
url: result.url,
|
|
5457
|
+
note: "Treat this URL as a password: anyone who has it can read this project's events until it expires, with no further authentication. Pass it straight to a WebSocket client and do NOT write it to a file, a commit, an issue or a chat reply, and do not log or repeat it. It is shown once and cannot be retrieved again. If it may have been exposed, call revoke_stream_token with the id above. Open it with subprotocol 'v1'; each text frame is one JSON event, and reconnecting with ?after=<seq> resumes from the last frame you saw."
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
// src/server/tools/create_stream_token.ts
|
|
5462
|
+
var inputSchema32 = {
|
|
5463
|
+
resource_type: z34.enum(["project"]).default("project").describe("What to follow. Only 'project' is supported today."),
|
|
5464
|
+
resource_id: z34.string().min(1).describe("UUID of the project to stream events for."),
|
|
5465
|
+
event_types: streamEventTypesSchema,
|
|
5466
|
+
ttl_seconds: z34.number().int().positive().optional().describe(
|
|
5467
|
+
"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."
|
|
5468
|
+
),
|
|
5469
|
+
label: z34.string().optional().describe("Short human-readable label, so a person can recognise this token in a listing.")
|
|
5470
|
+
};
|
|
5471
|
+
var createStreamTokenTool = {
|
|
5472
|
+
name: "create_stream_token",
|
|
5473
|
+
description: "Create a short-lived WebSocket URL that streams a project's events in real time \u2014 like an S3 presigned URL, the returned URL is the whole credential and needs no auth header. Choose the project and the event types to follow; the result carries the full wss:// URL. Feed it to any WebSocket client with subprotocol 'v1' (for example Claude Code's Monitor tool). Each text frame is one JSON event carrying a `seq` cursor; reconnect with ?after=<seq> to resume without missing events. Defaults to 24 hours, 7 days maximum. TREAT THE RETURNED URL LIKE A PASSWORD: anyone holding it can read every event on this project until it expires, without authenticating. Pass it directly to the WebSocket client; never write it to a file, a commit, an issue or a chat reply, and never log or repeat it. It is shown once and cannot be retrieved again. If it may have been exposed, revoke it at once with revoke_stream_token \u2014 that is also how you end one early. Prefer the shortest ttl_seconds that covers the work.",
|
|
5474
|
+
inputSchema: inputSchema32,
|
|
5475
|
+
handler: async (args, ctx) => {
|
|
5476
|
+
const result = await ctx.client.createStreamToken({
|
|
5477
|
+
resourceType: args.resource_type,
|
|
5478
|
+
resourceId: args.resource_id,
|
|
5479
|
+
eventTypes: args.event_types,
|
|
5480
|
+
ttlSeconds: args.ttl_seconds,
|
|
5481
|
+
label: args.label
|
|
5482
|
+
});
|
|
5483
|
+
return jsonResult(streamTokenMintOutput(result));
|
|
5484
|
+
}
|
|
5485
|
+
};
|
|
5486
|
+
|
|
5487
|
+
// src/server/tools/list_stream_tokens.ts
|
|
5488
|
+
import { z as z35 } from "zod";
|
|
5489
|
+
var LIST_DEFAULT5 = 50;
|
|
5490
|
+
var LIST_MAX5 = 200;
|
|
5491
|
+
var inputSchema33 = {
|
|
5492
|
+
resource_id: z35.string().optional().describe("Only tokens following this project."),
|
|
5493
|
+
live_only: z35.boolean().optional().describe("Hide expired and revoked tokens."),
|
|
5494
|
+
mine_only: z35.boolean().optional().describe(
|
|
5495
|
+
"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."
|
|
5496
|
+
),
|
|
5497
|
+
limit: z35.number().int().positive().optional().describe(`Max tokens to return (default ${String(LIST_DEFAULT5)}, capped at ${String(LIST_MAX5)}).`),
|
|
5498
|
+
offset: z35.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
|
|
5499
|
+
};
|
|
5500
|
+
var listStreamTokensTool = {
|
|
5501
|
+
name: "list_stream_tokens",
|
|
5502
|
+
description: "List the stream tokens in your organisation, with their scope, expiry and whether each is still live. Secrets are never returned \u2014 only a short prefix to tell tokens apart. Returns `total` and, when more remain, `next_offset`.",
|
|
5503
|
+
inputSchema: inputSchema33,
|
|
5504
|
+
handler: async (args, ctx) => {
|
|
5505
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT5, LIST_MAX5);
|
|
5506
|
+
const offset = args.offset ?? 0;
|
|
5507
|
+
const page = await ctx.client.listStreamTokens({
|
|
5508
|
+
resourceId: args.resource_id,
|
|
5509
|
+
liveOnly: args.live_only,
|
|
5510
|
+
mineOnly: args.mine_only,
|
|
5511
|
+
limit,
|
|
5512
|
+
offset
|
|
5513
|
+
});
|
|
5514
|
+
const next = nextOffsetOf(offset, page.tokens.length, page.total);
|
|
5515
|
+
return jsonResult({
|
|
5516
|
+
tokens: page.tokens.map(streamTokenSummary),
|
|
5517
|
+
total: page.total,
|
|
5518
|
+
limit,
|
|
5519
|
+
offset,
|
|
5520
|
+
...next === void 0 ? {} : { next_offset: next }
|
|
5521
|
+
});
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
5524
|
+
|
|
5525
|
+
// src/server/tools/revoke_stream_token.ts
|
|
5526
|
+
import { z as z36 } from "zod";
|
|
5527
|
+
var inputSchema34 = {
|
|
5528
|
+
id: z36.string().min(1).describe("Token id (from create_stream_token or list_stream_tokens) \u2014 NOT the secret itself.")
|
|
5529
|
+
};
|
|
5530
|
+
var revokeStreamTokenTool = {
|
|
5531
|
+
name: "revoke_stream_token",
|
|
5532
|
+
description: "Revoke a stream token immediately, closing its URL and deleting the events buffered for it. Use this as soon as a stream is no longer needed, or if its URL may have leaked \u2014 the URL is the credential, so revoking is the only way to stop it before it expires. Revoking a token you minted yourself always works. Revoking one somebody ELSE minted additionally requires the organisation owner or admin role, and is refused with 403 otherwise \u2014 so when hunting a leaked URL you did not create, expect that refusal and ask an organisation owner or admin rather than retrying.",
|
|
5533
|
+
inputSchema: inputSchema34,
|
|
5534
|
+
handler: async (args, ctx) => {
|
|
5535
|
+
const token = await ctx.client.revokeStreamToken(args.id);
|
|
5536
|
+
return jsonResult({ ...streamTokenSummary(token), revoked: true });
|
|
5537
|
+
}
|
|
5538
|
+
};
|
|
5539
|
+
|
|
4574
5540
|
// src/server/server.ts
|
|
4575
5541
|
function buildServer(deps) {
|
|
4576
5542
|
const server = new McpServer({
|
|
@@ -4602,6 +5568,15 @@ function buildServer(deps) {
|
|
|
4602
5568
|
registerTool(server, archiveSpecSessionTool, deps.client);
|
|
4603
5569
|
registerTool(server, unarchiveSpecSessionTool, deps.client);
|
|
4604
5570
|
registerTool(server, deleteSpecSessionTool, deps.client);
|
|
5571
|
+
registerTool(server, listArtifactsTool, deps.client);
|
|
5572
|
+
registerTool(server, getArtifactTool, deps.client);
|
|
5573
|
+
registerTool(server, readArtifactFileTool, deps.client);
|
|
5574
|
+
registerTool(server, createArtifactTool, deps.client);
|
|
5575
|
+
registerTool(server, writeArtifactRevisionTool, deps.client);
|
|
5576
|
+
registerTool(server, rollbackArtifactTool, deps.client);
|
|
5577
|
+
registerTool(server, createStreamTokenTool, deps.client);
|
|
5578
|
+
registerTool(server, listStreamTokensTool, deps.client);
|
|
5579
|
+
registerTool(server, revokeStreamTokenTool, deps.client);
|
|
4605
5580
|
return server;
|
|
4606
5581
|
}
|
|
4607
5582
|
async function startStdioServer(deps) {
|
|
@@ -4755,7 +5730,7 @@ async function canonicaliseRoot(root) {
|
|
|
4755
5730
|
import { promises as fs4 } from "fs";
|
|
4756
5731
|
import { join as join2, relative } from "path";
|
|
4757
5732
|
import { spawn } from "child_process";
|
|
4758
|
-
import { z as
|
|
5733
|
+
import { z as z37 } from "zod";
|
|
4759
5734
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4760
5735
|
|
|
4761
5736
|
// src/reverse/ignore.ts
|
|
@@ -5060,37 +6035,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
5060
6035
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
5061
6036
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
5062
6037
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
5063
|
-
var listDirInput =
|
|
5064
|
-
path:
|
|
5065
|
-
recursive:
|
|
5066
|
-
maxEntries:
|
|
5067
|
-
includeIgnored:
|
|
6038
|
+
var listDirInput = z37.object({
|
|
6039
|
+
path: z37.string(),
|
|
6040
|
+
recursive: z37.boolean().optional(),
|
|
6041
|
+
maxEntries: z37.number().int().positive().optional(),
|
|
6042
|
+
includeIgnored: z37.boolean().optional()
|
|
5068
6043
|
});
|
|
5069
|
-
var readFileInput =
|
|
5070
|
-
path:
|
|
5071
|
-
offset:
|
|
5072
|
-
limit:
|
|
5073
|
-
includeIgnored:
|
|
6044
|
+
var readFileInput = z37.object({
|
|
6045
|
+
path: z37.string(),
|
|
6046
|
+
offset: z37.number().int().nonnegative().optional(),
|
|
6047
|
+
limit: z37.number().int().positive().optional(),
|
|
6048
|
+
includeIgnored: z37.boolean().optional()
|
|
5074
6049
|
});
|
|
5075
|
-
var grepInput =
|
|
5076
|
-
pattern:
|
|
6050
|
+
var grepInput = z37.object({
|
|
6051
|
+
pattern: z37.string().describe(
|
|
5077
6052
|
"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."
|
|
5078
6053
|
),
|
|
5079
|
-
path:
|
|
5080
|
-
isRegex:
|
|
5081
|
-
caseSensitive:
|
|
5082
|
-
maxMatches:
|
|
5083
|
-
contextLines:
|
|
5084
|
-
includeIgnored:
|
|
6054
|
+
path: z37.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
|
|
6055
|
+
isRegex: z37.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
|
|
6056
|
+
caseSensitive: z37.boolean().optional(),
|
|
6057
|
+
maxMatches: z37.number().int().positive().optional(),
|
|
6058
|
+
contextLines: z37.number().int().nonnegative().optional(),
|
|
6059
|
+
includeIgnored: z37.boolean().optional()
|
|
5085
6060
|
});
|
|
5086
|
-
var packCodebaseInput =
|
|
5087
|
-
subpath:
|
|
5088
|
-
includePatterns:
|
|
5089
|
-
ignorePatterns:
|
|
6061
|
+
var packCodebaseInput = z37.object({
|
|
6062
|
+
subpath: z37.string().optional(),
|
|
6063
|
+
includePatterns: z37.string().optional(),
|
|
6064
|
+
ignorePatterns: z37.string().optional()
|
|
5090
6065
|
});
|
|
5091
|
-
var packCodebaseReadPageInput =
|
|
5092
|
-
outputId:
|
|
5093
|
-
page:
|
|
6066
|
+
var packCodebaseReadPageInput = z37.object({
|
|
6067
|
+
outputId: z37.string(),
|
|
6068
|
+
page: z37.number().int().positive()
|
|
5094
6069
|
});
|
|
5095
6070
|
function structuredError(error, message, extra) {
|
|
5096
6071
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -5618,7 +6593,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
5618
6593
|
return zodToJson(schema);
|
|
5619
6594
|
}
|
|
5620
6595
|
function zodToJson(schema) {
|
|
5621
|
-
if (schema instanceof
|
|
6596
|
+
if (schema instanceof z37.ZodObject) {
|
|
5622
6597
|
const shape = schema.shape;
|
|
5623
6598
|
const properties = {};
|
|
5624
6599
|
const required = [];
|
|
@@ -5633,19 +6608,19 @@ function zodToJson(schema) {
|
|
|
5633
6608
|
if (required.length > 0) out.required = required;
|
|
5634
6609
|
return out;
|
|
5635
6610
|
}
|
|
5636
|
-
if (schema instanceof
|
|
6611
|
+
if (schema instanceof z37.ZodOptional) {
|
|
5637
6612
|
return zodToJson(schema.unwrap());
|
|
5638
6613
|
}
|
|
5639
|
-
if (schema instanceof
|
|
6614
|
+
if (schema instanceof z37.ZodString) {
|
|
5640
6615
|
return { type: "string" };
|
|
5641
6616
|
}
|
|
5642
|
-
if (schema instanceof
|
|
6617
|
+
if (schema instanceof z37.ZodNumber) {
|
|
5643
6618
|
return { type: "number" };
|
|
5644
6619
|
}
|
|
5645
|
-
if (schema instanceof
|
|
6620
|
+
if (schema instanceof z37.ZodBoolean) {
|
|
5646
6621
|
return { type: "boolean" };
|
|
5647
6622
|
}
|
|
5648
|
-
if (schema instanceof
|
|
6623
|
+
if (schema instanceof z37.ZodEnum) {
|
|
5649
6624
|
return { type: "string", enum: schema.options };
|
|
5650
6625
|
}
|
|
5651
6626
|
return { type: "string" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myspec/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-next.112",
|
|
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": {
|