@myspec/mcp-server 0.4.0-next.98 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -2
  2. package/dist/index.js +1040 -65
  3. 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 of the file, plus `content_version` when reading the latest revision (errors for binary or files > 1 MiB — use `download_spec_file` for those). Cached on disk under `MYSPEC_DOWNLOAD_ROOT` |
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 files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
4034
+ const limit = clampLimit(args.limit, LIST_DEFAULT2, LIST_MAX2);
4035
+ const offset = args.offset ?? 0;
3575
4036
  const prefix = normalizePrefix(args.path);
3576
- const matches = files.filter((f) => isUnderPath(f.filePath, prefix)).map(toEntry).sort((a, b) => a.file_path.localeCompare(b.file_path));
3577
- return jsonResult({
3578
- project_id: args.project_id,
3579
- path: prefix,
3580
- trashed,
3581
- total: matches.length,
3582
- files: matches
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 LIST_DEFAULT2 = 10;
3630
- var LIST_MAX2 = 20;
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(LIST_DEFAULT2)}, capped at ${String(LIST_MAX2)}).`
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(LIST_MAX2)} per page; returns \`total\` and \`next_offset\`.`,
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, LIST_DEFAULT2, LIST_MAX2);
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 MAX_READ_BYTES = 1 * 1024 * 1024;
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. Fails for binary files or files larger than 1 MiB \u2014 use download_spec_file to save those to a local file instead.",
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 (isLatestRevision && meta.fileSizeBytes > MAX_READ_BYTES) {
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 cached = await pathExists(cachePath) ? await fs3.readFile(cachePath) : null;
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 > MAX_READ_BYTES) {
4408
+ if (bytes.byteLength > MAX_READABLE_SPEC_FILE_BYTES) {
3906
4409
  return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
3907
4410
  }
3908
- await writeBytesAtomically(cachePath, bytes);
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 > MAX_READ_BYTES) {
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: targetRevision,
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
- ...isLatestRevision && meta.contentVersion !== void 0 ? { content_version: meta.contentVersion } : {},
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(cachePath),
4465
+ cache_path: maskHomedir(effectiveCachePath),
3938
4466
  cache_hit: cacheHit,
3939
- content: text
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(MAX_READ_BYTES)} bytes. Use download_spec_file to save it to a local file and read it in chunks.`;
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 z26 } from "zod";
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 = z26.object({
5064
- path: z26.string(),
5065
- recursive: z26.boolean().optional(),
5066
- maxEntries: z26.number().int().positive().optional(),
5067
- includeIgnored: z26.boolean().optional()
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 = z26.object({
5070
- path: z26.string(),
5071
- offset: z26.number().int().nonnegative().optional(),
5072
- limit: z26.number().int().positive().optional(),
5073
- includeIgnored: z26.boolean().optional()
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 = z26.object({
5076
- pattern: z26.string().describe(
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: z26.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
5080
- isRegex: z26.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
5081
- caseSensitive: z26.boolean().optional(),
5082
- maxMatches: z26.number().int().positive().optional(),
5083
- contextLines: z26.number().int().nonnegative().optional(),
5084
- includeIgnored: z26.boolean().optional()
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 = z26.object({
5087
- subpath: z26.string().optional(),
5088
- includePatterns: z26.string().optional(),
5089
- ignorePatterns: z26.string().optional()
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 = z26.object({
5092
- outputId: z26.string(),
5093
- page: z26.number().int().positive()
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 z26.ZodObject) {
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 z26.ZodOptional) {
6611
+ if (schema instanceof z37.ZodOptional) {
5637
6612
  return zodToJson(schema.unwrap());
5638
6613
  }
5639
- if (schema instanceof z26.ZodString) {
6614
+ if (schema instanceof z37.ZodString) {
5640
6615
  return { type: "string" };
5641
6616
  }
5642
- if (schema instanceof z26.ZodNumber) {
6617
+ if (schema instanceof z37.ZodNumber) {
5643
6618
  return { type: "number" };
5644
6619
  }
5645
- if (schema instanceof z26.ZodBoolean) {
6620
+ if (schema instanceof z37.ZodBoolean) {
5646
6621
  return { type: "boolean" };
5647
6622
  }
5648
- if (schema instanceof z26.ZodEnum) {
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.4.0-next.98",
3
+ "version": "0.4.0",
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": {