@vardum/mcp 0.1.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.
- package/README.md +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +503 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @vardum/mcp
|
|
2
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
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 carries the permissions of whoever created it.
|
|
12
|
+
|
|
13
|
+
Claude Code:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
claude mcp add vardum --env VARDUM_API_KEY=varda_... -- npx -y @vardum/mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Any client that reads a JSON config (Claude Desktop, Cursor, …):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"vardum": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "@vardum/mcp"],
|
|
27
|
+
"env": { "VARDUM_API_KEY": "varda_..." }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
| Setting | Flag | Environment | Default |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| Vardum instance | `--server URL` | `VARDUM_SERVER` | `https://vardum.com` |
|
|
38
|
+
| Workspace API key | `--api-key KEY` | `VARDUM_API_KEY` | — (required) |
|
|
39
|
+
|
|
40
|
+
Point `--server` at your own host when self-hosting. `vardum-mcp --version` and
|
|
41
|
+
`--help` answer without needing a key, which is the quickest way to check that a
|
|
42
|
+
client can start the binary at all.
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
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`).
|
|
51
|
+
|
|
52
|
+
Images and video are accepted; video is stored and delivered as-is, and cannot
|
|
53
|
+
be resized or transformed into variants.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { supportedMimeTypes } from "@vardum/shared";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
// ── CLI / env helpers ──────────────────────────────────────────────────────
|
|
10
|
+
let VERSION = "0.0.0";
|
|
11
|
+
try {
|
|
12
|
+
VERSION = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// The version is cosmetic; a missing package.json must not stop the server.
|
|
16
|
+
}
|
|
17
|
+
function parseArgs() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const result = {};
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (arg === "--api-key" || arg === "--server") {
|
|
23
|
+
result[arg.slice(2)] = args[++i] ?? "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
// Answered before anything else needs configuring: a client that cannot even
|
|
29
|
+
// start the binary has no way to see an error that only appears after auth.
|
|
30
|
+
if (process.argv.slice(2).some((arg) => arg === "--version" || arg === "-v")) {
|
|
31
|
+
console.log(VERSION);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
|
|
35
|
+
console.log(`Vardum MCP server v${VERSION}
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
vardum-mcp [--server URL] [--api-key KEY]
|
|
39
|
+
|
|
40
|
+
Environment:
|
|
41
|
+
VARDUM_SERVER Vardum instance to talk to (default https://vardum.com)
|
|
42
|
+
VARDUM_API_KEY workspace API key; create one in Settings // API keys
|
|
43
|
+
|
|
44
|
+
The server speaks MCP over stdio, so it is started by the client, not by hand.`);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
// ── HTTP client ────────────────────────────────────────────────────────────
|
|
48
|
+
const cli = parseArgs();
|
|
49
|
+
const SERVER = cli.server ?? process.env.VARDUM_SERVER ?? "https://vardum.com";
|
|
50
|
+
const API_KEY = cli["api-key"] ?? process.env.VARDUM_API_KEY ?? "";
|
|
51
|
+
const headers = {
|
|
52
|
+
"user-agent": `vardum-mcp/${VERSION}`,
|
|
53
|
+
};
|
|
54
|
+
if (API_KEY)
|
|
55
|
+
headers["x-api-key"] = API_KEY;
|
|
56
|
+
async function api(path, init = {}) {
|
|
57
|
+
const merged = new Headers(init.headers);
|
|
58
|
+
for (const [k, v] of Object.entries(headers))
|
|
59
|
+
merged.set(k, v);
|
|
60
|
+
if (init.body && !merged.has("content-type"))
|
|
61
|
+
merged.set("content-type", "application/json");
|
|
62
|
+
const res = await fetch(new URL(path, `${SERVER}/`), {
|
|
63
|
+
...init,
|
|
64
|
+
headers: merged,
|
|
65
|
+
});
|
|
66
|
+
if (res.status === 204)
|
|
67
|
+
return undefined;
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let msg = text || res.statusText;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(text);
|
|
73
|
+
msg = parsed.message ?? parsed.error ?? msg;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* use body text */
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`${msg} (${res.status})`);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error("Invalid JSON response");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── Server ─────────────────────────────────────────────────────────────────
|
|
88
|
+
const server = new McpServer({
|
|
89
|
+
name: "Vardum",
|
|
90
|
+
version: "0.1.0",
|
|
91
|
+
});
|
|
92
|
+
// ── Resources ──────────────────────────────────────────────────────────────
|
|
93
|
+
server.resource("constraints", "vardum://constraints", () => ({
|
|
94
|
+
contents: [
|
|
95
|
+
{
|
|
96
|
+
uri: "vardum://constraints",
|
|
97
|
+
text: `# Vardum Constraints
|
|
98
|
+
|
|
99
|
+
## Slug immutability
|
|
100
|
+
Organization, workspace and Vard slugs are set at creation time and **cannot be changed**.
|
|
101
|
+
Each slug must match \`[a-z0-9][a-z0-9._-]*\`.
|
|
102
|
+
|
|
103
|
+
## Asset key rules
|
|
104
|
+
Every asset has a unique key scoped to a workspace. Rules:
|
|
105
|
+
- Must match \`[a-z0-9][a-z0-9._-]*(\\/[a-z0-9][a-z0-9._-]*)*\`
|
|
106
|
+
- Max 255 characters
|
|
107
|
+
- Only lowercase letters, digits, \`-\`, \`_\`, \`.\` and \`/\`
|
|
108
|
+
- The \`/\` separator creates virtual folders
|
|
109
|
+
|
|
110
|
+
## Upload limits
|
|
111
|
+
- Image types: AVIF, GIF, JPEG, PNG, SVG, TIFF, WebP
|
|
112
|
+
- Video types: MP4, WebM, MOV, AVI, MPEG
|
|
113
|
+
- Max 100 megapixels per image; videos are stored as-is (no transcode)
|
|
114
|
+
- Storage backend caps at 100 MiB per upload
|
|
115
|
+
- Video assets cannot be resized or transformed into variants (images only)
|
|
116
|
+
|
|
117
|
+
## Writable outputs
|
|
118
|
+
Only Vard items explicitly marked "Writable" in the UI can be pushed to.
|
|
119
|
+
Pinned and transformed outputs are always read-only.`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}));
|
|
123
|
+
server.resource("best-practices", "vardum://best-practices", () => ({
|
|
124
|
+
contents: [
|
|
125
|
+
{
|
|
126
|
+
uri: "vardum://best-practices",
|
|
127
|
+
text: `# Vardum Best Practices
|
|
128
|
+
|
|
129
|
+
## Prefer variants over multiple assets
|
|
130
|
+
Do **not** upload the same image in five sizes (logo.png, logo-small.png, logo@2x.png).
|
|
131
|
+
Upload one canonical source (preferably SVG) and use **on-the-fly variants**:
|
|
132
|
+
|
|
133
|
+
?w=800&format=avif&q=85
|
|
134
|
+
|
|
135
|
+
Variants are cached in S3 after first request — zero storage cost until used,
|
|
136
|
+
zero management overhead. The system handles resize, format conversion and
|
|
137
|
+
quality natively. Only a new upload should create a distinct asset key.
|
|
138
|
+
|
|
139
|
+
## SVG handling
|
|
140
|
+
SVG uploads are fully supported. When a variant is requested from an SVG source,
|
|
141
|
+
sharp rasterises it to **PNG** automatically via librsvg. Guidelines:
|
|
142
|
+
- Upload SVG at the ideal source dimensions (variant system only downscales)
|
|
143
|
+
- Upscaling an SVG loses quality — resize the SVG before upload if a larger
|
|
144
|
+
physical size is needed
|
|
145
|
+
- SVGs without explicit viewBox or width/height can render at unexpected sizes;
|
|
146
|
+
always include \`viewBox\` in the root element
|
|
147
|
+
|
|
148
|
+
## Folder organisation
|
|
149
|
+
Use the \`/\` separator in asset keys to create virtual folders:
|
|
150
|
+
icons/logo.svg, icons/button.svg, marketing/banners/hero.avif
|
|
151
|
+
|
|
152
|
+
Folders exist only as key prefixes — no need to "create" them.
|
|
153
|
+
|
|
154
|
+
## Role awareness
|
|
155
|
+
- **viewer**: read-only access to everything
|
|
156
|
+
- **editor**: full CRUD on assets, Vards, and presets
|
|
157
|
+
- **owner**: workspace administration (members, API keys, domain, deletion)
|
|
158
|
+
|
|
159
|
+
A workspace API key carries the permissions of whoever created it, nothing more.
|
|
160
|
+
|
|
161
|
+
## Video assets
|
|
162
|
+
MP4, WebM, MOV, AVI and MPEG are accepted. They are stored and delivered as-is —
|
|
163
|
+
Vardum does not transcode video, generate thumbnails, or offer video variants.
|
|
164
|
+
Use a purpose-built video platform for transcoding/streaming needs. For Vardum,
|
|
165
|
+
reference videos by their original key; the \`video\` element plays them directly
|
|
166
|
+
with browser support (H.264 MP4 and WebM have the widest coverage).`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
server.resource("roles", "vardum://roles", () => ({
|
|
171
|
+
contents: [
|
|
172
|
+
{
|
|
173
|
+
uri: "vardum://roles",
|
|
174
|
+
text: `# Vardum Roles
|
|
175
|
+
|
|
176
|
+
## Viewer
|
|
177
|
+
- Read assets, Vards, deliveries
|
|
178
|
+
- View workspace settings
|
|
179
|
+
- No mutations
|
|
180
|
+
|
|
181
|
+
## Editor
|
|
182
|
+
Everything a viewer can do, plus:
|
|
183
|
+
- Create, update, delete assets
|
|
184
|
+
- Upload new asset versions
|
|
185
|
+
- Create, update, delete Vards
|
|
186
|
+
- Manage Vard items and outputs
|
|
187
|
+
- Manage presets (transformations)
|
|
188
|
+
- Read domain settings
|
|
189
|
+
|
|
190
|
+
## Owner
|
|
191
|
+
Everything an editor can do, plus:
|
|
192
|
+
- Manage workspace members (invite, change roles, remove)
|
|
193
|
+
- Create and revoke API keys
|
|
194
|
+
- Manage workspace settings (name, deletion)
|
|
195
|
+
- Manage custom domains
|
|
196
|
+
- Full audit log access
|
|
197
|
+
|
|
198
|
+
## 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
|
|
201
|
+
(\`vardum logout\`) is available to CLI keys without workspace admin access.`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}));
|
|
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
|
+
// ── Tools: Assets ───────────────────────────────────────────────────────────
|
|
322
|
+
server.tool("list_assets", "List assets in a workspace, optionally filtered by prefix (virtual folder).", {
|
|
323
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
324
|
+
prefix: z
|
|
325
|
+
.string()
|
|
326
|
+
.optional()
|
|
327
|
+
.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`);
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: JSON.stringify(data.assets, null, 2) }],
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
server.tool("create_asset", "Create a new asset record with a key. Use upload_version to attach image data.", {
|
|
335
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
336
|
+
key: z
|
|
337
|
+
.string()
|
|
338
|
+
.regex(/^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/)
|
|
339
|
+
.max(255)
|
|
340
|
+
.describe("Asset key (use / for folder structure)"),
|
|
341
|
+
displayName: z.string().min(1).describe("Human-readable filename"),
|
|
342
|
+
}, async ({ workspaceId, key, displayName }) => {
|
|
343
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets`, { method: "POST", body: JSON.stringify({ key, displayName }) });
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
345
|
+
});
|
|
346
|
+
server.tool("delete_asset", "Delete an asset and all its versions permanently.", {
|
|
347
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
348
|
+
assetId: z.string().describe("Asset ID to delete"),
|
|
349
|
+
}, async ({ workspaceId, assetId }) => {
|
|
350
|
+
await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}`, { method: "DELETE" });
|
|
351
|
+
return { content: [{ type: "text", text: "Asset deleted." }] };
|
|
352
|
+
});
|
|
353
|
+
server.tool("upload_asset_version", "Upload a new image or video version for an existing asset. Provide base64-encoded data. The version gets processed asynchronously by the system.", {
|
|
354
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
355
|
+
assetId: z.string().describe("Asset ID to upload for"),
|
|
356
|
+
filename: z
|
|
357
|
+
.string()
|
|
358
|
+
.min(1)
|
|
359
|
+
.describe("Filename with extension (e.g. 'logo.avif')"),
|
|
360
|
+
mimeType: z
|
|
361
|
+
.string()
|
|
362
|
+
.describe(`Image or video MIME type (${supportedMimeTypes.join(", ")})`),
|
|
363
|
+
base64: z.string().describe("Base64-encoded image binary"),
|
|
364
|
+
}, async ({ workspaceId, assetId, filename, mimeType, base64 }) => {
|
|
365
|
+
const bytes = Buffer.from(base64, "base64");
|
|
366
|
+
// 1. Obtain a signed upload URL
|
|
367
|
+
const upload = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}/versions`, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
body: JSON.stringify({ filename, mimeType, size: bytes.byteLength }),
|
|
370
|
+
});
|
|
371
|
+
// 2. POST the file to the signed URL
|
|
372
|
+
const form = new FormData();
|
|
373
|
+
for (const [k, v] of Object.entries(upload.upload.fields))
|
|
374
|
+
form.append(k, v);
|
|
375
|
+
form.append("file", new Blob([bytes], { type: mimeType }), filename);
|
|
376
|
+
// pi-lens-ignore: identifier — server-signed upload URL, not user-controlled
|
|
377
|
+
const stored = await fetch(upload.upload.url, {
|
|
378
|
+
method: upload.upload.method,
|
|
379
|
+
body: form,
|
|
380
|
+
});
|
|
381
|
+
if (!stored.ok)
|
|
382
|
+
throw new Error(`Storage upload failed (${stored.status})`);
|
|
383
|
+
// 3. Finalize so the processing worker picks it up
|
|
384
|
+
await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}/versions/${encodeURIComponent(upload.id)}/finalize`, { method: "POST" });
|
|
385
|
+
return {
|
|
386
|
+
content: [
|
|
387
|
+
{
|
|
388
|
+
type: "text",
|
|
389
|
+
text: JSON.stringify({ versionId: upload.id, status: "processing", assetId }, null, 2),
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
// ── Tools: Vards ────────────────────────────────────────────────────────────
|
|
395
|
+
server.tool("list_vards", "List all Vards in a workspace.", { workspaceId: z.string().describe("Workspace ID") }, async ({ workspaceId }) => {
|
|
396
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards`);
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: JSON.stringify(data.vards, null, 2) }],
|
|
399
|
+
};
|
|
400
|
+
});
|
|
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
|
|
405
|
+
.string()
|
|
406
|
+
.regex(/^[a-z0-9][a-z0-9._-]*$/)
|
|
407
|
+
.describe("URL-safe slug (immutable)"),
|
|
408
|
+
isPublic: z
|
|
409
|
+
.boolean()
|
|
410
|
+
.optional()
|
|
411
|
+
.describe("Whether the Vard is publicly accessible"),
|
|
412
|
+
}, async ({ workspaceId, name, slug, isPublic }) => {
|
|
413
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards`, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
body: JSON.stringify({ name, slug, isPublic }),
|
|
416
|
+
});
|
|
417
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
418
|
+
});
|
|
419
|
+
server.tool("get_vard", "Retrieve a Vard and its full manifest (items, outputs, their versions and download URLs).", {
|
|
420
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
421
|
+
vardId: z.string().describe("Vard ID"),
|
|
422
|
+
}, async ({ workspaceId, vardId }) => {
|
|
423
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`);
|
|
424
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
425
|
+
});
|
|
426
|
+
server.tool("update_vard", "Update a Vard's metadata (name, public flag). The slug is immutable.", {
|
|
427
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
428
|
+
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 }) => {
|
|
432
|
+
const body = {};
|
|
433
|
+
if (name !== undefined)
|
|
434
|
+
body.name = name;
|
|
435
|
+
if (isPublic !== undefined)
|
|
436
|
+
body.isPublic = isPublic;
|
|
437
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "PATCH", body: JSON.stringify(body) });
|
|
438
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
439
|
+
});
|
|
440
|
+
server.tool("delete_vard", "Delete a Vard and all its output configurations permanently.", {
|
|
441
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
442
|
+
vardId: z.string().describe("Vard ID"),
|
|
443
|
+
}, async ({ workspaceId, vardId }) => {
|
|
444
|
+
await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}`, { method: "DELETE" });
|
|
445
|
+
return { content: [{ type: "text", text: "Vard deleted." }] };
|
|
446
|
+
});
|
|
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.", {
|
|
448
|
+
workspaceId: z.string().describe("Workspace ID"),
|
|
449
|
+
vardId: z.string().describe("Vard ID"),
|
|
450
|
+
items: z
|
|
451
|
+
.array(z.object({
|
|
452
|
+
assetId: z.string().describe("The asset ID to include"),
|
|
453
|
+
outputPath: z
|
|
454
|
+
.string()
|
|
455
|
+
.describe("Relative output path in the project"),
|
|
456
|
+
writable: z
|
|
457
|
+
.boolean()
|
|
458
|
+
.describe("Whether this output can be pushed back"),
|
|
459
|
+
pinned: z
|
|
460
|
+
.boolean()
|
|
461
|
+
.describe("Whether the version is pinned (read-only)"),
|
|
462
|
+
transformation: z
|
|
463
|
+
.record(z.unknown())
|
|
464
|
+
.nullable()
|
|
465
|
+
.describe("Optional transformation parameters (w, h, fit, format, q) or null"),
|
|
466
|
+
}))
|
|
467
|
+
.describe("Complete list of items to set (replaces all current items)"),
|
|
468
|
+
}, async ({ workspaceId, vardId, items }) => {
|
|
469
|
+
const data = await api(`/api/workspaces/${encodeURIComponent(workspaceId)}/vards/${encodeURIComponent(vardId)}/items`, { method: "PUT", body: JSON.stringify({ items }) });
|
|
470
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
471
|
+
});
|
|
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
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
492
|
+
async function main() {
|
|
493
|
+
if (!API_KEY) {
|
|
494
|
+
console.error("Vardum MCP server needs an API key. Set VARDUM_API_KEY or pass --api-key.");
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
const transport = new StdioServerTransport();
|
|
498
|
+
await server.connect(transport);
|
|
499
|
+
}
|
|
500
|
+
main().catch((error) => {
|
|
501
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vardum-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "pnpm build",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
27
|
+
"@vardum/shared": "workspace:^",
|
|
28
|
+
"zod": "^3.24.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.2.0",
|
|
32
|
+
"typescript": "5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|