@vardum/mcp 0.3.0 → 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 +91 -62
  2. package/dist/index.js +121 -23
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,30 +1,32 @@
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.0**, including revision-guarded Vard editing.
8
+ Check the [0.8 client release notes](https://github.com/f-tuma/vardum/blob/main/docs/client-release-0.8.md)
9
+ and [release status and compatibility](https://github.com/f-tuma/vardum/blob/main/docs/npm-release.md)
10
+ to confirm npm availability before installing this version. Published MCP 0.3.0
11
+ uses the legacy editing contract; installing it does not enable revision guards.
12
+ The published 0.1.0 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 target MCP 0.4.0**; use them once the release-status page
22
+ confirms that exact package is available. Deploy the matching server advertising
23
+ `vardEditRevision: 1` before editing Vards. For current published setup, see the
24
+ [MCP guide](../../docs/guide/mcp.md).
23
25
 
24
26
  Claude Code:
25
27
 
26
28
  ```sh
27
- claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp@0.3.0
29
+ claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp@0.4.0
28
30
  ```
29
31
 
30
32
  Any client that reads a JSON config (Claude Desktop, Cursor, …):
@@ -34,13 +36,17 @@ Any client that reads a JSON config (Claude Desktop, Cursor, …):
34
36
  "mcpServers": {
35
37
  "vardum": {
36
38
  "command": "npx",
37
- "args": ["-y", "@vardum/mcp@0.3.0"],
39
+ "args": ["-y", "@vardum/mcp@0.4.0"],
38
40
  "env": { "VARDUM_API_KEY": "varda_..." }
39
41
  }
40
42
  }
41
43
  }
42
44
  ```
43
45
 
46
+ For a source checkout, build with `pnpm --filter "@vardum/mcp..." build` and
47
+ configure the client to run `node /absolute/path/to/vardum/apps/mcp/dist/index.js`.
48
+ This does not establish npm availability.
49
+
44
50
  ## Configuration
45
51
 
46
52
  | Setting | Flag | Environment | Default |
@@ -49,12 +55,11 @@ Any client that reads a JSON config (Claude Desktop, Cursor, …):
49
55
  | Workspace API key | `--api-key KEY` | `VARDUM_API_KEY` | — (required) |
50
56
 
51
57
  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.
58
+ `--help` answer without needing a key.
54
59
 
55
60
  ## Tools
56
61
 
57
- The 11 tools are:
62
+ The 11 tools remain:
58
63
 
59
64
  | Area | Tools |
60
65
  | --- | --- |
@@ -62,74 +67,98 @@ The 11 tools are:
62
67
  | Vards | `list_vards`, `create_vard`, `get_vard`, `update_vard`, `delete_vard`, `replace_vard_items` |
63
68
 
64
69
  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.
70
+ publish within its scope. `update_vard` and `replace_vard_items` require API read
71
+ and update permission because they return the full definition. `delete_vard`
72
+ requires delete permission. Tools for browser-only administration were removed
73
+ in 0.2.0; an owner-created workspace key is still not an administration credential.
67
74
 
68
75
  `list_assets` returns `{ workspaceSlug, assets, nextCursor }`. It lists ready
69
76
  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.
77
+ until it is null. Pending/failed versions are not listed. Images and video are
78
+ accepted; video is stored and delivered as-is without variants.
78
79
 
79
80
  ## Asset visibility
80
81
 
81
82
  New assets are private. `list_assets` reports `isPublic` for each ready asset.
82
83
  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.
84
+ `isPublic` to publish or make an asset private. This requires publish permission.
85
85
 
86
86
  Publishing exposes every ready version and its variants. A public Vard does not
87
87
  publish its files: its anonymous manifest remains unavailable while any mapped
88
88
  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.
89
+ copies. These tools require the matching visibility-capable API in the
90
+ compatibility table.
91
+
92
+ ## Guarded editing
93
+
94
+ Read `get_vard` first. Its editable definition contains `revision`, saved
95
+ `folderRules` and **manual mappings only**, including stable mapping `id` values
96
+ and literal `pinnedVersionId`. A separate `get_vard` with `resolved: true` returns
97
+ a live preview without an editor revision. It can fail on collisions or limits
98
+ without blocking configuration inspection or repair. Never persist resolved
99
+ folder outputs as manual mappings.
100
+
101
+ `update_vard`, `replace_vard_items` and `delete_vard` require the caller's
102
+ **original `expectedRevision`**. It is an opaque `v1:<64 lowercase hex digits>`
103
+ equality token for saved state, not a counter. Keep it unchanged. After a
104
+ successful update or replacement, retain the full returned definition and its
105
+ revision for the next edit.
106
+
107
+ `replace_vard_items` takes the complete intended manual list. Each item must
108
+ contain `id` (its existing manual UUID, or `null` for a new mapping), `assetId`,
109
+ `outputPath`, `writable`, `transformation` (object or `null`) and an explicit
110
+ `versionId` (UUID or `null`). Keep existing IDs; omitting an existing item removes
111
+ it. Copy each saved `pinnedVersionId` into `versionId` literally, including an
112
+ unavailable saved pin. A resolved delivery `versionId` is not the saved pin.
113
+ `null` follows latest; pinned items must be read-only. Omitting `id` or
114
+ `versionId`, or supplying the old `pinned` boolean, is invalid.
115
+
116
+ These editor tools check `vardEditRevision: 1` and use dedicated `/editor`
117
+ endpoints. An unsupported server, missing revision or missing editor endpoint
118
+ stops the operation; there is no fallback to a legacy mutation.
119
+
120
+ A stale write returns a structured MCP tool error (`isError: true`) with
121
+ `status: 409`, `code: "vard_revision_conflict"`,
122
+ `error: "Vard changed; reload before saving."`, `currentRevision` and an
123
+ instruction to reload. Keep the intended edit, read `get_vard` again, review the
124
+ current definition, then reapply the edit with that reviewed definition's
125
+ revision. Do not automatically repeat a stale replacement list with a new token.
126
+ See the [guarded editor protocol](../../docs/vard-editor-concurrency.md).
90
127
 
91
128
  ## Folder rules
92
129
 
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
130
+ `create_vard` accepts `name`, optional `isPublic` (defaults private) and optional
102
131
  `folderRules` (defaults empty); it does not require a slug. `update_vard` changes
103
- only supplied fields. For example, call it with:
132
+ only supplied fields. After reading `get_vard`, update rules with:
104
133
 
105
134
  ```json
106
135
  {
107
136
  "workspaceId": "your-workspace-id",
108
137
  "vardId": "your-vard-id",
138
+ "expectedRevision": "v1:<copy the original revision from get_vard>",
109
139
  "folderRules": [{ "sourcePrefix": "brand/", "outputPrefix": "assets/brand/" }]
110
140
  }
111
141
  ```
112
142
 
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.
143
+ Replace the illustrative token with the unchanged revision from your read. This
144
+ preserves the Vard's name, public flag and manual mappings. Send `folderRules: []`
145
+ to remove all rules. `replace_vard_items` preserves rules.
118
146
 
119
147
  Rules include ready current originals recursively: `brand/icons/logo.svg` becomes
120
148
  `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.
149
+ untransformed; pending files are excluded until ready. Empty prefixes mean
150
+ workspace or project root. Empty/nonexistent folders are valid. Prefixes are
151
+ safe literal relative directories ending in `/` (or empty), at most 255
152
+ characters; no globs or excludes. Maximum 64 rules and 1,000 total manual +
153
+ generated outputs, with paths up to 500 characters. Explicit + rule and rule +
154
+ rule collisions fail, including case/Unicode equivalents and file/directory
155
+ overlaps. A new private member blocks the entire anonymous public manifest.
156
+
157
+ Normal `vardum pull` intentionally adopts membership changes; `--frozen`
158
+ consumes the committed snapshot without live resolution. There is no background
159
+ sync. Removing rules leaves source assets/manual mappings intact. Choose
160
+ `--delete` on the membership-updating pull: a normal no-delete pull keeps stale
161
+ disk files but drops their tracking, so a later `--delete` cannot remove them.
162
+ See [CLI snapshot limitations](https://github.com/f-tuma/vardum/tree/main/apps/cli#reproducible-asset-consumption)
163
+ and [operations](https://github.com/f-tuma/vardum/blob/main/docs/operations.md#folder-rule-upgrade)
164
+ before upgrading or rolling back.
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.0",
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.0",
23
23
  "zod": "^3.24.0"
24
24
  },
25
25
  "devDependencies": {