@myspec/mcp-server 0.4.0-next.106 → 0.4.0-next.107

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 +6 -0
  2. package/dist/index.js +655 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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
@@ -1349,7 +1349,10 @@ var KNOWN_CONFLICT_REASONS = [
1349
1349
  "revision_mismatch",
1350
1350
  "path_taken",
1351
1351
  "precondition_required",
1352
- "precondition_malformed"
1352
+ "precondition_malformed",
1353
+ "slug_taken",
1354
+ "blob_reclaimed",
1355
+ "lock_timeout"
1353
1356
  ];
1354
1357
  var PlatformHttpError = class extends Error {
1355
1358
  status;
@@ -2436,6 +2439,283 @@ var HttpSpecSessionClient = class {
2436
2439
  }
2437
2440
  };
2438
2441
 
2442
+ // ../../packages/platform-client/artifact/types/artifact.types.ts
2443
+ function transformArtifactResponse(raw) {
2444
+ return {
2445
+ id: raw.id,
2446
+ projectId: raw.project_id,
2447
+ org: raw.org,
2448
+ sessionId: raw.session_id ?? null,
2449
+ kind: raw.kind,
2450
+ name: raw.name,
2451
+ slug: raw.slug,
2452
+ description: raw.description ?? "",
2453
+ entryPath: raw.entry_path,
2454
+ revisionCount: raw.revision_count,
2455
+ createdBy: raw.created_by,
2456
+ createdAt: new Date(raw.created_at),
2457
+ updatedAt: new Date(raw.updated_at),
2458
+ trashedAt: raw.trashed_at ? new Date(raw.trashed_at) : void 0,
2459
+ deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
2460
+ };
2461
+ }
2462
+ function transformArtifactRevisionResponse(raw) {
2463
+ return {
2464
+ id: raw.id,
2465
+ artifactId: raw.artifact_id,
2466
+ revisionNumber: raw.revision_number,
2467
+ source: raw.source,
2468
+ restoredFromRevision: raw.restored_from_revision,
2469
+ message: raw.message ?? "",
2470
+ changedBy: raw.changed_by,
2471
+ changedByKind: raw.changed_by_kind === "ai" ? "ai" : "human",
2472
+ sessionId: raw.session_id ?? null,
2473
+ fileCount: raw.file_count,
2474
+ totalBytes: raw.total_bytes,
2475
+ manifestChecksum: raw.manifest_checksum,
2476
+ createdAt: new Date(raw.created_at)
2477
+ };
2478
+ }
2479
+ function transformArtifactFileEntry(raw) {
2480
+ return {
2481
+ path: raw.path,
2482
+ contentType: raw.content_type,
2483
+ sizeBytes: raw.size_bytes,
2484
+ checksum: raw.checksum
2485
+ };
2486
+ }
2487
+ function transformArtifactRevisionDetail(raw) {
2488
+ return {
2489
+ ...transformArtifactRevisionResponse(raw),
2490
+ files: (raw.files ?? []).map(transformArtifactFileEntry)
2491
+ };
2492
+ }
2493
+ function transformArtifactDetail(raw) {
2494
+ return {
2495
+ artifact: transformArtifactResponse(raw.artifact),
2496
+ head: transformArtifactRevisionDetail(raw.head)
2497
+ };
2498
+ }
2499
+ function transformArtifactWriteResult(raw) {
2500
+ return {
2501
+ artifact: transformArtifactResponse(raw.artifact),
2502
+ revision: transformArtifactRevisionDetail(raw.revision),
2503
+ noChange: raw.no_change === true
2504
+ };
2505
+ }
2506
+ function transformArtifactListResult(raw) {
2507
+ return {
2508
+ items: (raw.items ?? []).map(transformArtifactResponse),
2509
+ total: raw.total,
2510
+ limit: raw.limit,
2511
+ offset: raw.offset
2512
+ };
2513
+ }
2514
+ function transformArtifactRevisionListResult(raw) {
2515
+ return {
2516
+ items: (raw.items ?? []).map(transformArtifactRevisionResponse),
2517
+ total: raw.total,
2518
+ limit: raw.limit,
2519
+ offset: raw.offset
2520
+ };
2521
+ }
2522
+ function buildFilesBody(files) {
2523
+ return files.map((f) => {
2524
+ const body = { path: f.path, content: f.content };
2525
+ if (f.encoding) {
2526
+ body.encoding = f.encoding;
2527
+ }
2528
+ if (f.contentType) {
2529
+ body.content_type = f.contentType;
2530
+ }
2531
+ return body;
2532
+ });
2533
+ }
2534
+ function buildCreateArtifactBody(request) {
2535
+ const body = {
2536
+ name: request.name,
2537
+ files: buildFilesBody(request.files)
2538
+ };
2539
+ if (request.slug) body.slug = request.slug;
2540
+ if (request.kind) body.kind = request.kind;
2541
+ if (request.description !== void 0) body.description = request.description;
2542
+ if (request.entryPath) body.entry_path = request.entryPath;
2543
+ if (request.message) body.message = request.message;
2544
+ if (request.sessionId) body.session_id = request.sessionId;
2545
+ return body;
2546
+ }
2547
+ function buildCreateArtifactRevisionBody(request) {
2548
+ const body = { files: buildFilesBody(request.files) };
2549
+ if (request.message) body.message = request.message;
2550
+ if (request.baseRevision !== void 0) body.base_revision = request.baseRevision;
2551
+ if (request.remove && request.remove.length > 0) body.remove = request.remove;
2552
+ if (request.entryPath) body.entry_path = request.entryPath;
2553
+ if (request.sessionId) body.session_id = request.sessionId;
2554
+ return body;
2555
+ }
2556
+ function buildRollbackArtifactBody(request) {
2557
+ const body = { revision_number: request.revisionNumber };
2558
+ if (request.message) body.message = request.message;
2559
+ if (request.sessionId) body.session_id = request.sessionId;
2560
+ return body;
2561
+ }
2562
+ function buildUpdateArtifactBody(request) {
2563
+ const body = {};
2564
+ if (request.name !== void 0) body.name = request.name;
2565
+ if (request.description !== void 0) body.description = request.description;
2566
+ if (request.entryPath !== void 0) body.entry_path = request.entryPath;
2567
+ return body;
2568
+ }
2569
+
2570
+ // ../../packages/platform-client/artifact/http/artifact.http-client.ts
2571
+ var BASE_PATH = "/artifact/v1";
2572
+ var HEADER_EXPECTED_VERSION2 = "X-Expected-Version";
2573
+ var HEADER_ACTOR_KIND2 = "X-Actor-Kind";
2574
+ function writeHeaders(opts) {
2575
+ const headers = {};
2576
+ if (opts?.expectedVersion !== void 0) {
2577
+ headers[HEADER_EXPECTED_VERSION2] = String(opts.expectedVersion);
2578
+ }
2579
+ if (opts?.actorKind) {
2580
+ headers[HEADER_ACTOR_KIND2] = opts.actorKind;
2581
+ }
2582
+ return headers;
2583
+ }
2584
+ function requestOptions(headers) {
2585
+ return Object.keys(headers).length > 0 ? { headers } : {};
2586
+ }
2587
+ function encodeArtifactPath(path4) {
2588
+ return path4.split("/").map(encodeURIComponent).join("/");
2589
+ }
2590
+ function paginationQuery(opts) {
2591
+ const params = new URLSearchParams();
2592
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
2593
+ if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
2594
+ if (opts?.kind) params.set("kind", opts.kind);
2595
+ const query = params.toString();
2596
+ return query ? `?${query}` : "";
2597
+ }
2598
+ var HttpArtifactClient = class {
2599
+ httpClient;
2600
+ constructor(httpClient) {
2601
+ this.httpClient = httpClient;
2602
+ }
2603
+ async createArtifact(projectId, request, jwtToken, opts) {
2604
+ const raw = await this.httpClient.post(
2605
+ `${BASE_PATH}/projects/${projectId}/artifacts`,
2606
+ buildCreateArtifactBody(request),
2607
+ jwtToken,
2608
+ requestOptions(writeHeaders(opts))
2609
+ );
2610
+ return transformArtifactWriteResult(raw);
2611
+ }
2612
+ async listArtifacts(projectId, jwtToken, opts) {
2613
+ const raw = await this.httpClient.get(
2614
+ `${BASE_PATH}/projects/${projectId}/artifacts${paginationQuery(opts)}`,
2615
+ jwtToken,
2616
+ {}
2617
+ );
2618
+ return transformArtifactListResult(raw);
2619
+ }
2620
+ async listTrashedArtifacts(projectId, jwtToken) {
2621
+ const raw = await this.httpClient.get(
2622
+ `${BASE_PATH}/projects/${projectId}/artifacts/trashed`,
2623
+ jwtToken,
2624
+ {}
2625
+ );
2626
+ return (raw.items ?? []).map(transformArtifactResponse);
2627
+ }
2628
+ async getArtifact(artifactId, jwtToken) {
2629
+ const raw = await this.httpClient.get(
2630
+ `${BASE_PATH}/artifacts/${artifactId}`,
2631
+ jwtToken,
2632
+ {}
2633
+ );
2634
+ return transformArtifactDetail(raw);
2635
+ }
2636
+ async updateArtifact(artifactId, request, jwtToken) {
2637
+ const raw = await this.httpClient.patch(
2638
+ `${BASE_PATH}/artifacts/${artifactId}`,
2639
+ buildUpdateArtifactBody(request),
2640
+ jwtToken,
2641
+ {}
2642
+ );
2643
+ return transformArtifactResponse(raw);
2644
+ }
2645
+ async trashArtifact(artifactId, jwtToken) {
2646
+ await this.httpClient.delete(`${BASE_PATH}/artifacts/${artifactId}`, jwtToken, {});
2647
+ }
2648
+ async restoreArtifact(artifactId, jwtToken) {
2649
+ await this.httpClient.post(
2650
+ `${BASE_PATH}/artifacts/${artifactId}/restore`,
2651
+ {},
2652
+ jwtToken,
2653
+ {}
2654
+ );
2655
+ }
2656
+ async deleteArtifactPermanently(artifactId, jwtToken) {
2657
+ await this.httpClient.delete(
2658
+ `${BASE_PATH}/artifacts/${artifactId}/permanent`,
2659
+ jwtToken,
2660
+ {}
2661
+ );
2662
+ }
2663
+ async createRevision(artifactId, request, jwtToken, opts) {
2664
+ const raw = await this.httpClient.post(
2665
+ `${BASE_PATH}/artifacts/${artifactId}/revisions`,
2666
+ buildCreateArtifactRevisionBody(request),
2667
+ jwtToken,
2668
+ requestOptions(writeHeaders(opts))
2669
+ );
2670
+ return transformArtifactWriteResult(raw);
2671
+ }
2672
+ async rollback(artifactId, request, jwtToken, opts) {
2673
+ const raw = await this.httpClient.post(
2674
+ `${BASE_PATH}/artifacts/${artifactId}/rollback`,
2675
+ buildRollbackArtifactBody(request),
2676
+ jwtToken,
2677
+ requestOptions(writeHeaders(opts))
2678
+ );
2679
+ return transformArtifactWriteResult(raw);
2680
+ }
2681
+ async listRevisions(artifactId, jwtToken, opts) {
2682
+ const raw = await this.httpClient.get(
2683
+ `${BASE_PATH}/artifacts/${artifactId}/revisions${paginationQuery(opts)}`,
2684
+ jwtToken,
2685
+ {}
2686
+ );
2687
+ return transformArtifactRevisionListResult(raw);
2688
+ }
2689
+ async getRevision(artifactId, revisionNumber, jwtToken) {
2690
+ const raw = await this.httpClient.get(
2691
+ `${BASE_PATH}/artifacts/${artifactId}/revisions/${revisionNumber}`,
2692
+ jwtToken,
2693
+ {}
2694
+ );
2695
+ return transformArtifactRevisionDetail(raw);
2696
+ }
2697
+ fileUrl(artifactId, path4, revision) {
2698
+ const baseUrl = this.httpClient.getBaseUrl().replace(/\/$/, "");
2699
+ const encoded = encodeArtifactPath(path4);
2700
+ return revision === void 0 ? `${baseUrl}${BASE_PATH}/artifacts/${artifactId}/files/${encoded}` : `${baseUrl}${BASE_PATH}/artifacts/${artifactId}/revisions/${revision}/files/${encoded}`;
2701
+ }
2702
+ async downloadFile(artifactId, path4, jwtToken, opts) {
2703
+ const response = await fetch(this.fileUrl(artifactId, path4, opts?.revision), {
2704
+ method: "GET",
2705
+ headers: { Authorization: `Bearer ${jwtToken}` }
2706
+ });
2707
+ if (!response.ok) {
2708
+ await throwFromResponse(response, "Artifact file download failed");
2709
+ }
2710
+ const buffer = await response.arrayBuffer();
2711
+ return {
2712
+ content: new Uint8Array(buffer),
2713
+ contentType: response.headers.get("Content-Type") ?? "application/octet-stream",
2714
+ checksum: (response.headers.get("ETag") ?? "").replace(/"/g, "").trim()
2715
+ };
2716
+ }
2717
+ };
2718
+
2439
2719
  // src/platform/client.ts
2440
2720
  var silentLogger = {
2441
2721
  debug: () => void 0,
@@ -2722,9 +3002,43 @@ var PlatformClient = class {
2722
3002
  project: new HttpProjectClient(httpClient),
2723
3003
  session: new HttpSpecSessionClient(httpClient),
2724
3004
  file: new HttpFileClient(httpClient),
2725
- attachment: new HttpAttachmentClient(httpClient)
3005
+ attachment: new HttpAttachmentClient(httpClient),
3006
+ artifact: new HttpArtifactClient(httpClient)
2726
3007
  };
2727
3008
  }
3009
+ // ----------------------------------------------------------- artifacts
3010
+ //
3011
+ // Every write is attributed `actorKind: 'ai'`: an MCP client is an
3012
+ // assistant acting on the user's behalf, exactly as for spec files.
3013
+ async listArtifacts(projectId, opts = {}) {
3014
+ return this.withTokenRetry((jwt, c) => c.artifact.listArtifacts(projectId, jwt, opts));
3015
+ }
3016
+ async getArtifact(artifactId) {
3017
+ return this.withTokenRetry((jwt, c) => c.artifact.getArtifact(artifactId, jwt));
3018
+ }
3019
+ async getArtifactRevision(artifactId, revisionNumber) {
3020
+ return this.withTokenRetry((jwt, c) => c.artifact.getRevision(artifactId, revisionNumber, jwt));
3021
+ }
3022
+ async downloadArtifactFile(artifactId, path4, revision) {
3023
+ return this.withTokenRetry(
3024
+ (jwt, c) => c.artifact.downloadFile(artifactId, path4, jwt, revision === void 0 ? void 0 : { revision })
3025
+ );
3026
+ }
3027
+ async createArtifact(projectId, request) {
3028
+ return this.withTokenRetry(
3029
+ (jwt, c) => c.artifact.createArtifact(projectId, request, jwt, { actorKind: "ai" })
3030
+ );
3031
+ }
3032
+ async createArtifactRevision(artifactId, request, expectedVersion) {
3033
+ return this.withTokenRetry(
3034
+ (jwt, c) => c.artifact.createRevision(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
3035
+ );
3036
+ }
3037
+ async rollbackArtifact(artifactId, request, expectedVersion) {
3038
+ return this.withTokenRetry(
3039
+ (jwt, c) => c.artifact.rollback(artifactId, request, jwt, { expectedVersion, actorKind: "ai" })
3040
+ );
3041
+ }
2728
3042
  async withTokenRetry(call) {
2729
3043
  const clients = await this.clients();
2730
3044
  const jwt = await this.tokenManager.getValidAccessToken();
@@ -4571,6 +4885,307 @@ var uploadSpecFileTool = {
4571
4885
  }
4572
4886
  };
4573
4887
 
4888
+ // src/server/tools/list_artifacts.ts
4889
+ import { z as z27 } from "zod";
4890
+
4891
+ // src/server/tools/artifact-output.ts
4892
+ import { z as z26 } from "zod";
4893
+ var artifactFilesSchema = z26.array(
4894
+ z26.object({
4895
+ path: z26.string().min(1).describe('Bundle-relative path, "/"-separated, no leading slash (e.g. assets/logo.svg).'),
4896
+ content: z26.string().describe("The file content, in `encoding` (default utf8)."),
4897
+ encoding: z26.enum(["utf8", "base64"]).optional().describe("utf8 (default) for text; base64 for binary files such as images."),
4898
+ content_type: z26.string().optional().describe("Hint used only for extensions the platform does not recognise.")
4899
+ })
4900
+ ).describe("Files of the bundle.");
4901
+ function artifactSummary(a) {
4902
+ return {
4903
+ id: a.id,
4904
+ project_id: a.projectId,
4905
+ name: a.name,
4906
+ slug: a.slug,
4907
+ kind: a.kind,
4908
+ description: a.description,
4909
+ entry_path: a.entryPath,
4910
+ revision_count: a.revisionCount,
4911
+ session_id: a.sessionId,
4912
+ created_by: a.createdBy,
4913
+ created_at: a.createdAt,
4914
+ updated_at: a.updatedAt,
4915
+ ...a.trashedAt ? { trashed_at: a.trashedAt } : {}
4916
+ };
4917
+ }
4918
+ function fileEntry(f) {
4919
+ return { path: f.path, content_type: f.contentType, size_bytes: f.sizeBytes, checksum: f.checksum };
4920
+ }
4921
+ function revisionSummary(r) {
4922
+ return {
4923
+ id: r.id,
4924
+ revision_number: r.revisionNumber,
4925
+ source: r.source,
4926
+ ...r.restoredFromRevision === void 0 ? {} : { restored_from_revision: r.restoredFromRevision },
4927
+ message: r.message,
4928
+ changed_by: r.changedBy,
4929
+ changed_by_kind: r.changedByKind,
4930
+ file_count: r.fileCount,
4931
+ total_bytes: r.totalBytes,
4932
+ manifest_checksum: r.manifestChecksum,
4933
+ created_at: r.createdAt,
4934
+ ..."files" in r ? { files: r.files.map(fileEntry) } : {}
4935
+ };
4936
+ }
4937
+ function writeResultOutput(result) {
4938
+ return {
4939
+ artifact: artifactSummary(result.artifact),
4940
+ revision: revisionSummary(result.revision),
4941
+ no_change: result.noChange
4942
+ };
4943
+ }
4944
+ function isArtifactWriteConflict(conflict) {
4945
+ return conflict.reason === "revision_mismatch" || conflict.reason === "blob_reclaimed" || conflict.reason === "lock_timeout" || conflict.reason === "unknown";
4946
+ }
4947
+ function artifactConflictMessage(artifactId, conflict) {
4948
+ if (conflict.reason === "lock_timeout") {
4949
+ 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.`;
4950
+ }
4951
+ if (conflict.reason === "blob_reclaimed") {
4952
+ 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).`;
4953
+ }
4954
+ const head = conflict.currentVersion === void 0 ? "" : ` The head is now revision ${String(conflict.currentVersion)}.`;
4955
+ 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.`;
4956
+ }
4957
+ function artifactCreateConflictMessage(conflict) {
4958
+ if (conflict.reason === "slug_taken") {
4959
+ 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.";
4960
+ }
4961
+ if (conflict.reason === "lock_timeout") {
4962
+ return "Artifact was NOT created: the platform timed out waiting for a lock. Nothing changed \u2014 retry the same call as-is.";
4963
+ }
4964
+ return `Artifact was NOT created: the platform reported a conflict (${conflict.reason}). Re-read the project's artifacts with list_artifacts before retrying.`;
4965
+ }
4966
+
4967
+ // src/server/tools/list_artifacts.ts
4968
+ var LIST_DEFAULT3 = 50;
4969
+ var LIST_MAX3 = 100;
4970
+ var inputSchema26 = {
4971
+ project_id: z27.string().min(1).describe("UUID of the project whose artifacts to list."),
4972
+ limit: z27.number().int().positive().optional().describe(`Max artifacts to return (default ${String(LIST_DEFAULT3)}, capped at ${String(LIST_MAX3)}).`),
4973
+ offset: z27.number().int().nonnegative().optional().describe("Pagination offset (default 0).")
4974
+ };
4975
+ var listArtifactsTool = {
4976
+ name: "list_artifacts",
4977
+ 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.",
4978
+ inputSchema: inputSchema26,
4979
+ handler: async (args, ctx) => {
4980
+ const limit = clampLimit(args.limit, LIST_DEFAULT3, LIST_MAX3);
4981
+ const offset = args.offset ?? 0;
4982
+ const page = await ctx.client.listArtifacts(args.project_id, { limit, offset });
4983
+ const next = nextOffsetOf(offset, page.items.length, page.total);
4984
+ return jsonResult({
4985
+ project_id: args.project_id,
4986
+ artifacts: page.items.map(artifactSummary),
4987
+ total: page.total,
4988
+ limit,
4989
+ offset,
4990
+ ...next === void 0 ? {} : { next_offset: next }
4991
+ });
4992
+ }
4993
+ };
4994
+
4995
+ // src/server/tools/get_artifact.ts
4996
+ import { z as z28 } from "zod";
4997
+ var inputSchema27 = {
4998
+ artifact_id: z28.string().min(1).describe("UUID of the artifact."),
4999
+ revision: z28.number().int().positive().optional().describe("Revision whose manifest to return (defaults to the head revision).")
5000
+ };
5001
+ var getArtifactTool = {
5002
+ name: "get_artifact",
5003
+ 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.",
5004
+ inputSchema: inputSchema27,
5005
+ handler: async (args, ctx) => {
5006
+ const detail = await ctx.client.getArtifact(args.artifact_id);
5007
+ if (args.revision === void 0) {
5008
+ return jsonResult({ artifact: artifactSummary(detail.artifact), revision: revisionSummary(detail.head) });
5009
+ }
5010
+ if (args.revision > detail.artifact.revisionCount) {
5011
+ return errorResult(
5012
+ `revision=${String(args.revision)} is greater than revision_count=${String(detail.artifact.revisionCount)} for artifact ${args.artifact_id}.`
5013
+ );
5014
+ }
5015
+ const revision = await ctx.client.getArtifactRevision(args.artifact_id, args.revision);
5016
+ return jsonResult({ artifact: artifactSummary(detail.artifact), revision: revisionSummary(revision) });
5017
+ }
5018
+ };
5019
+
5020
+ // src/server/tools/read_artifact_file.ts
5021
+ import { z as z29 } from "zod";
5022
+ var MAX_READABLE_ARTIFACT_FILE_BYTES = 8 * 1024 * 1024;
5023
+ var inputSchema28 = {
5024
+ artifact_id: z29.string().min(1).describe("UUID of the artifact."),
5025
+ path: z29.string().min(1).describe("Bundle-relative path of the file (e.g. index.html, assets/app.js)."),
5026
+ revision: z29.number().int().positive().optional().describe("Revision to read from (defaults to the head revision)."),
5027
+ offset: z29.number().int().nonnegative().optional().describe("1-based line number to start reading from (default 1; 0 is an alias for 1)."),
5028
+ limit: z29.number().int().positive().optional().describe("Max lines to return (default 2000, capped at 2000; larger values are clamped).")
5029
+ };
5030
+ var readArtifactFileTool = {
5031
+ name: "read_artifact_file",
5032
+ 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.",
5033
+ inputSchema: inputSchema28,
5034
+ handler: async (args, ctx) => {
5035
+ const file = await ctx.client.downloadArtifactFile(args.artifact_id, args.path, args.revision);
5036
+ if (file.content.byteLength > MAX_READABLE_ARTIFACT_FILE_BYTES) {
5037
+ return errorResult(
5038
+ `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.`
5039
+ );
5040
+ }
5041
+ if (isBinary(file.content)) {
5042
+ return errorResult(
5043
+ `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.`
5044
+ );
5045
+ }
5046
+ const text = decodeUtf8Strict(file.content);
5047
+ if (text === null) {
5048
+ return errorResult(
5049
+ `File ${args.path} of artifact ${args.artifact_id} is not valid UTF-8 text (content_type=${file.contentType}).`
5050
+ );
5051
+ }
5052
+ const paginated = paginateText(text, args.offset, args.limit);
5053
+ if (!paginated.ok) {
5054
+ return errorResult(paginated.error);
5055
+ }
5056
+ return jsonResult({
5057
+ artifact_id: args.artifact_id,
5058
+ path: args.path,
5059
+ ...args.revision === void 0 ? {} : { revision_number: args.revision },
5060
+ content_type: file.contentType,
5061
+ checksum: file.checksum,
5062
+ ...paginated.window
5063
+ });
5064
+ }
5065
+ };
5066
+
5067
+ // src/server/tools/create_artifact.ts
5068
+ import { z as z30 } from "zod";
5069
+ var inputSchema29 = {
5070
+ project_id: z30.string().min(1).describe("UUID of the project to create the artifact in."),
5071
+ name: z30.string().min(1).describe("Display name (max 200 characters)."),
5072
+ slug: z30.string().optional().describe("URL-safe identifier, unique within the project; derived from the name when omitted."),
5073
+ description: z30.string().optional().describe("Optional description (max 2000 characters)."),
5074
+ entry_path: z30.string().optional().describe("The file a renderer opens first. Defaults to index.html when present, else the only file."),
5075
+ message: z30.string().optional().describe("Short summary of this first revision (max 500 characters)."),
5076
+ files: artifactFilesSchema
5077
+ };
5078
+ var createArtifactTool = {
5079
+ name: "create_artifact",
5080
+ 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).",
5081
+ inputSchema: inputSchema29,
5082
+ handler: async (args, ctx) => {
5083
+ try {
5084
+ const result = await ctx.client.createArtifact(args.project_id, {
5085
+ name: args.name,
5086
+ slug: args.slug,
5087
+ description: args.description,
5088
+ entryPath: args.entry_path,
5089
+ message: args.message,
5090
+ files: args.files.map((f) => ({
5091
+ path: f.path,
5092
+ content: f.content,
5093
+ encoding: f.encoding,
5094
+ contentType: f.content_type
5095
+ }))
5096
+ });
5097
+ return jsonResult(writeResultOutput(result));
5098
+ } catch (err) {
5099
+ const conflict = conflictOf(err);
5100
+ if (conflict) {
5101
+ return errorResult(artifactCreateConflictMessage(conflict));
5102
+ }
5103
+ throw err;
5104
+ }
5105
+ }
5106
+ };
5107
+
5108
+ // src/server/tools/write_artifact_revision.ts
5109
+ import { z as z31 } from "zod";
5110
+ var inputSchema30 = {
5111
+ artifact_id: z31.string().min(1).describe("UUID of the artifact."),
5112
+ files: artifactFilesSchema,
5113
+ base_revision: z31.number().int().positive().optional().describe(
5114
+ "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."
5115
+ ),
5116
+ remove: z31.array(z31.string().min(1)).optional().describe("Patch mode only: paths to drop from the base revision."),
5117
+ expected_version: z31.number().int().nonnegative().optional().describe(
5118
+ "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."
5119
+ ),
5120
+ message: z31.string().optional().describe("Short summary of the change (max 500 characters)."),
5121
+ entry_path: z31.string().optional().describe("Change the file a renderer opens first, from this revision on.")
5122
+ };
5123
+ var writeArtifactRevisionTool = {
5124
+ name: "write_artifact_revision",
5125
+ 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.",
5126
+ inputSchema: inputSchema30,
5127
+ handler: async (args, ctx) => {
5128
+ if (args.remove && args.remove.length > 0 && args.base_revision === void 0) {
5129
+ return errorResult("`remove` is only valid in patch mode: set base_revision to the revision you are editing.");
5130
+ }
5131
+ try {
5132
+ const result = await ctx.client.createArtifactRevision(
5133
+ args.artifact_id,
5134
+ {
5135
+ files: args.files.map((f) => ({
5136
+ path: f.path,
5137
+ content: f.content,
5138
+ encoding: f.encoding,
5139
+ contentType: f.content_type
5140
+ })),
5141
+ remove: args.remove,
5142
+ baseRevision: args.base_revision,
5143
+ message: args.message,
5144
+ entryPath: args.entry_path
5145
+ },
5146
+ args.expected_version
5147
+ );
5148
+ return jsonResult(writeResultOutput(result));
5149
+ } catch (err) {
5150
+ const conflict = conflictOf(err);
5151
+ if (!conflict || !isArtifactWriteConflict(conflict)) {
5152
+ throw err;
5153
+ }
5154
+ return errorResult(artifactConflictMessage(args.artifact_id, conflict));
5155
+ }
5156
+ }
5157
+ };
5158
+
5159
+ // src/server/tools/rollback_artifact.ts
5160
+ import { z as z32 } from "zod";
5161
+ var inputSchema31 = {
5162
+ artifact_id: z32.string().min(1).describe("UUID of the artifact."),
5163
+ revision_number: z32.number().int().positive().describe("The revision whose files to restore."),
5164
+ expected_version: z32.number().int().nonnegative().optional().describe("The revision_count this rollback is based on (from get_artifact); defaults to the current head."),
5165
+ message: z32.string().optional().describe('Optional summary; defaults to "Rolled back to revision N".')
5166
+ };
5167
+ var rollbackArtifactTool = {
5168
+ name: "rollback_artifact",
5169
+ 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.",
5170
+ inputSchema: inputSchema31,
5171
+ handler: async (args, ctx) => {
5172
+ try {
5173
+ const result = await ctx.client.rollbackArtifact(
5174
+ args.artifact_id,
5175
+ { revisionNumber: args.revision_number, message: args.message },
5176
+ args.expected_version
5177
+ );
5178
+ return jsonResult(writeResultOutput(result));
5179
+ } catch (err) {
5180
+ const conflict = conflictOf(err);
5181
+ if (!conflict || !isArtifactWriteConflict(conflict)) {
5182
+ throw err;
5183
+ }
5184
+ return errorResult(artifactConflictMessage(args.artifact_id, conflict));
5185
+ }
5186
+ }
5187
+ };
5188
+
4574
5189
  // src/server/server.ts
4575
5190
  function buildServer(deps) {
4576
5191
  const server = new McpServer({
@@ -4602,6 +5217,12 @@ function buildServer(deps) {
4602
5217
  registerTool(server, archiveSpecSessionTool, deps.client);
4603
5218
  registerTool(server, unarchiveSpecSessionTool, deps.client);
4604
5219
  registerTool(server, deleteSpecSessionTool, deps.client);
5220
+ registerTool(server, listArtifactsTool, deps.client);
5221
+ registerTool(server, getArtifactTool, deps.client);
5222
+ registerTool(server, readArtifactFileTool, deps.client);
5223
+ registerTool(server, createArtifactTool, deps.client);
5224
+ registerTool(server, writeArtifactRevisionTool, deps.client);
5225
+ registerTool(server, rollbackArtifactTool, deps.client);
4605
5226
  return server;
4606
5227
  }
4607
5228
  async function startStdioServer(deps) {
@@ -4755,7 +5376,7 @@ async function canonicaliseRoot(root) {
4755
5376
  import { promises as fs4 } from "fs";
4756
5377
  import { join as join2, relative } from "path";
4757
5378
  import { spawn } from "child_process";
4758
- import { z as z26 } from "zod";
5379
+ import { z as z33 } from "zod";
4759
5380
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4760
5381
 
4761
5382
  // src/reverse/ignore.ts
@@ -5060,37 +5681,37 @@ var READ_FILE_MAX_LINES = 2e3;
5060
5681
  var LIST_DIR_HARD_CAP = 5e3;
5061
5682
  var LIST_DIR_DEFAULT_CAP = 1e3;
5062
5683
  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()
5684
+ var listDirInput = z33.object({
5685
+ path: z33.string(),
5686
+ recursive: z33.boolean().optional(),
5687
+ maxEntries: z33.number().int().positive().optional(),
5688
+ includeIgnored: z33.boolean().optional()
5068
5689
  });
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()
5690
+ var readFileInput = z33.object({
5691
+ path: z33.string(),
5692
+ offset: z33.number().int().nonnegative().optional(),
5693
+ limit: z33.number().int().positive().optional(),
5694
+ includeIgnored: z33.boolean().optional()
5074
5695
  });
5075
- var grepInput = z26.object({
5076
- pattern: z26.string().describe(
5696
+ var grepInput = z33.object({
5697
+ pattern: z33.string().describe(
5077
5698
  "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
5699
  ),
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()
5700
+ path: z33.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
5701
+ isRegex: z33.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
5702
+ caseSensitive: z33.boolean().optional(),
5703
+ maxMatches: z33.number().int().positive().optional(),
5704
+ contextLines: z33.number().int().nonnegative().optional(),
5705
+ includeIgnored: z33.boolean().optional()
5085
5706
  });
5086
- var packCodebaseInput = z26.object({
5087
- subpath: z26.string().optional(),
5088
- includePatterns: z26.string().optional(),
5089
- ignorePatterns: z26.string().optional()
5707
+ var packCodebaseInput = z33.object({
5708
+ subpath: z33.string().optional(),
5709
+ includePatterns: z33.string().optional(),
5710
+ ignorePatterns: z33.string().optional()
5090
5711
  });
5091
- var packCodebaseReadPageInput = z26.object({
5092
- outputId: z26.string(),
5093
- page: z26.number().int().positive()
5712
+ var packCodebaseReadPageInput = z33.object({
5713
+ outputId: z33.string(),
5714
+ page: z33.number().int().positive()
5094
5715
  });
5095
5716
  function structuredError(error, message, extra) {
5096
5717
  const payload = { error, message, ...extra ?? {} };
@@ -5618,7 +6239,7 @@ function jsonSchemaFromZod(schema) {
5618
6239
  return zodToJson(schema);
5619
6240
  }
5620
6241
  function zodToJson(schema) {
5621
- if (schema instanceof z26.ZodObject) {
6242
+ if (schema instanceof z33.ZodObject) {
5622
6243
  const shape = schema.shape;
5623
6244
  const properties = {};
5624
6245
  const required = [];
@@ -5633,19 +6254,19 @@ function zodToJson(schema) {
5633
6254
  if (required.length > 0) out.required = required;
5634
6255
  return out;
5635
6256
  }
5636
- if (schema instanceof z26.ZodOptional) {
6257
+ if (schema instanceof z33.ZodOptional) {
5637
6258
  return zodToJson(schema.unwrap());
5638
6259
  }
5639
- if (schema instanceof z26.ZodString) {
6260
+ if (schema instanceof z33.ZodString) {
5640
6261
  return { type: "string" };
5641
6262
  }
5642
- if (schema instanceof z26.ZodNumber) {
6263
+ if (schema instanceof z33.ZodNumber) {
5643
6264
  return { type: "number" };
5644
6265
  }
5645
- if (schema instanceof z26.ZodBoolean) {
6266
+ if (schema instanceof z33.ZodBoolean) {
5646
6267
  return { type: "boolean" };
5647
6268
  }
5648
- if (schema instanceof z26.ZodEnum) {
6269
+ if (schema instanceof z33.ZodEnum) {
5649
6270
  return { type: "string", enum: schema.options };
5650
6271
  }
5651
6272
  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.106",
3
+ "version": "0.4.0-next.107",
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": {