@vardum/mcp 0.1.0 → 0.3.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -10
  3. package/dist/index.js +117 -171
  4. package/package.json +19 -10
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vardum contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,19 +1,30 @@
1
1
  # @vardum/mcp
2
2
 
3
3
  MCP server for [Vardum](https://vardum.com): lets an AI agent manage assets,
4
- Vards, workspaces, members, API keys and custom domains through the same API
5
- the dashboard and the CLI use.
4
+ Vards using a key scoped to one workspace. Workspace creation, membership,
5
+ API-key management and domains are managed in the web app with a signed-in session.
6
6
 
7
7
  ## Setup
8
8
 
9
9
  The server speaks MCP over stdio, so the client starts it — there is nothing to
10
10
  run by hand. Create a workspace API key in **Settings // API keys** first; the
11
- key carries the permissions of whoever created it.
11
+ key is restricted to that workspace and its selected read/write permissions.
12
+ Copy the workspace ID from Settings → General for tool calls.
13
+
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.
18
+
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`.
12
23
 
13
24
  Claude Code:
14
25
 
15
26
  ```sh
16
- claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp
27
+ claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp@0.3.0
17
28
  ```
18
29
 
19
30
  Any client that reads a JSON config (Claude Desktop, Cursor, …):
@@ -23,7 +34,7 @@ Any client that reads a JSON config (Claude Desktop, Cursor, …):
23
34
  "mcpServers": {
24
35
  "vardum": {
25
36
  "command": "npx",
26
- "args": ["-y", "@vardum/mcp"],
37
+ "args": ["-y", "@vardum/mcp@0.3.0"],
27
38
  "env": { "VARDUM_API_KEY": "varda_..." }
28
39
  }
29
40
  }
@@ -43,11 +54,82 @@ client can start the binary at all.
43
54
 
44
55
  ## Tools
45
56
 
46
- Assets (`list_assets`, `create_asset`, `upload_asset_version`, `delete_asset`),
47
- Vards (`list_vards`, `create_vard`, `get_vard`, `update_vard`, `delete_vard`,
48
- `replace_vard_items`), workspaces (`list_organizations`, `create_workspace`,
49
- `get_workspace`, `update_workspace`), members and invitations, API keys, and
50
- custom domains (`get_domain`, `configure_domain`, `verify_domain`).
57
+ The 11 tools are:
58
+
59
+ | Area | Tools |
60
+ | --- | --- |
61
+ | Assets | `list_assets`, `create_asset`, `upload_asset_version`, `set_asset_visibility`, `delete_asset` |
62
+ | Vards | `list_vards`, `create_vard`, `get_vard`, `update_vard`, `delete_vard`, `replace_vard_items` |
63
+
64
+ 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
+
68
+ `list_assets` returns `{ workspaceSlug, assets, nextCursor }`. It lists ready
69
+ 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.
51
75
 
52
76
  Images and video are accepted; video is stored and delivered as-is, and cannot
53
77
  be resized or transformed into variants.
78
+
79
+ ## Asset visibility
80
+
81
+ New assets are private. `list_assets` reports `isPublic` for each ready asset.
82
+ 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.
85
+
86
+ Publishing exposes every ready version and its variants. A public Vard does not
87
+ publish its files: its anonymous manifest remains unavailable while any mapped
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.
90
+
91
+ ## Folder rules
92
+
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
102
+ `folderRules` (defaults empty); it does not require a slug. `update_vard` changes
103
+ only supplied fields. For example, call it with:
104
+
105
+ ```json
106
+ {
107
+ "workspaceId": "your-workspace-id",
108
+ "vardId": "your-vard-id",
109
+ "folderRules": [{ "sourcePrefix": "brand/", "outputPrefix": "assets/brand/" }]
110
+ }
111
+ ```
112
+
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.
118
+
119
+ Rules include ready current originals recursively: `brand/icons/logo.svg` becomes
120
+ `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.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { supportedMimeTypes } from "@vardum/shared";
5
+ import { supportedMimeTypes, } from "@vardum/shared";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { z } from "zod";
@@ -87,7 +87,7 @@ async function api(path, init = {}) {
87
87
  // ── Server ─────────────────────────────────────────────────────────────────
88
88
  const server = new McpServer({
89
89
  name: "Vardum",
90
- version: "0.1.0",
90
+ version: VERSION,
91
91
  });
92
92
  // ── Resources ──────────────────────────────────────────────────────────────
93
93
  server.resource("constraints", "vardum://constraints", () => ({
@@ -97,7 +97,7 @@ server.resource("constraints", "vardum://constraints", () => ({
97
97
  text: `# Vardum Constraints
98
98
 
99
99
  ## Slug immutability
100
- Organization, workspace and Vard slugs are set at creation time and **cannot be changed**.
100
+ Organization and workspace slugs are set at creation time and **cannot be changed**.
101
101
  Each slug must match \`[a-z0-9][a-z0-9._-]*\`.
102
102
 
103
103
  ## Asset key rules
@@ -111,9 +111,29 @@ Every asset has a unique key scoped to a workspace. Rules:
111
111
  - Image types: AVIF, GIF, JPEG, PNG, SVG, TIFF, WebP
112
112
  - Video types: MP4, WebM, MOV, AVI, MPEG
113
113
  - Max 100 megapixels per image; videos are stored as-is (no transcode)
114
- - Storage backend caps at 100 MiB per upload
114
+ - Per-upload size ceiling is set by the server, and differs for images and videos (the API rejects anything over it)
115
115
  - Video assets cannot be resized or transformed into variants (images only)
116
116
 
117
+ ## Asset visibility
118
+ New assets are private. Use set_asset_visibility with publish permission to change visibility.
119
+ Public assets expose every ready version and its variants without authentication.
120
+ A public Vard does not publish its assets: anonymous manifests require all mapped assets to be public.
121
+ Making an asset private cannot revoke previously downloaded or cached copies.
122
+
123
+ ## Folder inclusion
124
+ Vards support up to 64 literal recursive sourcePrefix → outputPrefix rules (255 characters each).
125
+ Example: brand/ → assets/brand/. Empty prefixes mean root; empty/nonexistent folders are valid.
126
+ Only ready current originals are included; pending files wait until ready. Generated outputs are
127
+ read-only, unpinned, and untransformed. No globs or excludes. Manual + generated outputs must
128
+ not collide (including case/Unicode or file/directory overlaps) and total at most 1000.
129
+ get_vard returns editable manual items and folderRules, even if resolution fails. Use
130
+ get_vard with resolved=true for a separate live preview; never persist expanded items.
131
+ update_vard replaces only supplied fields; folderRules: [] removes rules without changing manual items.
132
+ A new private folder member makes the whole anonymous public manifest unavailable.
133
+ Normal vardum pull intentionally updates membership; --frozen consumes the committed snapshot.
134
+ There is no background sync. Choose --delete on the membership-updating pull: without it,
135
+ stale files remain but lose tracking, requiring deliberate manual cleanup later.
136
+
117
137
  ## Writable outputs
118
138
  Only Vard items explicitly marked "Writable" in the UI can be pushed to.
119
139
  Pinned and transformed outputs are always read-only.`,
@@ -156,7 +176,7 @@ Folders exist only as key prefixes — no need to "create" them.
156
176
  - **editor**: full CRUD on assets, Vards, and presets
157
177
  - **owner**: workspace administration (members, API keys, domain, deletion)
158
178
 
159
- A workspace API key carries the permissions of whoever created it, nothing more.
179
+ MCP uses a workspace-scoped asset key. Members, invitations, keys, domains and workspace administration require a browser session and are managed in the web app.
160
180
 
161
181
  ## Video assets
162
182
  MP4, WebM, MOV, AVI and MPEG are accepted. They are stored and delivered as-is —
@@ -196,142 +216,42 @@ Everything an editor can do, plus:
196
216
  - Full audit log access
197
217
 
198
218
  ## API key scoping
199
- A workspace API key inherits the permissions of the person who created it.
200
- A key can never grant more access than its creator has. Self-revocation
219
+ MCP exposes asset and Vard tools only. A read key can list/read; a write key adds
220
+ create, update, delete and publish. Keys are restricted to one workspace and do
221
+ not grant browser-session administration, even when created by an owner. Self-revocation
201
222
  (\`vardum logout\`) is available to CLI keys without workspace admin access.`,
202
223
  },
203
224
  ],
204
225
  }));
205
- // ── Tools: Workspace ────────────────────────────────────────────────────────
206
- server.tool("list_organizations", "List all organizations the authenticated user belongs to.", {}, async () => {
207
- const data = await api("/api/organizations");
208
- return {
209
- content: [
210
- { type: "text", text: JSON.stringify(data.organizations, null, 2) },
211
- ],
212
- };
213
- });
214
- server.tool("create_workspace", "Create a new workspace inside an organization.", {
215
- organizationId: z.string().describe("Organization ID"),
216
- name: z.string().min(1).max(64).describe("Display name for the workspace"),
217
- slug: z
218
- .string()
219
- .regex(/^[a-z0-9][a-z0-9._-]*$/)
220
- .describe("URL-safe slug (cannot be changed later)"),
221
- }, async ({ organizationId, name, slug }) => {
222
- const data = await api(`/api/organizations/${encodeURIComponent(organizationId)}/workspaces`, { method: "POST", body: JSON.stringify({ name, slug }) });
223
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
224
- });
225
- server.tool("get_workspace", "Retrieve workspace details including name, slug, and member count.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
226
- const [members, overview] = await Promise.all([
227
- api(`/api/workspaces/${encodeURIComponent(workspaceId)}/members`),
228
- api(`/api/workspaces/${encodeURIComponent(workspaceId)}/overview`),
229
- ]);
230
- return {
231
- content: [
232
- {
233
- type: "text",
234
- text: JSON.stringify({ ...overview, members: members.members }, null, 2),
235
- },
236
- ],
237
- };
238
- });
239
- server.tool("update_workspace", "Update the display name of a workspace. The slug cannot be changed.", {
240
- workspaceId: z.string().describe("Workspace ID"),
241
- name: z.string().min(1).max(64).describe("New display name"),
242
- }, async ({ workspaceId, name }) => {
243
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}`, { method: "PATCH", body: JSON.stringify({ name }) });
244
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
245
- });
246
- // ── Tools: Members & Invitations ────────────────────────────────────────────
247
- server.tool("list_members", "List all members of a workspace with their roles.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
248
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/members`);
249
- return {
250
- content: [{ type: "text", text: JSON.stringify(data.members, null, 2) }],
251
- };
252
- });
253
- server.tool("change_member_role", "Change a member's role in the workspace.", {
254
- workspaceId: z.string().describe("Workspace ID"),
255
- memberId: z.string().describe("Member ID"),
256
- role: z.enum(["owner", "editor", "viewer"]).describe("New role"),
257
- }, async ({ workspaceId, memberId, role }) => {
258
- await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(memberId)}`, { method: "PATCH", body: JSON.stringify({ role }) });
259
- return { content: [{ type: "text", text: `Role changed to ${role}.` }] };
260
- });
261
- server.tool("list_invitations", "List pending invitations for a workspace.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
262
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/invitations`);
263
- return {
264
- content: [
265
- { type: "text", text: JSON.stringify(data.invitations, null, 2) },
266
- ],
267
- };
268
- });
269
- server.tool("create_invitation", "Invite someone by email to join a workspace with a specific role.", {
270
- workspaceId: z.string().describe("Workspace ID"),
271
- email: z.string().email().describe("Recipient email address"),
272
- role: z
273
- .enum(["editor", "viewer"])
274
- .describe("Role to assign (owner cannot be invited directly)"),
275
- }, async ({ workspaceId, email, role }) => {
276
- await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/invitations`, { method: "POST", body: JSON.stringify({ email, role }) });
277
- return {
278
- content: [
279
- { type: "text", text: `Invitation sent to ${email} (${role}).` },
280
- ],
281
- };
282
- });
283
- server.tool("revoke_invitation", "Revoke a pending invitation before it expires.", {
284
- workspaceId: z.string().describe("Workspace ID"),
285
- invitationId: z.string().describe("Invitation ID"),
286
- }, async ({ workspaceId, invitationId }) => {
287
- await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/invitations/${encodeURIComponent(invitationId)}`, { method: "DELETE" });
288
- return { content: [{ type: "text", text: "Invitation revoked." }] };
289
- });
290
- // ── Tools: API Keys ─────────────────────────────────────────────────────────
291
- server.tool("list_api_keys", "List all API keys for a workspace.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
292
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/api-keys`);
293
- return {
294
- content: [{ type: "text", text: JSON.stringify(data.keys, null, 2) }],
295
- };
296
- });
297
- server.tool("create_api_key", "Create a new API key for a workspace. The full key is shown only once.", {
298
- workspaceId: z.string().describe("Workspace ID"),
299
- name: z
300
- .string()
301
- .min(1)
302
- .max(32)
303
- .describe("Human-readable label for the key"),
304
- access: z
305
- .enum(["read", "write"])
306
- .describe("Access level: read-only or read-write"),
307
- }, async ({ workspaceId, name, access }) => {
308
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/api-keys`, {
309
- method: "POST",
310
- body: JSON.stringify({ name, access }),
311
- });
312
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
313
- });
314
- server.tool("revoke_api_key", "Revoke (delete) an API key by ID.", {
315
- workspaceId: z.string().describe("Workspace ID"),
316
- keyId: z.string().describe("API key ID to revoke"),
317
- }, async ({ workspaceId, keyId }) => {
318
- await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/api-keys/${encodeURIComponent(keyId)}`, { method: "DELETE" });
319
- return { content: [{ type: "text", text: "API key revoked." }] };
320
- });
321
226
  // ── Tools: Assets ───────────────────────────────────────────────────────────
322
- server.tool("list_assets", "List assets in a workspace, optionally filtered by prefix (virtual folder).", {
227
+ server.tool("list_assets", "List one page of ready assets and visibility, optionally filtered by prefix. Pass nextCursor as cursor to continue; null means the end.", {
323
228
  workspaceId: z.string().describe("Workspace ID"),
324
229
  prefix: z
325
230
  .string()
326
231
  .optional()
327
232
  .describe("Prefix filter (e.g. 'icons/' to list only that folder)"),
328
- }, async ({ workspaceId, prefix = "" }) => {
329
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/sync/assets?prefix=${encodeURIComponent(prefix)}&limit=500`);
233
+ cursor: z
234
+ .string()
235
+ .max(255)
236
+ .optional()
237
+ .describe("nextCursor from the previous page"),
238
+ limit: z
239
+ .number()
240
+ .int()
241
+ .min(1)
242
+ .max(500)
243
+ .optional()
244
+ .describe("Page size, default 100"),
245
+ }, async ({ workspaceId, prefix = "", cursor, limit = 100 }) => {
246
+ const query = new URLSearchParams({ prefix, limit: String(limit) });
247
+ if (cursor !== undefined)
248
+ query.set("cursor", cursor);
249
+ const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/sync/assets?${query}`);
330
250
  return {
331
- content: [{ type: "text", text: JSON.stringify(data.assets, null, 2) }],
251
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
332
252
  };
333
253
  });
334
- server.tool("create_asset", "Create a new asset record with a key. Use upload_version to attach image data.", {
254
+ server.tool("create_asset", "Create a private asset record with a key. Use upload_asset_version to attach media, then set_asset_visibility to publish explicitly.", {
335
255
  workspaceId: z.string().describe("Workspace ID"),
336
256
  key: z
337
257
  .string()
@@ -340,7 +260,20 @@ server.tool("create_asset", "Create a new asset record with a key. Use upload_ve
340
260
  .describe("Asset key (use / for folder structure)"),
341
261
  displayName: z.string().min(1).describe("Human-readable filename"),
342
262
  }, async ({ workspaceId, key, displayName }) => {
343
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets`, { method: "POST", body: JSON.stringify({ key, displayName }) });
263
+ const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets`, {
264
+ method: "POST",
265
+ body: JSON.stringify({ key, displayName }),
266
+ });
267
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
268
+ });
269
+ server.tool("set_asset_visibility", "Set asset visibility with publish permission. Public exposes every ready version and variant. Private requires workspace read access but cannot revoke downloaded or cached copies. Vard visibility is independent.", {
270
+ workspaceId: z.string().min(1).describe("Workspace ID"),
271
+ assetId: z.string().uuid().describe("Asset ID"),
272
+ isPublic: z
273
+ .boolean()
274
+ .describe("True to publish; false to require workspace read access"),
275
+ }, async ({ workspaceId, assetId, isPublic }) => {
276
+ const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}/visibility`, { method: "PATCH", body: JSON.stringify({ isPublic }) });
344
277
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
345
278
  });
346
279
  server.tool("delete_asset", "Delete an asset and all its versions permanently.", {
@@ -398,42 +331,69 @@ server.tool("list_vards", "List all Vards in a workspace.", { workspaceId: z.str
398
331
  content: [{ type: "text", text: JSON.stringify(data.vards, null, 2) }],
399
332
  };
400
333
  });
401
- server.tool("create_vard", "Create a new Vard in a workspace. The slug cannot be changed later.", {
402
- workspaceId: z.string().describe("Workspace ID"),
403
- name: z.string().min(1).max(128).describe("Display name"),
404
- slug: z
334
+ const folderRulesInput = z
335
+ .array(z
336
+ .object({
337
+ sourcePrefix: z
338
+ .string()
339
+ .max(255)
340
+ .describe("Literal recursive source directory, slash-terminated; empty means workspace root"),
341
+ outputPrefix: z
405
342
  .string()
406
- .regex(/^[a-z0-9][a-z0-9._-]*$/)
407
- .describe("URL-safe slug (immutable)"),
343
+ .max(255)
344
+ .describe("Relative destination directory, slash-terminated; empty means project root"),
345
+ })
346
+ .strict())
347
+ .max(64)
348
+ .describe("Complete folder rule list; [] removes all rules. API validates safe prefixes. Ready current originals only, no globs; 1000 combined outputs maximum.");
349
+ server.tool("create_vard", "Create a Vard with optional recursive folder rules, private by default. Rules generate read-only originals separately from manual items.", {
350
+ workspaceId: z.string().describe("Workspace ID"),
351
+ name: z.string().min(1).max(80).describe("Display name"),
408
352
  isPublic: z
409
353
  .boolean()
410
354
  .optional()
411
- .describe("Whether the Vard is publicly accessible"),
412
- }, async ({ workspaceId, name, slug, isPublic }) => {
355
+ .describe("Public manifest; every resolved asset must also be public for anonymous access"),
356
+ folderRules: folderRulesInput.optional(),
357
+ }, async ({ workspaceId, name, isPublic, folderRules }) => {
358
+ const body = { name };
359
+ if (isPublic !== undefined)
360
+ body.isPublic = isPublic;
361
+ if (folderRules !== undefined)
362
+ body.folderRules = folderRules;
413
363
  const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards`, {
414
364
  method: "POST",
415
- body: JSON.stringify({ name, slug, isPublic }),
365
+ body: JSON.stringify(body),
416
366
  });
417
367
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
418
368
  });
419
- server.tool("get_vard", "Retrieve a Vard and its full manifest (items, outputs, their versions and download URLs).", {
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.", {
420
370
  workspaceId: z.string().describe("Workspace ID"),
421
371
  vardId: z.string().describe("Vard ID"),
422
- }, async ({ workspaceId, vardId }) => {
423
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`);
372
+ resolved: z
373
+ .boolean()
374
+ .optional()
375
+ .describe("True: resolve current outputs, which may fail on collisions/limits. Omitted/false: editable configuration only."),
376
+ }, async ({ workspaceId, vardId, resolved }) => {
377
+ const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}${resolved ? "/manifest" : ""}`);
424
378
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
425
379
  });
426
- server.tool("update_vard", "Update a Vard's metadata (name, public flag). The slug is immutable.", {
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.", {
427
381
  workspaceId: z.string().describe("Workspace ID"),
428
382
  vardId: z.string().describe("Vard ID"),
429
- name: z.string().min(1).max(128).optional().describe("New display name"),
430
- isPublic: z.boolean().optional().describe("Public accessibility"),
431
- }, async ({ workspaceId, vardId, name, isPublic }) => {
383
+ name: z.string().min(1).max(80).optional().describe("New display name"),
384
+ isPublic: z
385
+ .boolean()
386
+ .optional()
387
+ .describe("Public manifest only; does not publish resolved assets"),
388
+ folderRules: folderRulesInput.optional(),
389
+ }, async ({ workspaceId, vardId, name, isPublic, folderRules }) => {
432
390
  const body = {};
433
391
  if (name !== undefined)
434
392
  body.name = name;
435
393
  if (isPublic !== undefined)
436
394
  body.isPublic = isPublic;
395
+ if (folderRules !== undefined)
396
+ body.folderRules = folderRules;
437
397
  const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "PATCH", body: JSON.stringify(body) });
438
398
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
439
399
  });
@@ -444,11 +404,12 @@ server.tool("delete_vard", "Delete a Vard and all its output configurations perm
444
404
  await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "DELETE" });
445
405
  return { content: [{ type: "text", text: "Vard deleted." }] };
446
406
  });
447
- server.tool("replace_vard_items", "Replace every item in a Vard with a new set of item definitions. Each item maps an asset to an output path and optional transformation settings.", {
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.", {
448
408
  workspaceId: z.string().describe("Workspace ID"),
449
409
  vardId: z.string().describe("Vard ID"),
450
410
  items: z
451
- .array(z.object({
411
+ .array(z
412
+ .object({
452
413
  assetId: z.string().describe("The asset ID to include"),
453
414
  outputPath: z
454
415
  .string()
@@ -456,38 +417,23 @@ server.tool("replace_vard_items", "Replace every item in a Vard with a new set o
456
417
  writable: z
457
418
  .boolean()
458
419
  .describe("Whether this output can be pushed back"),
459
- pinned: z
460
- .boolean()
461
- .describe("Whether the version is pinned (read-only)"),
420
+ versionId: z
421
+ .string()
422
+ .uuid()
423
+ .nullable()
424
+ .optional()
425
+ .describe("Exact ready version ID to pin; null/omitted follows latest. Pinned outputs must not be writable."),
462
426
  transformation: z
463
427
  .record(z.unknown())
464
428
  .nullable()
465
429
  .describe("Optional transformation parameters (w, h, fit, format, q) or null"),
466
- }))
467
- .describe("Complete list of items to set (replaces all current items)"),
430
+ })
431
+ .strict())
432
+ .describe("Complete list of manual items to set (replaces manual items only)"),
468
433
  }, async ({ workspaceId, vardId, items }) => {
469
434
  const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}/items`, { method: "PUT", body: JSON.stringify({ items }) });
470
435
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
471
436
  });
472
- // ── Domain ─────────────────────────────────────────────────────────────────
473
- server.tool("get_domain", "Get the current custom domain configuration for a workspace.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
474
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/domain`);
475
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
476
- });
477
- server.tool("configure_domain", "Configure a custom domain for a workspace. Requires DNS verification (TXT + CNAME).", {
478
- workspaceId: z.string().describe("Workspace ID"),
479
- domain: z.string().describe("Domain name (e.g. assets.example.com)"),
480
- }, async ({ workspaceId, domain }) => {
481
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/domain`, {
482
- method: "PUT",
483
- body: JSON.stringify({ domain }),
484
- });
485
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
486
- });
487
- server.tool("verify_domain", "Trigger domain verification. The agent should first read the current domain config to show the required DNS records, then call this once they are in place.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
488
- const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/domain/verify`, { method: "POST" });
489
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
490
- });
491
437
  // ── Main ────────────────────────────────────────────────────────────────────
492
438
  async function main() {
493
439
  if (!API_KEY) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vardum/mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for Vardum \u2014 let AI agents manage assets, Vards, workspaces, and more.",
3
+ "version": "0.3.0",
4
+ "description": "MCP server for workspace-scoped Vardum assets and Vards",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -14,21 +14,30 @@
14
14
  "engines": {
15
15
  "node": ">=22"
16
16
  },
17
- "scripts": {
18
- "build": "tsc -p tsconfig.json",
19
- "prepublishOnly": "pnpm build",
20
- "typecheck": "tsc -p tsconfig.json --noEmit"
21
- },
22
17
  "publishConfig": {
23
18
  "access": "public"
24
19
  },
25
20
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.15.0",
27
- "@vardum/shared": "workspace:^",
21
+ "@modelcontextprotocol/sdk": "1.31.0",
22
+ "@vardum/shared": "^0.3.0",
28
23
  "zod": "^3.24.0"
29
24
  },
30
25
  "devDependencies": {
31
26
  "@types/node": "^26.2.0",
32
27
  "typescript": "5.9.3"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/f-tuma/vardum.git",
32
+ "directory": "apps/mcp"
33
+ },
34
+ "homepage": "https://github.com/f-tuma/vardum/tree/main/apps/mcp#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/f-tuma/vardum/issues"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "test": "node --import tsx --test test/*.test.ts",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit"
33
42
  }
34
- }
43
+ }