@vardum/mcp 0.3.0 → 0.4.1

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 +104 -62
  2. package/dist/index.js +121 -23
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,30 +1,33 @@
1
1
  # @vardum/mcp
2
2
 
3
- MCP server for [Vardum](https://vardum.com): lets an AI agent manage assets,
3
+ MCP server for [Vardum](https://vardum.com): lets an AI agent manage assets and
4
4
  Vards using a key scoped to one workspace. Workspace creation, membership,
5
5
  API-key management and domains are managed in the web app with a signed-in session.
6
6
 
7
- ## Setup
7
+ This README describes [MCP **0.4.1**](https://www.npmjs.com/package/@vardum/mcp/v/0.4.1),
8
+ including revision-guarded Vard editing. The matching client batch uses CLI
9
+ **0.8.1** and shared **0.4.1**. This patch makes setup self-contained without
10
+ changing the tools or editor protocol. Older MCP 0.3.0 uses the legacy editing
11
+ contract; installing it does not enable revision guards. The published 0.1.0
12
+ artifact has a broken workspace dependency.
8
13
 
9
- The server speaks MCP over stdio, so the client starts it — there is nothing to
10
- run by hand. Create a workspace API key in **Settings // API keys** first; the
11
- key is restricted to that workspace and its selected read/write permissions.
12
- Copy the workspace ID from Settings → General for tool calls.
14
+ ## Setup
13
15
 
14
- This README describes **MCP 0.3.0**. The 11-tool surface is unchanged. Check the
15
- [release notes](https://github.com/f-tuma/vardum/blob/main/docs/client-release-0.7.md)
16
- and [release status and compatibility](https://github.com/f-tuma/vardum/blob/main/docs/npm-release.md).
17
- The published 0.1.0 artifact has a broken workspace dependency.
16
+ The server speaks MCP over stdio, so the client starts it. Create a workspace
17
+ API key in **Settings → API keys** first; it is restricted to that workspace
18
+ and its selected permissions. Copy the workspace ID from Settings → General
19
+ for tool calls. Use Node.js 22 or newer.
18
20
 
19
- The configurations below pin **0.3.0**. Verify its availability in your registry
20
- before installing, or build from source with
21
- `pnpm --filter "@vardum/mcp..." build` and configure the client to run
22
- `node /absolute/path/to/vardum/apps/mcp/dist/index.js`.
21
+ These configurations pin **MCP 0.4.1** and need no source-repository access.
22
+ Deploy a server advertising `vardEditRevision: 1` from `GET /api/capabilities`
23
+ before editable Vard reads or mutations; there is no legacy-edit fallback.
24
+ Use a read key for inspection or a key with the required write permissions for
25
+ mutation. Keep real keys in the client's private configuration, never in Git.
23
26
 
24
27
  Claude Code:
25
28
 
26
29
  ```sh
27
- claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp@0.3.0
30
+ claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp@0.4.1
28
31
  ```
29
32
 
30
33
  Any client that reads a JSON config (Claude Desktop, Cursor, …):
@@ -34,7 +37,7 @@ Any client that reads a JSON config (Claude Desktop, Cursor, …):
34
37
  "mcpServers": {
35
38
  "vardum": {
36
39
  "command": "npx",
37
- "args": ["-y", "@vardum/mcp@0.3.0"],
40
+ "args": ["-y", "@vardum/mcp@0.4.1"],
38
41
  "env": { "VARDUM_API_KEY": "varda_..." }
39
42
  }
40
43
  }
@@ -49,12 +52,11 @@ Any client that reads a JSON config (Claude Desktop, Cursor, …):
49
52
  | Workspace API key | `--api-key KEY` | `VARDUM_API_KEY` | — (required) |
50
53
 
51
54
  Point `--server` at your own host when self-hosting. `vardum-mcp --version` and
52
- `--help` answer without needing a key, which is the quickest way to check that a
53
- client can start the binary at all.
55
+ `--help` answer without needing a key.
54
56
 
55
57
  ## Tools
56
58
 
57
- The 11 tools are:
59
+ The 11 tools remain:
58
60
 
59
61
  | Area | Tools |
60
62
  | --- | --- |
@@ -62,74 +64,114 @@ The 11 tools are:
62
64
  | Vards | `list_vards`, `create_vard`, `get_vard`, `update_vard`, `delete_vard`, `replace_vard_items` |
63
65
 
64
66
  A read key can list/read. A write key also permits create, update, delete and
65
- publish. Tools for browser-only administration were removed in 0.2.0; creating
66
- an asset key as an owner does not make it an administration credential.
67
+ publish within its scope. `update_vard` and `replace_vard_items` require API read
68
+ and update permission because they return the full definition. `delete_vard`
69
+ requires delete permission. Tools for browser-only administration were removed
70
+ in 0.2.0; an owner-created workspace key is still not an administration credential.
67
71
 
68
72
  `list_assets` returns `{ workspaceSlug, assets, nextCursor }`. It lists ready
69
73
  assets in pages of 100 by default (`limit`: 1–500); pass `nextCursor` as `cursor`
70
- until it is null. Pending/failed versions are not listed.
71
-
72
- `replace_vard_items` pins with `versionId: "<ready-version-uuid>"` and
73
- `writable: false`. Omit `versionId` or send null to follow latest. The old
74
- `pinned` boolean is rejected: it never identified a specific version for the API.
75
-
76
- Images and video are accepted; video is stored and delivered as-is, and cannot
77
- be resized or transformed into variants.
74
+ until it is null. Pending/failed versions are not listed. Images and video are
75
+ accepted; video is stored and delivered as-is without variants.
78
76
 
79
77
  ## Asset visibility
80
78
 
81
79
  New assets are private. `list_assets` reports `isPublic` for each ready asset.
82
80
  Use `set_asset_visibility` with `workspaceId`, `assetId`, and an explicit boolean
83
- `isPublic` to publish or make an asset private. The API requires asset publish
84
- permission; read-only keys cannot change visibility.
81
+ `isPublic` to publish or make an asset private. This requires publish permission.
85
82
 
86
83
  Publishing exposes every ready version and its variants. A public Vard does not
87
84
  publish its files: its anonymous manifest remains unavailable while any mapped
88
85
  asset is private. Making a file private cannot revoke downloaded or cached
89
- copies. These tools require the matching visibility-capable API in the compatibility table.
86
+ copies. The server must implement asset visibility, enforce workspace read access to
87
+ private version/variant URLs, and require publish permission for visibility changes.
88
+ Installing this package does not add those backend capabilities.
89
+
90
+ ## Guarded editing
91
+
92
+ Read `get_vard` first. Its editable definition contains `revision`, saved
93
+ `folderRules` and **manual mappings only**, including stable mapping `id` values
94
+ and literal `pinnedVersionId`. A separate `get_vard` with `resolved: true` returns
95
+ a live preview without an editor revision. It can fail on collisions or limits
96
+ without blocking configuration inspection or repair. Never persist resolved
97
+ folder outputs as manual mappings.
98
+
99
+ `update_vard`, `replace_vard_items` and `delete_vard` require the caller's
100
+ **original `expectedRevision`**. It is an opaque `v1:<64 lowercase hex digits>`
101
+ equality token for saved state, not a counter. Keep it unchanged. After a
102
+ successful update or replacement, retain the full returned definition and its
103
+ revision for the next edit.
104
+
105
+ `replace_vard_items` takes the complete intended manual list. Each item must
106
+ contain `id` (its existing manual UUID, or `null` for a new mapping), `assetId`,
107
+ `outputPath`, `writable`, `transformation` (object or `null`) and an explicit
108
+ `versionId` (UUID or `null`). Keep existing IDs; omitting an existing item removes
109
+ it. Copy each saved `pinnedVersionId` into `versionId` literally, including an
110
+ unavailable saved pin. A resolved delivery `versionId` is not the saved pin.
111
+ `null` follows latest; pinned items must be read-only. Omitting `id` or
112
+ `versionId`, or supplying the old `pinned` boolean, is invalid.
113
+
114
+ These editor tools check `vardEditRevision: 1` and use dedicated `/editor`
115
+ endpoints. An unsupported server, missing revision or missing editor endpoint
116
+ stops the operation; there is no fallback to a legacy mutation.
117
+
118
+ A stale write returns a structured MCP tool error (`isError: true`) with
119
+ `status: 409`, `code: "vard_revision_conflict"`,
120
+ `error: "Vard changed; reload before saving."`, `currentRevision` and an
121
+ instruction to reload. Keep the intended edit, read `get_vard` again, review the
122
+ current definition, then reapply the edit with that reviewed definition's
123
+ revision. Do not automatically repeat a stale replacement list with a new token.
124
+ An unresolved preview never permits bypassing the revision check or writing
125
+ expanded folder outputs as manual mappings.
90
126
 
91
127
  ## Folder rules
92
128
 
93
- MCP 0.3.0 retains the folder-rule behavior introduced in 0.2.0 and requires the
94
- matching folder-aware API.
95
- `get_vard` with `workspaceId` and `vardId` returns editable configuration:
96
- `items` contains **manual mappings only**, and `folderRules` contains saved rules.
97
- For a separate current live preview use `get_vard` with `resolved: true`; this is
98
- not a committed frozen snapshot and can fail on collisions or limits without
99
- blocking configuration inspection or repair.
100
-
101
- `create_vard` accepts `name`, optional `isPublic` (defaults private), and optional
129
+ `create_vard` accepts `name`, optional `isPublic` (defaults private) and optional
102
130
  `folderRules` (defaults empty); it does not require a slug. `update_vard` changes
103
- only supplied fields. For example, call it with:
131
+ only supplied fields. After reading `get_vard`, update rules with:
104
132
 
105
133
  ```json
106
134
  {
107
135
  "workspaceId": "your-workspace-id",
108
136
  "vardId": "your-vard-id",
137
+ "expectedRevision": "v1:<copy the original revision from get_vard>",
109
138
  "folderRules": [{ "sourcePrefix": "brand/", "outputPrefix": "assets/brand/" }]
110
139
  }
111
140
  ```
112
141
 
113
- This preserves the Vard's name, public flag and manual item IDs. Send
114
- `folderRules: []` to remove all rules. `replace_vard_items` replaces only manual
115
- mappings and does not change rules: **never copy resolved folder outputs into it**.
116
- The API remains authoritative for workspace permissions and validation; tools
117
- forward validation, authorization and resolution errors.
142
+ Replace the illustrative token with the unchanged revision from your read. This
143
+ preserves the Vard's name, public flag and manual mappings. Send `folderRules: []`
144
+ to remove all rules. `replace_vard_items` preserves rules.
118
145
 
119
146
  Rules include ready current originals recursively: `brand/icons/logo.svg` becomes
120
147
  `assets/brand/icons/logo.svg`. Generated outputs are read-only, unpinned and
121
- untransformed; pending files are excluded until ready. Empty prefixes mean workspace
122
- or project root. Empty/nonexistent folders are valid. Prefixes must be safe literal
123
- relative directories ending in `/` (or empty), at most 255 characters; no globs or
124
- excludes. Maximum 64 rules and 1,000 total manual + generated outputs, with paths
125
- up to 500 characters. Explicit + rule and rule + rule collisions fail, including
126
- case/Unicode equivalents and file/directory overlaps; no precedence or truncation.
127
- A new private folder member makes the entire anonymous public manifest unavailable.
128
-
129
- Normal `vardum pull` intentionally adopts membership changes; `--frozen` consumes
130
- the committed snapshot without live resolution. There is no background sync.
131
- Removing rules leaves source assets/manual mappings intact. Choose `--delete` on
132
- the membership-updating pull: a normal no-delete pull keeps stale disk files but
133
- drops their tracking, so a later `--delete` cannot remove them; deliberate manual
134
- cleanup is then required. See [CLI snapshot limitations](https://github.com/f-tuma/vardum/tree/main/apps/cli#reproducible-asset-consumption)
135
- and [operations](https://github.com/f-tuma/vardum/blob/main/docs/operations.md#folder-rule-upgrade) before upgrading or rolling back.
148
+ untransformed; pending files are excluded until ready. Empty prefixes mean
149
+ workspace or project root. Empty/nonexistent folders are valid. Prefixes are
150
+ safe literal relative directories ending in `/` (or empty), at most 255
151
+ characters; no globs or excludes. Maximum 64 rules and 1,000 total manual +
152
+ generated outputs, with paths up to 500 characters. Explicit + rule and rule +
153
+ rule collisions fail, including case/Unicode equivalents and file/directory
154
+ overlaps. A new private member blocks the entire anonymous public manifest.
155
+
156
+ Normal `vardum pull` intentionally adopts membership changes; `--frozen`
157
+ consumes the committed snapshot without live resolution. There is no background
158
+ sync. Removing rules leaves source assets/manual mappings intact. Choose
159
+ `--delete` on the membership-updating pull: a normal no-delete pull keeps stale
160
+ disk files but drops their tracking, so a later `--delete` cannot remove them.
161
+ The CLI snapshot records locked URLs and byte checksums, not an archive. Frozen
162
+ restore needs those versions and URLs to remain reachable with the required read
163
+ permission. Source-key or workspace-slug renames, deleted versions, signing-key
164
+ rotation, expired signatures or changed transformation bytes can break an old
165
+ snapshot; checksum failure never falls back to latest. Do not assume a rule removal
166
+ or a successful live preview proves historical frozen restores.
167
+
168
+ ## Self-hosted backend
169
+
170
+ The matching backend exposes `GET /api/capabilities` with `vardEditRevision: 1`
171
+ and dedicated `/editor` endpoints. Guarded editing needs no migration beyond the
172
+ upload/import baseline through `0015_upload_request_checksum.sql`. Folder rules
173
+ need recursive ready-original resolution with the collision/limit rules above.
174
+ Asset uploads need `GET /api/upload-limits`, accepted MIME validation and ready
175
+ version processing. Images and videos have separate byte limits; video delivery
176
+ keeps original bytes without transformation variants. Verify the actual instance
177
+ before granting an agent write permissions.
package/dist/index.js CHANGED
@@ -53,6 +53,19 @@ const headers = {
53
53
  };
54
54
  if (API_KEY)
55
55
  headers["x-api-key"] = API_KEY;
56
+ class VardumHttpError extends Error {
57
+ responseError;
58
+ status;
59
+ code;
60
+ currentRevision;
61
+ constructor(responseError, status, code, currentRevision) {
62
+ super(`${responseError} (${status})`);
63
+ this.responseError = responseError;
64
+ this.status = status;
65
+ this.code = code;
66
+ this.currentRevision = currentRevision;
67
+ }
68
+ }
56
69
  async function api(path, init = {}) {
57
70
  const merged = new Headers(init.headers);
58
71
  for (const [k, v] of Object.entries(headers))
@@ -68,14 +81,20 @@ async function api(path, init = {}) {
68
81
  const text = await res.text();
69
82
  if (!res.ok) {
70
83
  let msg = text || res.statusText;
84
+ let code;
85
+ let currentRevision;
71
86
  try {
72
87
  const parsed = JSON.parse(text);
88
+ if (typeof parsed.code === "string")
89
+ code = parsed.code;
90
+ if (typeof parsed.currentRevision === "string")
91
+ currentRevision = parsed.currentRevision;
73
92
  msg = parsed.message ?? parsed.error ?? msg;
74
93
  }
75
94
  catch {
76
95
  /* use body text */
77
96
  }
78
- throw new Error(`${msg} (${res.status})`);
97
+ throw new VardumHttpError(msg, res.status, code, currentRevision);
79
98
  }
80
99
  try {
81
100
  return JSON.parse(text);
@@ -84,6 +103,57 @@ async function api(path, init = {}) {
84
103
  throw new Error("Invalid JSON response");
85
104
  }
86
105
  }
106
+ const vardRevision = (value) => typeof value === "string" && /^v1:[a-f0-9]{64}$/.test(value);
107
+ const editorUuid = (value) => typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
108
+ async function requireVardEditRevision() {
109
+ const capabilities = await api("/api/capabilities");
110
+ if (!capabilities || typeof capabilities !== "object" ||
111
+ capabilities.vardEditRevision !== 1)
112
+ throw new Error("This server does not support revision-guarded Vard editing (vardEditRevision: 1); upgrade the server before editing Vards");
113
+ }
114
+ function editorPath(workspaceId, vardId) {
115
+ return `/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}/editor`;
116
+ }
117
+ function validateVardEditor(value) {
118
+ if (!value || typeof value !== "object" || Array.isArray(value))
119
+ throw new Error("Invalid Vard editor definition; upgrade the server and reload before editing");
120
+ const definition = value;
121
+ if (definition.schema !== 1 || !vardRevision(definition.revision) ||
122
+ typeof definition.id !== "string" || typeof definition.workspaceId !== "string" ||
123
+ typeof definition.name !== "string" || typeof definition.isPublic !== "boolean" ||
124
+ typeof definition.updatedAt !== "string" || !Array.isArray(definition.folderRules) ||
125
+ !Array.isArray(definition.items) || definition.items.some((item) => {
126
+ if (!item || typeof item !== "object" || Array.isArray(item))
127
+ return true;
128
+ const mapping = item;
129
+ return !editorUuid(mapping.id) || !(mapping.pinnedVersionId === null || editorUuid(mapping.pinnedVersionId));
130
+ }))
131
+ throw new Error("Invalid Vard editor definition or missing revision/literal pins; upgrade the server and reload before editing");
132
+ return value;
133
+ }
134
+ async function editorResult(action) {
135
+ try {
136
+ const data = await action();
137
+ return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] };
138
+ }
139
+ catch (error) {
140
+ if (error instanceof VardumHttpError && error.status === 409 && error.code === "vard_revision_conflict") {
141
+ const data = {
142
+ status: error.status,
143
+ code: error.code,
144
+ error: error.responseError,
145
+ currentRevision: error.currentRevision,
146
+ instruction: "Reload with get_vard, review the current definition, and reapply the intended edit with its revision. Do not retry the stale save.",
147
+ };
148
+ return {
149
+ isError: true,
150
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
151
+ structuredContent: data,
152
+ };
153
+ }
154
+ throw error;
155
+ }
156
+ }
87
157
  // ── Server ─────────────────────────────────────────────────────────────────
88
158
  const server = new McpServer({
89
159
  name: "Vardum",
@@ -128,6 +198,12 @@ read-only, unpinned, and untransformed. No globs or excludes. Manual + generated
128
198
  not collide (including case/Unicode or file/directory overlaps) and total at most 1000.
129
199
  get_vard returns editable manual items and folderRules, even if resolution fails. Use
130
200
  get_vard with resolved=true for a separate live preview; never persist expanded items.
201
+ get_vard supplies an opaque revision and literal pinnedVersionId for each manual item.
202
+ update_vard, replace_vard_items and delete_vard require the original expectedRevision; a stale
203
+ revision returns vard_revision_conflict. Reload and review before reapplying; never retry blindly.
204
+ replace_vard_items requires each existing manual UUID or null for a new item. Copy the stored
205
+ pinnedVersionId to versionId, including unavailable pins; resolved versionId is not the saved pin.
206
+ These editor tools require vardEditRevision: 1; unsupported servers are refused without legacy writes.
131
207
  update_vard replaces only supplied fields; folderRules: [] removes rules without changing manual items.
132
208
  A new private folder member makes the whole anonymous public manifest unavailable.
133
209
  Normal vardum pull intentionally updates membership; --frozen consumes the committed snapshot.
@@ -366,50 +442,71 @@ server.tool("create_vard", "Create a Vard with optional recursive folder rules,
366
442
  });
367
443
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
368
444
  });
369
- server.tool("get_vard", "Retrieve editable Vard configuration (manual items and folderRules), even when expansion fails. Set resolved=true for a separate live manifest preview, not a committed frozen snapshot. Never write resolved items back as manual items.", {
445
+ const expectedRevisionInput = z
446
+ .string()
447
+ .regex(/^v1:[a-f0-9]{64}$/)
448
+ .describe("Original opaque revision from get_vard; reload and review after a conflict before reapplying the edit");
449
+ server.tool("get_vard", "Retrieve editable Vard configuration with its revision, manual item IDs and literal pinnedVersionId, even when expansion fails. Requires vardEditRevision: 1. Set resolved=true for a separate live manifest preview without an editing revision; never write resolved items back as manual items.", {
370
450
  workspaceId: z.string().describe("Workspace ID"),
371
451
  vardId: z.string().describe("Vard ID"),
372
452
  resolved: z
373
453
  .boolean()
374
454
  .optional()
375
- .describe("True: resolve current outputs, which may fail on collisions/limits. Omitted/false: editable configuration only."),
455
+ .describe("True: resolve current outputs, which may fail on collisions/limits. Omitted/false: editable definition and revision only."),
376
456
  }, async ({ workspaceId, vardId, resolved }) => {
377
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}${resolved ? "/manifest" : ""}`);
457
+ if (resolved) {
458
+ const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}/manifest`);
459
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
460
+ }
461
+ await requireVardEditRevision();
462
+ const data = validateVardEditor(await api(editorPath(workspaceId, vardId)));
378
463
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
379
464
  });
380
- server.tool("update_vard", "Update only supplied metadata or folderRules; omitted fields and manual item IDs are preserved. folderRules: [] removes rules. Saving valid configuration may still leave a collision/limit error in the live preview; repair rules or manual paths. Requires API update permission.", {
465
+ server.tool("update_vard", "Update supplied metadata or folderRules using the caller's original expectedRevision; returns the full manual-only definition and revision. Omitted fields and manual IDs are preserved; folderRules: [] removes rules. Requires API read+update permission and vardEditRevision: 1. On conflict reload, review and reapply the intended edit; never retry a stale save automatically.", {
381
466
  workspaceId: z.string().describe("Workspace ID"),
382
467
  vardId: z.string().describe("Vard ID"),
468
+ expectedRevision: expectedRevisionInput,
383
469
  name: z.string().min(1).max(80).optional().describe("New display name"),
384
470
  isPublic: z
385
471
  .boolean()
386
472
  .optional()
387
473
  .describe("Public manifest only; does not publish resolved assets"),
388
474
  folderRules: folderRulesInput.optional(),
389
- }, async ({ workspaceId, vardId, name, isPublic, folderRules }) => {
390
- const body = {};
475
+ }, async ({ workspaceId, vardId, expectedRevision, name, isPublic, folderRules }) => editorResult(async () => {
476
+ const body = { expectedRevision };
391
477
  if (name !== undefined)
392
478
  body.name = name;
393
479
  if (isPublic !== undefined)
394
480
  body.isPublic = isPublic;
395
481
  if (folderRules !== undefined)
396
482
  body.folderRules = folderRules;
397
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "PATCH", body: JSON.stringify(body) });
398
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
399
- });
400
- server.tool("delete_vard", "Delete a Vard and all its output configurations permanently.", {
483
+ await requireVardEditRevision();
484
+ return validateVardEditor(await api(editorPath(workspaceId, vardId), {
485
+ method: "PATCH", body: JSON.stringify(body),
486
+ }));
487
+ }));
488
+ server.tool("delete_vard", "Delete a Vard and all output configurations permanently using the caller's original expectedRevision. Requires API delete permission and vardEditRevision: 1. On conflict reload and review before deciding to delete again.", {
401
489
  workspaceId: z.string().describe("Workspace ID"),
402
490
  vardId: z.string().describe("Vard ID"),
403
- }, async ({ workspaceId, vardId }) => {
404
- await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "DELETE" });
405
- return { content: [{ type: "text", text: "Vard deleted." }] };
406
- });
407
- server.tool("replace_vard_items", "Replace only manual items in a Vard; folderRules are unchanged. Never pass expanded folder outputs here. Each manual item maps an asset to an output path and optional transformation settings.", {
491
+ expectedRevision: expectedRevisionInput,
492
+ }, async ({ workspaceId, vardId, expectedRevision }) => editorResult(async () => {
493
+ await requireVardEditRevision();
494
+ const query = new URLSearchParams({ expectedRevision });
495
+ await api(editorPath(workspaceId, vardId) + `?${query}`, { method: "DELETE" });
496
+ return "Vard deleted.";
497
+ }));
498
+ server.tool("replace_vard_items", "Replace manual items using the caller's original expectedRevision; returns the full definition and revision. Keep existing manual IDs, use null for new IDs, and copy pinnedVersionId to versionId literally, including unavailable pins; never infer a pin from resolved versionId or pass expanded folder outputs. Requires API read+update permission and vardEditRevision: 1. On conflict reload, review and reapply the intended edit; never retry a stale list automatically.", {
408
499
  workspaceId: z.string().describe("Workspace ID"),
409
500
  vardId: z.string().describe("Vard ID"),
501
+ expectedRevision: expectedRevisionInput,
410
502
  items: z
411
503
  .array(z
412
504
  .object({
505
+ id: z
506
+ .string()
507
+ .uuid()
508
+ .nullable()
509
+ .describe("Existing manual item UUID to preserve identity, or null to create a new manual item"),
413
510
  assetId: z.string().describe("The asset ID to include"),
414
511
  outputPath: z
415
512
  .string()
@@ -421,19 +518,20 @@ server.tool("replace_vard_items", "Replace only manual items in a Vard; folderRu
421
518
  .string()
422
519
  .uuid()
423
520
  .nullable()
424
- .optional()
425
- .describe("Exact ready version ID to pin; null/omitted follows latest. Pinned outputs must not be writable."),
521
+ .describe("Literal saved pin: copy pinnedVersionId from get_vard, never resolved versionId. Null follows latest. Pinned outputs must not be writable."),
426
522
  transformation: z
427
523
  .record(z.unknown())
428
524
  .nullable()
429
525
  .describe("Optional transformation parameters (w, h, fit, format, q) or null"),
430
526
  })
431
527
  .strict())
432
- .describe("Complete list of manual items to set (replaces manual items only)"),
433
- }, async ({ workspaceId, vardId, items }) => {
434
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}/items`, { method: "PUT", body: JSON.stringify({ items }) });
435
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
436
- });
528
+ .describe("Complete manual item list only; folderRules are preserved"),
529
+ }, async ({ workspaceId, vardId, expectedRevision, items }) => editorResult(async () => {
530
+ await requireVardEditRevision();
531
+ return validateVardEditor(await api(editorPath(workspaceId, vardId) + "/items", {
532
+ method: "PUT", body: JSON.stringify({ expectedRevision, items }),
533
+ }));
534
+ }));
437
535
  // ── Main ────────────────────────────────────────────────────────────────────
438
536
  async function main() {
439
537
  if (!API_KEY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vardum/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for workspace-scoped Vardum assets and Vards",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/sdk": "1.31.0",
22
- "@vardum/shared": "^0.3.0",
22
+ "@vardum/shared": "^0.4.1",
23
23
  "zod": "^3.24.0"
24
24
  },
25
25
  "devDependencies": {
@@ -31,7 +31,7 @@
31
31
  "url": "git+https://github.com/f-tuma/vardum.git",
32
32
  "directory": "apps/mcp"
33
33
  },
34
- "homepage": "https://github.com/f-tuma/vardum/tree/main/apps/mcp#readme",
34
+ "homepage": "https://vardum.com",
35
35
  "bugs": {
36
36
  "url": "https://github.com/f-tuma/vardum/issues"
37
37
  },