vibedsgn-mcp 1.2.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/dist/index.cjs ADDED
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/index.ts
5
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+
8
+ // src/auth.ts
9
+ function getAuthHeader() {
10
+ const apiKey = process.env.VIBEDSGN_API_KEY;
11
+ if (apiKey) return `Bearer ${apiKey}`;
12
+ const sessionToken = process.env.VIBEDSGN_SESSION_TOKEN;
13
+ if (sessionToken) return `Bearer ${sessionToken}`;
14
+ throw new Error(
15
+ "Missing authentication: set VIBEDSGN_API_KEY or VIBEDSGN_SESSION_TOKEN environment variable"
16
+ );
17
+ }
18
+ function getApiBase() {
19
+ return process.env.VIBEDSGN_API_URL ?? "https://vibedsgn.com";
20
+ }
21
+
22
+ // src/client.ts
23
+ var VibedsgnApiError = class extends Error {
24
+ code;
25
+ status;
26
+ details;
27
+ constructor(code, status, message, details) {
28
+ super(message);
29
+ this.code = code;
30
+ this.status = status;
31
+ this.details = details;
32
+ }
33
+ };
34
+ var VibedsgnClient = class {
35
+ apiBase;
36
+ authHeader;
37
+ constructor() {
38
+ this.apiBase = getApiBase();
39
+ this.authHeader = getAuthHeader();
40
+ }
41
+ async parseError(res) {
42
+ let body = null;
43
+ try {
44
+ body = await res.json();
45
+ } catch {
46
+ body = null;
47
+ }
48
+ if (body && typeof body === "object" && "error" in body && body.error && typeof body.error === "object") {
49
+ const err = body.error;
50
+ return new VibedsgnApiError(err.code ?? "INTERNAL", res.status, err.message ?? res.statusText, err.details);
51
+ }
52
+ const flatMsg = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : res.statusText;
53
+ return new VibedsgnApiError("INTERNAL", res.status, flatMsg);
54
+ }
55
+ /**
56
+ * Verify the current API key and return the authenticated user.
57
+ * Useful for agents to fail fast with a clear message before doing real work.
58
+ */
59
+ async whoami() {
60
+ const res = await fetch(`${this.apiBase}/api/mcp/whoami`, {
61
+ method: "GET",
62
+ headers: { Authorization: this.authHeader }
63
+ });
64
+ if (!res.ok) throw await this.parseError(res);
65
+ const data = await res.json();
66
+ return data.user;
67
+ }
68
+ /**
69
+ * Upload a base64-encoded image to Vercel Blob via the MCP upload route.
70
+ * Returns the public blob URL.
71
+ */
72
+ async uploadBase64Image(base64, filename, contentType) {
73
+ const res = await fetch(`${this.apiBase}/api/mcp/upload-base64`, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ Authorization: this.authHeader
78
+ },
79
+ body: JSON.stringify({
80
+ image: base64,
81
+ filename: filename ?? `mcp-upload-${Date.now()}.png`,
82
+ contentType: contentType ?? "image/png"
83
+ })
84
+ });
85
+ if (!res.ok) throw await this.parseError(res);
86
+ const data = await res.json();
87
+ return data.url;
88
+ }
89
+ /**
90
+ * Fetch a public image URL, base64-encode it, and upload via uploadBase64Image.
91
+ * Lets agents skip the encode-and-stream step when they already have a hosted image.
92
+ * Caps at 4MB to match the upload route's limit.
93
+ */
94
+ async uploadImageFromUrl(url, filename) {
95
+ const res = await fetch(url);
96
+ if (!res.ok) {
97
+ throw new VibedsgnApiError(
98
+ "UPLOAD_FETCH_FAILED",
99
+ res.status,
100
+ `Failed to fetch image from ${url}: ${res.status} ${res.statusText}`
101
+ );
102
+ }
103
+ const contentType = res.headers.get("content-type") ?? "image/png";
104
+ if (!contentType.startsWith("image/")) {
105
+ throw new VibedsgnApiError(
106
+ "UPLOAD_INVALID_TYPE",
107
+ 400,
108
+ `URL did not return an image (content-type: ${contentType})`
109
+ );
110
+ }
111
+ const buffer = await res.arrayBuffer();
112
+ if (buffer.byteLength > 4 * 1024 * 1024) {
113
+ throw new VibedsgnApiError(
114
+ "UPLOAD_TOO_LARGE",
115
+ 413,
116
+ `Image at ${url} is ${(buffer.byteLength / 1024 / 1024).toFixed(2)} MB (max 4 MB)`
117
+ );
118
+ }
119
+ const base64 = Buffer.from(buffer).toString("base64");
120
+ const ext = contentType.split("/")[1]?.split(";")[0] ?? "png";
121
+ return this.uploadBase64Image(base64, filename ?? `mcp-fetched-${Date.now()}.${ext}`, contentType);
122
+ }
123
+ /**
124
+ * Create and publish a vibe via the MCP vibes route.
125
+ * Publishes immediately, no draft step. Returns absolute URL.
126
+ */
127
+ async createVibe(params) {
128
+ const res = await fetch(`${this.apiBase}/api/mcp/vibes`, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Authorization: this.authHeader
133
+ },
134
+ body: JSON.stringify(params)
135
+ });
136
+ if (!res.ok) throw await this.parseError(res);
137
+ return res.json();
138
+ }
139
+ /**
140
+ * Delete a vibe owned by the authenticated user. Returns 404 if not found
141
+ * or owned by a different user (we don't distinguish, to avoid info leak).
142
+ */
143
+ async deleteVibe(id) {
144
+ const res = await fetch(`${this.apiBase}/api/mcp/vibes/${encodeURIComponent(id)}`, {
145
+ method: "DELETE",
146
+ headers: { Authorization: this.authHeader }
147
+ });
148
+ if (!res.ok) throw await this.parseError(res);
149
+ return res.json();
150
+ }
151
+ /**
152
+ * List available design tools, optionally filtered.
153
+ * - category: exact match on category slug
154
+ * - slug: exact match on tool slug (case-insensitive)
155
+ * - q: fuzzy substring match across name/slug/description
156
+ * Multiple filters combine with AND.
157
+ */
158
+ async listDesignTools(opts) {
159
+ const params = new URLSearchParams();
160
+ if (opts?.category) params.set("category", opts.category);
161
+ if (opts?.slug) params.set("slug", opts.slug);
162
+ if (opts?.q) params.set("q", opts.q);
163
+ const qs = params.toString();
164
+ const url = `${this.apiBase}/api/mcp/tools${qs ? `?${qs}` : ""}`;
165
+ const res = await fetch(url, {
166
+ method: "GET",
167
+ headers: { Authorization: this.authHeader }
168
+ });
169
+ if (!res.ok) throw await this.parseError(res);
170
+ const data = await res.json();
171
+ return data.tools;
172
+ }
173
+ };
174
+
175
+ // src/tools/whoami.ts
176
+ var import_zod = require("zod");
177
+ function registerWhoami(server2, client2) {
178
+ server2.registerTool(
179
+ "whoami",
180
+ {
181
+ title: "Whoami",
182
+ description: "Verify the current API key and return the authenticated Vibedsgn user. Call this FIRST before doing any real work \u2014 it confirms the key is valid and tells you which account vibes will be posted under. Cheap (no DB writes), and lets you fail fast with a clear error if auth is misconfigured.",
183
+ inputSchema: import_zod.z.object({}).shape
184
+ },
185
+ async () => {
186
+ try {
187
+ const user = await client2.whoami();
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: JSON.stringify({
193
+ success: true,
194
+ user: {
195
+ id: user.id,
196
+ name: user.name,
197
+ handle: user.handle,
198
+ image: user.image
199
+ }
200
+ })
201
+ }
202
+ ]
203
+ };
204
+ } catch (err) {
205
+ const apiErr = err instanceof VibedsgnApiError ? err : null;
206
+ const code = apiErr?.code ?? "INTERNAL";
207
+ const message = err instanceof Error ? err.message : "Unknown error";
208
+ console.error(`[vibedsgn-mcp] whoami error (${code}): ${message}`);
209
+ return {
210
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: { code, message } }) }],
211
+ isError: true
212
+ };
213
+ }
214
+ }
215
+ );
216
+ }
217
+
218
+ // src/tools/list-design-tools.ts
219
+ var import_zod2 = require("zod");
220
+ var ListDesignToolsInputSchema = import_zod2.z.object({
221
+ category: import_zod2.z.string().optional().describe(
222
+ "Filter to a single tool category. Common values: 'ai-design', 'ai-agents', 'design-skills', 'ai-stack'."
223
+ ),
224
+ slug: import_zod2.z.string().optional().describe("Look up a single tool by slug (case-insensitive). Use this when you already know the exact tool."),
225
+ q: import_zod2.z.string().optional().describe(
226
+ "Fuzzy substring match across name, slug, and description. Use this when you have a hint like 'cursor', 'midjourney', or 'next.js'."
227
+ )
228
+ });
229
+ function registerListDesignTools(server2, client2) {
230
+ server2.registerTool(
231
+ "list_design_tools",
232
+ {
233
+ title: "List Design Tools",
234
+ description: "List the design tool registry on Vibedsgn so you can pick a valid `primaryToolId` for `post_vibe`. Returns id, name, slug, category, and description for each tool. Always call this BEFORE `post_vibe` unless you already have a tool id from a previous call. Use the `q` filter when you know the tool name (e.g. `q: 'cursor'`), the `category` filter to browse a section, or `slug` for an exact lookup. If you're unsure, call without arguments to see all tools.",
235
+ inputSchema: ListDesignToolsInputSchema.shape
236
+ },
237
+ async (args) => {
238
+ try {
239
+ const tools = await client2.listDesignTools({
240
+ category: args.category,
241
+ slug: args.slug,
242
+ q: args.q
243
+ });
244
+ return {
245
+ content: [
246
+ {
247
+ type: "text",
248
+ text: JSON.stringify({
249
+ success: true,
250
+ count: tools.length,
251
+ tools: tools.map((t) => ({
252
+ id: t.id,
253
+ name: t.name,
254
+ slug: t.slug,
255
+ category: t.category,
256
+ description: t.description
257
+ }))
258
+ })
259
+ }
260
+ ]
261
+ };
262
+ } catch (err) {
263
+ const apiErr = err instanceof VibedsgnApiError ? err : null;
264
+ const code = apiErr?.code ?? "INTERNAL";
265
+ const message = err instanceof Error ? err.message : "Unknown error";
266
+ console.error(`[vibedsgn-mcp] list_design_tools error (${code}): ${message}`);
267
+ return {
268
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: { code, message } }) }],
269
+ isError: true
270
+ };
271
+ }
272
+ }
273
+ );
274
+ }
275
+
276
+ // src/tools/post-vibe.ts
277
+ var import_zod3 = require("zod");
278
+ var AiStackSchema = import_zod3.z.object({
279
+ codingAgents: import_zod3.z.array(import_zod3.z.string()).optional().default([]),
280
+ imageGen: import_zod3.z.array(import_zod3.z.string()).optional().default([]),
281
+ videoGen: import_zod3.z.array(import_zod3.z.string()).optional().default([]),
282
+ other: import_zod3.z.array(import_zod3.z.string()).optional().default([])
283
+ }).optional().describe("AI tools used in the project. All inner arrays are optional \u2014 pass only what you used.");
284
+ var DesignSystemSchema = import_zod3.z.object({
285
+ colors: import_zod3.z.array(import_zod3.z.string()).optional().default([]),
286
+ typography: import_zod3.z.string().optional().default("")
287
+ }).optional().describe("Design system info. All fields optional.");
288
+ var PostVibeInputSchema = import_zod3.z.object({
289
+ title: import_zod3.z.string().min(1).describe("Project name (required, max 200 chars)."),
290
+ // Image: must provide ONE of desktopImageUrl (preferred) or desktopImage (base64).
291
+ desktopImageUrl: import_zod3.z.string().url().optional().describe(
292
+ "PREFERRED: a public URL to the desktop screenshot. The MCP server fetches it, base64-encodes, and uploads. Faster than sending base64 over stdio. Must be a publicly accessible image URL (https://...). Max 4MB."
293
+ ),
294
+ desktopImage: import_zod3.z.string().optional().describe(
295
+ "FALLBACK: a base64-encoded desktop screenshot (max 4MB decoded). Use this only if you don't have a public URL for the image. Slower than `desktopImageUrl` because base64 doubles the payload size."
296
+ ),
297
+ primaryToolId: import_zod3.z.string().min(1).describe("Tool ID from `list_design_tools`. Must match the `id` field exactly, not the slug or name."),
298
+ description: import_zod3.z.string().optional().describe("Short project description (1-2 sentences ideal)."),
299
+ mobileImageUrl: import_zod3.z.string().url().optional().describe("Optional public URL for the mobile screenshot."),
300
+ mobileImage: import_zod3.z.string().optional().describe("Optional base64-encoded mobile screenshot. Prefer mobileImageUrl."),
301
+ tags: import_zod3.z.array(import_zod3.z.string()).optional().describe("Free-form curation tags (e.g. ['saas', 'dashboard', 'ai-tooling'])."),
302
+ systemPrompt: import_zod3.z.string().optional().describe("The system/generation prompt used to build the design."),
303
+ processNotes: import_zod3.z.string().optional().describe("Notes about the build process \u2014 what worked, what didn't."),
304
+ aiStack: AiStackSchema,
305
+ designSystem: DesignSystemSchema,
306
+ liveUrl: import_zod3.z.string().url().optional().describe("Live URL of the project, if hosted somewhere."),
307
+ githubUrl: import_zod3.z.string().url().optional().describe("GitHub repository URL, if open source."),
308
+ dryRun: import_zod3.z.boolean().optional().default(false).describe(
309
+ "When true, validate inputs and return what would be published (slug, url, primary tool name) WITHOUT actually uploading the screenshot or creating the vibe. Use this to preview-confirm with the user before committing. Default false."
310
+ )
311
+ }).refine(
312
+ (v) => Boolean(v.desktopImageUrl) || Boolean(v.desktopImage) || v.dryRun === true,
313
+ {
314
+ message: "Either `desktopImageUrl` (preferred) or `desktopImage` (base64) is required.",
315
+ path: ["desktopImageUrl"]
316
+ }
317
+ );
318
+ function registerPostVibe(server2, client2) {
319
+ server2.registerTool(
320
+ "post_vibe",
321
+ {
322
+ title: "Post Vibe",
323
+ description: "Publish a design project to Vibedsgn. WORKFLOW: (1) Call `whoami` first to confirm the API key is valid. (2) Call `list_design_tools` (optionally with `q` or `category`) to pick a `primaryToolId`. (3) Call this tool with the title, primaryToolId, and EITHER `desktopImageUrl` (a public URL \u2014 preferred, faster) OR `desktopImage` (base64-encoded PNG, max 4MB). (4) Optional fields like `tags`, `description`, `aiStack`, `designSystem` improve discoverability but aren't required. (5) For preview-without-publish, set `dryRun: true` \u2014 returns the predicted slug and primary tool without uploading or creating anything. Returns the absolute URL of the published vibe and an `id` you can pass to `delete_vibe` if you need to clean up. Vibes are published immediately \u2014 there is no draft step.",
324
+ inputSchema: PostVibeInputSchema.shape
325
+ },
326
+ async (rawArgs) => {
327
+ try {
328
+ const args = PostVibeInputSchema.parse(rawArgs);
329
+ if (args.dryRun) {
330
+ console.error(`[vibedsgn-mcp] Dry-run post_vibe "${args.title}" \u2014 no upload, no DB write.`);
331
+ const placeholderImageUrl = args.desktopImageUrl ?? "dry-run://placeholder.png";
332
+ const dryResult = await client2.createVibe({
333
+ title: args.title,
334
+ desktopImageUrl: placeholderImageUrl,
335
+ primaryToolId: args.primaryToolId,
336
+ description: args.description,
337
+ tags: args.tags,
338
+ systemPrompt: args.systemPrompt,
339
+ processNotes: args.processNotes,
340
+ aiStack: args.aiStack,
341
+ designSystem: args.designSystem,
342
+ liveUrl: args.liveUrl,
343
+ githubUrl: args.githubUrl,
344
+ dryRun: true
345
+ });
346
+ if (!("dryRun" in dryResult)) {
347
+ throw new Error("Expected dry-run response from backend, got a live create result.");
348
+ }
349
+ return {
350
+ content: [
351
+ {
352
+ type: "text",
353
+ text: JSON.stringify({
354
+ success: true,
355
+ dryRun: true,
356
+ wouldCreate: dryResult.wouldCreate,
357
+ message: "Dry run only \u2014 no vibe was created. Call again with dryRun: false to publish."
358
+ })
359
+ }
360
+ ]
361
+ };
362
+ }
363
+ let desktopImageUrl;
364
+ if (args.desktopImageUrl) {
365
+ console.error(`[vibedsgn-mcp] Fetching desktop image from URL: ${args.desktopImageUrl}`);
366
+ desktopImageUrl = await client2.uploadImageFromUrl(
367
+ args.desktopImageUrl,
368
+ `${args.title.replace(/\s+/g, "-").toLowerCase()}-desktop.png`
369
+ );
370
+ } else if (args.desktopImage) {
371
+ console.error(`[vibedsgn-mcp] Uploading base64 desktop image (${args.desktopImage.length} chars)...`);
372
+ desktopImageUrl = await client2.uploadBase64Image(
373
+ args.desktopImage,
374
+ `${args.title.replace(/\s+/g, "-").toLowerCase()}-desktop.png`
375
+ );
376
+ } else {
377
+ throw new Error("Either desktopImageUrl or desktopImage is required.");
378
+ }
379
+ let mobileImageUrl;
380
+ if (args.mobileImageUrl) {
381
+ console.error(`[vibedsgn-mcp] Fetching mobile image from URL...`);
382
+ mobileImageUrl = await client2.uploadImageFromUrl(
383
+ args.mobileImageUrl,
384
+ `${args.title.replace(/\s+/g, "-").toLowerCase()}-mobile.png`
385
+ );
386
+ } else if (args.mobileImage) {
387
+ console.error(`[vibedsgn-mcp] Uploading base64 mobile image...`);
388
+ mobileImageUrl = await client2.uploadBase64Image(
389
+ args.mobileImage,
390
+ `${args.title.replace(/\s+/g, "-").toLowerCase()}-mobile.png`
391
+ );
392
+ }
393
+ console.error(`[vibedsgn-mcp] Creating vibe "${args.title}"...`);
394
+ const result = await client2.createVibe({
395
+ title: args.title,
396
+ desktopImageUrl,
397
+ primaryToolId: args.primaryToolId,
398
+ description: args.description,
399
+ mobileImageUrl,
400
+ tags: args.tags,
401
+ systemPrompt: args.systemPrompt,
402
+ processNotes: args.processNotes,
403
+ aiStack: args.aiStack,
404
+ designSystem: args.designSystem,
405
+ liveUrl: args.liveUrl,
406
+ githubUrl: args.githubUrl
407
+ });
408
+ if ("dryRun" in result) {
409
+ throw new Error("Unexpected dry-run response from backend on a live post_vibe call.");
410
+ }
411
+ console.error(`[vibedsgn-mcp] Vibe published: ${result.url}`);
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: JSON.stringify({
417
+ success: true,
418
+ id: result.id,
419
+ slug: result.slug,
420
+ url: result.url,
421
+ // absolute URL — server guarantees full https://...
422
+ message: `Vibe "${args.title}" published. To remove it, call \`delete_vibe\` with id "${result.id}".`
423
+ })
424
+ }
425
+ ]
426
+ };
427
+ } catch (err) {
428
+ const apiErr = err instanceof VibedsgnApiError ? err : null;
429
+ const code = apiErr?.code ?? (err instanceof import_zod3.z.ZodError ? "VALIDATION_ERROR" : "INTERNAL");
430
+ const message = err instanceof Error ? err.message : "Unknown error";
431
+ const details = apiErr?.details ?? (err instanceof import_zod3.z.ZodError ? { issues: err.issues } : void 0);
432
+ console.error(`[vibedsgn-mcp] post_vibe error (${code}): ${message}`);
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: JSON.stringify({ success: false, error: { code, message, details } })
438
+ }
439
+ ],
440
+ isError: true
441
+ };
442
+ }
443
+ }
444
+ );
445
+ }
446
+
447
+ // src/tools/delete-vibe.ts
448
+ var import_zod4 = require("zod");
449
+ var DeleteVibeInputSchema = import_zod4.z.object({
450
+ id: import_zod4.z.string().min(1).describe(
451
+ "The vibe id to delete (returned from `post_vibe`). You can ONLY delete vibes owned by the authenticated user \u2014 attempts to delete someone else's vibe return a not-found error."
452
+ )
453
+ });
454
+ function registerDeleteVibe(server2, client2) {
455
+ server2.registerTool(
456
+ "delete_vibe",
457
+ {
458
+ title: "Delete Vibe",
459
+ description: "Permanently delete a vibe you own (cascades to likes, bookmarks, comments, and notifications). Use the `id` returned by `post_vibe`. If the vibe doesn't exist OR is owned by a different user, returns NOT_FOUND (we don't distinguish, to avoid leaking ownership). This is irreversible \u2014 there is no soft-delete or restore.",
460
+ inputSchema: DeleteVibeInputSchema.shape
461
+ },
462
+ async (args) => {
463
+ try {
464
+ const result = await client2.deleteVibe(args.id);
465
+ return {
466
+ content: [
467
+ {
468
+ type: "text",
469
+ text: JSON.stringify({
470
+ success: true,
471
+ id: result.id,
472
+ message: `Vibe ${result.id} deleted.`
473
+ })
474
+ }
475
+ ]
476
+ };
477
+ } catch (err) {
478
+ const apiErr = err instanceof VibedsgnApiError ? err : null;
479
+ const code = apiErr?.code ?? "INTERNAL";
480
+ const message = err instanceof Error ? err.message : "Unknown error";
481
+ console.error(`[vibedsgn-mcp] delete_vibe error (${code}): ${message}`);
482
+ return {
483
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: { code, message } }) }],
484
+ isError: true
485
+ };
486
+ }
487
+ }
488
+ );
489
+ }
490
+
491
+ // src/index.ts
492
+ console.log = console.error;
493
+ var server = new import_mcp.McpServer({
494
+ name: "vibedsgn-mcp",
495
+ version: "1.2.0"
496
+ });
497
+ var client = new VibedsgnClient();
498
+ registerWhoami(server, client);
499
+ registerListDesignTools(server, client);
500
+ registerPostVibe(server, client);
501
+ registerDeleteVibe(server, client);
502
+ var transport = new import_stdio.StdioServerTransport();
503
+ async function main() {
504
+ await server.connect(transport);
505
+ console.error("[vibedsgn-mcp] Server started on stdio transport (v1.2.0)");
506
+ }
507
+ main().catch((err) => {
508
+ console.error("[vibedsgn-mcp] Fatal error:", err);
509
+ process.exit(1);
510
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node