@vibesharingapp/mcp-server 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +677 -286
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
5
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const os_1 = require("os");
7
10
  // VibeSharing API client
8
11
  class VibesharingClient {
9
12
  baseUrl;
@@ -44,10 +47,10 @@ class VibesharingClient {
44
47
  body: JSON.stringify(params),
45
48
  });
46
49
  }
47
- async deployFiles(prototypeId, files, commitMessage) {
50
+ async deployFiles(prototypeId, files, commitMessage, deployName) {
48
51
  return this.request(`/api/prototypes/${prototypeId}/deploy-code`, {
49
52
  method: "POST",
50
- body: JSON.stringify({ files, commitMessage }),
53
+ body: JSON.stringify({ files, commitMessage, deployName }),
51
54
  });
52
55
  }
53
56
  async listPrototypes() {
@@ -101,10 +104,10 @@ class VibesharingClient {
101
104
  }),
102
105
  });
103
106
  }
104
- async importRepo(prototypeId, repoUrl) {
107
+ async importRepo(prototypeId, repoUrl, deployName) {
105
108
  return this.request("/api/git/import-repo", {
106
109
  method: "POST",
107
- body: JSON.stringify({ prototypeId, repoUrl }),
110
+ body: JSON.stringify({ prototypeId, repoUrl, deployName }),
108
111
  });
109
112
  }
110
113
  async addContextLink(params) {
@@ -131,6 +134,102 @@ class VibesharingClient {
131
134
  });
132
135
  }
133
136
  }
137
+ /**
138
+ * Simple fuzzy text matching. Scores based on:
139
+ * - Exact match → 1.0
140
+ * - Case-insensitive exact → 0.95
141
+ * - Query is a substring → 0.7–0.9 (bonus for matching at word boundaries)
142
+ * - Token overlap (words in common) → 0.3–0.7
143
+ * - Otherwise → 0
144
+ */
145
+ function fuzzyScore(query, target) {
146
+ const q = query.toLowerCase().trim();
147
+ const t = target.toLowerCase().trim();
148
+ if (q === t)
149
+ return 1.0;
150
+ if (t === q)
151
+ return 0.95;
152
+ // Substring match
153
+ if (t.includes(q)) {
154
+ // Bonus if it matches at a word boundary
155
+ const wordBoundary = t.startsWith(q) || t.includes(` ${q}`);
156
+ return wordBoundary ? 0.9 : 0.75;
157
+ }
158
+ if (q.includes(t))
159
+ return 0.7;
160
+ // Token overlap
161
+ const qTokens = q.split(/[\s\-_]+/).filter(Boolean);
162
+ const tTokens = t.split(/[\s\-_]+/).filter(Boolean);
163
+ if (qTokens.length === 0 || tTokens.length === 0)
164
+ return 0;
165
+ let matches = 0;
166
+ for (const qt of qTokens) {
167
+ if (tTokens.some((tt) => tt.includes(qt) || qt.includes(tt))) {
168
+ matches++;
169
+ }
170
+ }
171
+ const overlap = matches / Math.max(qTokens.length, tTokens.length);
172
+ return overlap * 0.7;
173
+ }
174
+ function fuzzyMatch(query, items, getName, threshold = 0.3) {
175
+ return items
176
+ .map((item) => ({ item, score: fuzzyScore(query, getName(item)) }))
177
+ .filter((m) => m.score >= threshold)
178
+ .sort((a, b) => b.score - a.score);
179
+ }
180
+ // ---- Version tracking & What's New ----
181
+ const CURRENT_VERSION = "0.4.1";
182
+ const WHATS_NEW = {
183
+ "0.4.0": [
184
+ "🆕 VibeSharing MCP v0.4.0 — What's New:",
185
+ "",
186
+ "• resolve_target tool — Call this before deploying to confirm the collection,",
187
+ " project name, and deploy URL with the user. Fuzzy-matches names so you don't",
188
+ " need exact IDs.",
189
+ "• Named deployments — Use deploy_name on import_repo or deploy_files to set a",
190
+ " friendly Vercel URL (e.g., 'erg-v3-teams' → erg-v3-teams.vercel.app).",
191
+ "• Fuzzy search — list_collections and list_prototypes now accept a 'search'",
192
+ " parameter to filter results.",
193
+ "• Guardrails — Deploy tools now ask for confirmation when collection or project",
194
+ " info is missing, instead of auto-generating everything silently.",
195
+ ].join("\n"),
196
+ };
197
+ function getWhatsNew() {
198
+ try {
199
+ const configDir = (0, path_1.join)((0, os_1.homedir)(), ".vibesharing");
200
+ const versionFile = (0, path_1.join)(configDir, "mcp-version");
201
+ let lastVersion = null;
202
+ try {
203
+ lastVersion = (0, fs_1.readFileSync)(versionFile, "utf-8").trim();
204
+ }
205
+ catch {
206
+ // First run or file missing
207
+ }
208
+ // Update stored version
209
+ try {
210
+ (0, fs_1.mkdirSync)(configDir, { recursive: true });
211
+ (0, fs_1.writeFileSync)(versionFile, CURRENT_VERSION);
212
+ }
213
+ catch {
214
+ // Non-fatal
215
+ }
216
+ if (lastVersion === CURRENT_VERSION)
217
+ return null;
218
+ // Collect all changelogs newer than lastVersion
219
+ const notes = [];
220
+ for (const [ver, note] of Object.entries(WHATS_NEW)) {
221
+ if (!lastVersion || ver > lastVersion) {
222
+ notes.push(note);
223
+ }
224
+ }
225
+ return notes.length > 0 ? notes.join("\n\n") : null;
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ // Check once at startup, prepend to first tool call
232
+ let pendingWhatsNew = getWhatsNew();
134
233
  // Get configuration from environment
135
234
  const VIBESHARING_URL = process.env.VIBESHARING_URL || "https://vibesharing.app";
136
235
  const VIBESHARING_TOKEN = process.env.VIBESHARING_TOKEN;
@@ -143,7 +242,7 @@ const client = new VibesharingClient(VIBESHARING_URL, VIBESHARING_TOKEN);
143
242
  // Create MCP server
144
243
  const server = new index_js_1.Server({
145
244
  name: "vibesharing",
146
- version: "0.2.0",
245
+ version: "0.4.0",
147
246
  }, {
148
247
  capabilities: {
149
248
  tools: {},
@@ -156,7 +255,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
156
255
  tools: [
157
256
  {
158
257
  name: "register_prototype",
159
- description: "Register a new prototype on VibeSharing. Use this after deploying a prototype to Vercel, Netlify, or any hosting service. Optionally upload source code so colleagues can download it. Returns the VibeSharing URL where the team can view and leave feedback.",
258
+ description: "Register a new prototype on VibeSharing. IMPORTANT: Before calling this, use resolve_target to confirm the collection and project name with the user. Do not auto-generate names without user confirmation. Returns the VibeSharing URL where the team can view and leave feedback.",
160
259
  inputSchema: {
161
260
  type: "object",
162
261
  properties: {
@@ -194,18 +293,28 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
194
293
  },
195
294
  {
196
295
  name: "list_prototypes",
197
- description: "List all prototypes in your VibeSharing organization. Shows name, URL, and recent activity.",
296
+ description: "List all prototypes in your VibeSharing organization. Shows name, URL, and recent activity. Optionally filter by search query (fuzzy matched).",
198
297
  inputSchema: {
199
298
  type: "object",
200
- properties: {},
299
+ properties: {
300
+ search: {
301
+ type: "string",
302
+ description: "Optional: fuzzy search query to filter prototypes by name (e.g., 'erg' or 'dashboard')",
303
+ },
304
+ },
201
305
  },
202
306
  },
203
307
  {
204
308
  name: "list_collections",
205
- description: "List all collections (folders) in your VibeSharing organization. Use this to find the collection_id when registering prototypes.",
309
+ description: "List all collections (folders) in your VibeSharing organization. Use this to find the collection_id when registering prototypes. Optionally filter by search query (fuzzy matched).",
206
310
  inputSchema: {
207
311
  type: "object",
208
- properties: {},
312
+ properties: {
313
+ search: {
314
+ type: "string",
315
+ description: "Optional: fuzzy search query to filter collections by name (e.g., 'hero' or 'compliance')",
316
+ },
317
+ },
209
318
  },
210
319
  },
211
320
  {
@@ -276,7 +385,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
276
385
  },
277
386
  {
278
387
  name: "deploy_prototype",
279
- description: "Deploy code directly to VibeSharing. This deploys your code to Vercel and registers it as a prototype in one step. Use this when you've just built something and want to share it with the team immediately.",
388
+ description: "Deploy code directly to VibeSharing. This deploys your code to Vercel and registers it as a prototype in one step. IMPORTANT: Before calling this, use resolve_target to confirm the collection, project name, and deploy name with the user. Do not deploy without user confirmation on where it should go.",
280
389
  inputSchema: {
281
390
  type: "object",
282
391
  properties: {
@@ -316,7 +425,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
316
425
  },
317
426
  {
318
427
  name: "deploy_files",
319
- description: "Deploy a multi-file Next.js project to VibeSharing. This pushes files to a GitHub repo, deploys to Vercel, and makes the prototype live. Use this for deploying a full project directory (not just a single code string). Requires an existing prototype ID create one first with register_prototype.",
428
+ description: "Deploy a multi-file Next.js project to VibeSharing. Pushes files to GitHub, deploys to Vercel. Requires an existing prototype ID. IMPORTANT: Before calling this, use resolve_target to confirm the target prototype with the user. If the user hasn't specified where to deploy, do NOT proceed — ask first.",
320
429
  inputSchema: {
321
430
  type: "object",
322
431
  properties: {
@@ -346,13 +455,17 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
346
455
  type: "string",
347
456
  description: "Optional: Git commit message (default: 'Deploy via MCP')",
348
457
  },
458
+ deploy_name: {
459
+ type: "string",
460
+ description: "Optional: Friendly name for the Vercel project URL (e.g., 'erg-v3-teams' → erg-v3-teams.vercel.app). On redeploy, renames the Vercel project if different from current name.",
461
+ },
349
462
  },
350
463
  required: ["prototype_id", "files"],
351
464
  },
352
465
  },
353
466
  {
354
467
  name: "import_repo",
355
- description: "Import an existing GitHub repo into VibeSharing. Pulls the code into a VibeSharing-hosted repo and deploys it to Vercel automatically. Use this when you already have a repo on GitHub and want to share it as a prototype. If no prototype_id is provided, a new prototype is created automatically.",
468
+ description: "Import an existing GitHub repo into VibeSharing. Pulls the code into a VibeSharing-hosted repo and deploys it to Vercel. IMPORTANT: Before calling this, use resolve_target to confirm the collection, project name, and deploy name with the user. Do not import without user confirmation on where it should go and what it should be called.",
356
469
  inputSchema: {
357
470
  type: "object",
358
471
  properties: {
@@ -380,10 +493,35 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
380
493
  type: "string",
381
494
  description: "Optional: Description of the prototype.",
382
495
  },
496
+ deploy_name: {
497
+ type: "string",
498
+ description: "Optional: Friendly name for the Vercel project URL (e.g., 'erg-v3-teams' → erg-v3-teams.vercel.app). Lowercase, hyphens allowed, max 100 chars. Auto-derived from 'name' if omitted.",
499
+ },
383
500
  },
384
501
  required: ["repo_url"],
385
502
  },
386
503
  },
504
+ {
505
+ name: "resolve_target",
506
+ description: "CALL THIS BEFORE deploying or registering a prototype when the user hasn't provided exact IDs. Fuzzy-matches collection and project names, suggests where to put the prototype, and checks deploy_name availability. Returns structured options so you can confirm with the user before proceeding.",
507
+ inputSchema: {
508
+ type: "object",
509
+ properties: {
510
+ collection_name: {
511
+ type: "string",
512
+ description: "Approximate collection name to search for (e.g., 'hero use cases'). Fuzzy matched.",
513
+ },
514
+ project_name: {
515
+ type: "string",
516
+ description: "Approximate project/prototype name to search for (e.g., 'ERG v3'). Fuzzy matched.",
517
+ },
518
+ deploy_name: {
519
+ type: "string",
520
+ description: "Desired Vercel deploy name to check availability for (e.g., 'erg-v3-teams').",
521
+ },
522
+ },
523
+ },
524
+ },
387
525
  {
388
526
  name: "add_context_link",
389
527
  description: "Attach a reference link or note to a collection or project/prototype. Use this to add links to Figma designs, PRDs, Confluence docs, or free-text notes that provide context for reviewers.",
@@ -451,367 +589,620 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
451
589
  // Handle tool calls
452
590
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
453
591
  const { name, arguments: args } = request.params;
454
- try {
455
- switch (name) {
456
- case "register_prototype": {
457
- const params = args;
458
- const { source_code, source_filename, ...registerParams } = params;
459
- const result = await client.registerPrototype(registerParams);
460
- const protoId = result.prototype?.id;
461
- // Upload source code if provided
462
- let sourceInfo = "";
463
- if (source_code && protoId) {
464
- try {
465
- const sourceResult = await client.uploadSource(protoId, source_code, source_filename);
466
- sourceInfo = `\nSource code uploaded (${sourceResult.source?.size || source_code.length} chars). Colleagues can download it from the prototype page.`;
592
+ const toolResult = await (async () => {
593
+ try {
594
+ switch (name) {
595
+ case "register_prototype": {
596
+ const params = args;
597
+ // Guardrail: if no collection specified, bounce back with options
598
+ if (!params.collection_id) {
599
+ const [guardCollResult, guardProtoResult] = await Promise.all([
600
+ client.listCollections(),
601
+ client.listPrototypes(),
602
+ ]);
603
+ const guardCollections = guardCollResult.collections || [];
604
+ const guardPrototypes = guardProtoResult.prototypes || [];
605
+ const similarProjects = fuzzyMatch(params.name, guardPrototypes, (p) => p.name).slice(0, 5);
606
+ const sections = [
607
+ `HOLD ON — confirm with the user before registering "${params.name}":\n`,
608
+ ];
609
+ if (similarProjects.length > 0) {
610
+ sections.push(`Similar existing projects:\n` +
611
+ similarProjects.map((m) => {
612
+ const p = m.item;
613
+ return ` - "${p.name}" (ID: ${p.id})`;
614
+ }).join("\n") +
615
+ `\n\nAsk the user: Is this an update to one of these, or a new project?`);
616
+ }
617
+ if (guardCollections.length > 0) {
618
+ sections.push(`\nNo collection specified. Available collections:\n` +
619
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
620
+ `\n\nAsk the user: Which collection should "${params.name}" go in?`);
621
+ }
622
+ return {
623
+ content: [
624
+ {
625
+ type: "text",
626
+ text: sections.join("\n"),
627
+ },
628
+ ],
629
+ };
467
630
  }
468
- catch (err) {
469
- sourceInfo = `\nWarning: Source upload failed: ${err instanceof Error ? err.message : "Unknown error"}`;
631
+ const { source_code, source_filename, ...registerParams } = params;
632
+ const result = await client.registerPrototype(registerParams);
633
+ const protoId = result.prototype?.id;
634
+ // Upload source code if provided
635
+ let sourceInfo = "";
636
+ if (source_code && protoId) {
637
+ try {
638
+ const sourceResult = await client.uploadSource(protoId, source_code, source_filename);
639
+ sourceInfo = `\nSource code uploaded (${sourceResult.source?.size || source_code.length} chars). Colleagues can download it from the prototype page.`;
640
+ }
641
+ catch (err) {
642
+ sourceInfo = `\nWarning: Source upload failed: ${err instanceof Error ? err.message : "Unknown error"}`;
643
+ }
470
644
  }
471
- }
472
- return {
473
- content: [
474
- {
475
- type: "text",
476
- text: `Prototype registered successfully!\n\nName: ${result.prototype?.name || params.name}\nVibeSharing URL: ${VIBESHARING_URL}/dashboard/projects/${protoId}\n${params.external_url ? `Live URL: ${params.external_url}` : ""}${sourceInfo}\n\nYour team can now view and leave feedback on this prototype.`,
477
- },
478
- ],
479
- };
480
- }
481
- case "list_prototypes": {
482
- const result = await client.listPrototypes();
483
- const prototypes = result.prototypes || [];
484
- if (prototypes.length === 0) {
485
645
  return {
486
646
  content: [
487
647
  {
488
648
  type: "text",
489
- text: "No prototypes found. Use register_prototype to add your first one!",
649
+ text: `Prototype registered successfully!\n\nName: ${result.prototype?.name || params.name}\nVibeSharing URL: ${VIBESHARING_URL}/dashboard/projects/${protoId}\n${params.external_url ? `Live URL: ${params.external_url}` : ""}${sourceInfo}\n\nYour team can now view and leave feedback on this prototype.`,
490
650
  },
491
651
  ],
492
652
  };
493
653
  }
494
- const list = prototypes
495
- .map((p) => `- ${p.name}\n ID: ${p.id}\n ${p.external_url ? `URL: ${p.external_url}\n ` : ""}Updated: ${new Date(p.updated_at).toLocaleDateString()}`)
496
- .join("\n\n");
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: `Found ${prototypes.length} prototype(s):\n\n${list}`,
502
- },
503
- ],
504
- };
505
- }
506
- case "list_collections": {
507
- const result = await client.listCollections();
508
- const collections = result.collections || [];
509
- if (collections.length === 0) {
654
+ case "list_prototypes": {
655
+ const { search: protoSearch } = (args || {});
656
+ const result = await client.listPrototypes();
657
+ let prototypes = result.prototypes || [];
658
+ if (protoSearch) {
659
+ const matches = fuzzyMatch(protoSearch, prototypes, (p) => p.name);
660
+ prototypes = matches.map((m) => m.item);
661
+ }
662
+ if (prototypes.length === 0) {
663
+ return {
664
+ content: [
665
+ {
666
+ type: "text",
667
+ text: protoSearch
668
+ ? `No prototypes matching "${protoSearch}". Use list_prototypes without search to see all.`
669
+ : "No prototypes found. Use register_prototype to add your first one!",
670
+ },
671
+ ],
672
+ };
673
+ }
674
+ const list = prototypes
675
+ .map((p) => `- ${p.name}\n ID: ${p.id}\n ${p.external_url ? `URL: ${p.external_url}\n ` : ""}Updated: ${new Date(p.updated_at).toLocaleDateString()}`)
676
+ .join("\n\n");
510
677
  return {
511
678
  content: [
512
679
  {
513
680
  type: "text",
514
- text: "No collections found. Create one in the VibeSharing dashboard first.",
681
+ text: `Found ${prototypes.length} prototype(s)${protoSearch ? ` matching "${protoSearch}"` : ""}:\n\n${list}`,
515
682
  },
516
683
  ],
517
684
  };
518
685
  }
519
- const list = collections
520
- .map((c) => `- ${c.name}\n ID: ${c.id}${c.description ? `\n ${c.description}` : ""}`)
521
- .join("\n\n");
522
- return {
523
- content: [
524
- {
525
- type: "text",
526
- text: `Found ${collections.length} collection(s):\n\n${list}`,
527
- },
528
- ],
529
- };
530
- }
531
- case "get_feedback": {
532
- const { project_id } = args;
533
- const result = await client.getFeedback(project_id);
534
- const feedback = result.feedback || [];
535
- if (feedback.length === 0) {
686
+ case "list_collections": {
687
+ const { search: collSearch } = (args || {});
688
+ const result = await client.listCollections();
689
+ let collections = result.collections || [];
690
+ if (collSearch) {
691
+ const matches = fuzzyMatch(collSearch, collections, (c) => c.name);
692
+ collections = matches.map((m) => m.item);
693
+ }
694
+ if (collections.length === 0) {
695
+ return {
696
+ content: [
697
+ {
698
+ type: "text",
699
+ text: collSearch
700
+ ? `No collections matching "${collSearch}". Use list_collections without search to see all.`
701
+ : "No collections found. Create one in the VibeSharing dashboard first.",
702
+ },
703
+ ],
704
+ };
705
+ }
706
+ const list = collections
707
+ .map((c) => `- ${c.name}\n ID: ${c.id}${c.description ? `\n ${c.description}` : ""}`)
708
+ .join("\n\n");
536
709
  return {
537
710
  content: [
538
711
  {
539
712
  type: "text",
540
- text: "No feedback yet for this prototype. Share it with your team to get their thoughts!",
713
+ text: `Found ${collections.length} collection(s)${collSearch ? ` matching "${collSearch}"` : ""}:\n\n${list}`,
541
714
  },
542
715
  ],
543
716
  };
544
717
  }
545
- const feedbackList = feedback
546
- .map((f) => {
547
- const status = f.resolved_at ? " [Resolved]" : "";
548
- const replies = f.replies && f.replies.length > 0
549
- ? `\n Replies: ${f.replies.length}`
550
- : "";
551
- return `- ${f.user_name}${status}: "${f.content}"\n ${new Date(f.created_at).toLocaleDateString()}${replies}`;
552
- })
553
- .join("\n\n");
554
- return {
555
- content: [
556
- {
557
- type: "text",
558
- text: `Feedback (${feedback.length} items):\n\n${feedbackList}`,
559
- },
560
- ],
561
- };
562
- }
563
- case "sync_context": {
564
- const { project_id, content } = args;
565
- await client.syncContext(project_id, content);
566
- return {
567
- content: [
568
- {
569
- type: "text",
570
- text: `Context synced successfully to VibeSharing!\n\nProject ID: ${project_id}\nContent length: ${content.length} characters\n\nThis context will be available to team members viewing the prototype.`,
571
- },
572
- ],
573
- };
574
- }
575
- case "verify_token": {
576
- const result = await client.verifyToken();
577
- if (result.valid) {
718
+ case "get_feedback": {
719
+ const { project_id } = args;
720
+ const result = await client.getFeedback(project_id);
721
+ const feedback = result.feedback || [];
722
+ if (feedback.length === 0) {
723
+ return {
724
+ content: [
725
+ {
726
+ type: "text",
727
+ text: "No feedback yet for this prototype. Share it with your team to get their thoughts!",
728
+ },
729
+ ],
730
+ };
731
+ }
732
+ const feedbackList = feedback
733
+ .map((f) => {
734
+ const status = f.resolved_at ? " [Resolved]" : "";
735
+ const replies = f.replies && f.replies.length > 0
736
+ ? `\n Replies: ${f.replies.length}`
737
+ : "";
738
+ return `- ${f.user_name}${status}: "${f.content}"\n ${new Date(f.created_at).toLocaleDateString()}${replies}`;
739
+ })
740
+ .join("\n\n");
578
741
  return {
579
742
  content: [
580
743
  {
581
744
  type: "text",
582
- text: `Token is valid! Connected to ${VIBESHARING_URL}\n\nYour organization has ${result.prototypeCount} prototype(s).`,
745
+ text: `Feedback (${feedback.length} items):\n\n${feedbackList}`,
583
746
  },
584
747
  ],
585
748
  };
586
749
  }
587
- else {
750
+ case "sync_context": {
751
+ const { project_id, content } = args;
752
+ await client.syncContext(project_id, content);
588
753
  return {
589
754
  content: [
590
755
  {
591
756
  type: "text",
592
- text: `Token is invalid: ${result.error}\n\nGet a new deploy token from VibeSharing → Dashboard → Account Settings.\nThen update your MCP config:\n claude mcp remove vibesharing\n claude mcp add vibesharing -e VIBESHARING_TOKEN=vs_YOUR_NEW_TOKEN -- node /path/to/mcp-server/dist/index.js`,
757
+ text: `Context synced successfully to VibeSharing!\n\nProject ID: ${project_id}\nContent length: ${content.length} characters\n\nThis context will be available to team members viewing the prototype.`,
593
758
  },
594
759
  ],
595
- isError: true,
596
760
  };
597
761
  }
598
- }
599
- case "upload_source": {
600
- const { prototype_id, source_code, filename, storage_option } = args;
601
- const result = await client.uploadSource(prototype_id, source_code, filename, storage_option);
602
- return {
603
- content: [
604
- {
605
- type: "text",
606
- text: `Source code uploaded successfully!\n\nPrototype ID: ${prototype_id}\nFilename: ${result.source?.filename || filename || "page.tsx"}\nSize: ${result.source?.size || source_code.length} characters\nStorage: ${result.source?.storage_option || storage_option || "permanent"}\n\nColleagues can download this from: ${VIBESHARING_URL}/dashboard/projects/${prototype_id}`,
607
- },
608
- ],
609
- };
610
- }
611
- case "deploy_prototype": {
612
- const { code, name, prototype_id } = args;
613
- // If no prototype_id, first register a new prototype
614
- let prototypeId = prototype_id;
615
- if (!prototypeId) {
616
- const registered = await client.registerPrototype({ name });
617
- prototypeId = registered.prototype?.id;
762
+ case "verify_token": {
763
+ const result = await client.verifyToken();
764
+ if (result.valid) {
765
+ return {
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: `Token is valid! Connected to ${VIBESHARING_URL}\n\nYour organization has ${result.prototypeCount} prototype(s).`,
770
+ },
771
+ ],
772
+ };
773
+ }
774
+ else {
775
+ return {
776
+ content: [
777
+ {
778
+ type: "text",
779
+ text: `Token is invalid: ${result.error}\n\nGet a new deploy token from VibeSharing → Dashboard → Account Settings.\nThen update your MCP config:\n claude mcp remove vibesharing\n claude mcp add vibesharing -e VIBESHARING_TOKEN=vs_YOUR_NEW_TOKEN -- node /path/to/mcp-server/dist/index.js`,
780
+ },
781
+ ],
782
+ isError: true,
783
+ };
784
+ }
618
785
  }
619
- if (!prototypeId) {
786
+ case "upload_source": {
787
+ const { prototype_id, source_code, filename, storage_option } = args;
788
+ const result = await client.uploadSource(prototype_id, source_code, filename, storage_option);
620
789
  return {
621
790
  content: [
622
791
  {
623
792
  type: "text",
624
- text: "Error: Could not create prototype. Please try again.",
793
+ text: `Source code uploaded successfully!\n\nPrototype ID: ${prototype_id}\nFilename: ${result.source?.filename || filename || "page.tsx"}\nSize: ${result.source?.size || source_code.length} characters\nStorage: ${result.source?.storage_option || storage_option || "permanent"}\n\nColleagues can download this from: ${VIBESHARING_URL}/dashboard/projects/${prototype_id}`,
625
794
  },
626
795
  ],
627
- isError: true,
628
796
  };
629
797
  }
630
- const result = await client.deployPrototype({
631
- code,
632
- prototypeName: name,
633
- prototypeId,
634
- });
635
- return {
636
- content: [
637
- {
638
- type: "text",
639
- text: `Deployed successfully!\n\nLive URL: ${result.deployedUrl}\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${prototypeId}\n\nYour team can now view the prototype and leave feedback.${result.contextImported ? "\n\nProject context was automatically imported." : ""}`,
640
- },
641
- ],
642
- };
643
- }
644
- case "create_collection": {
645
- const { name: collName, description: collDesc } = args;
646
- const collResult = await client.createCollection({
647
- name: collName,
648
- description: collDesc,
649
- });
650
- const coll = collResult.collection;
651
- return {
652
- content: [
653
- {
654
- type: "text",
655
- text: `Collection created!\n\nName: ${coll.name}\nID: ${coll.id}\nSlug: ${coll.slug}\n\nYou can now use this collection_id when registering prototypes with register_prototype.`,
656
- },
657
- ],
658
- };
659
- }
660
- case "deploy_files": {
661
- const { prototype_id: deployProtoId, files, commit_message } = args;
662
- const deployResult = await client.deployFiles(deployProtoId, files, commit_message || "Deploy via MCP");
663
- return {
664
- content: [
665
- {
666
- type: "text",
667
- text: `Deployed ${files.length} files!\n\nLive URL: ${deployResult.deployUrl || "Deploying..."}\nRepo: ${deployResult.repoUrl || "N/A"}\nCommit: ${deployResult.commitSha || "N/A"}\n\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${deployProtoId}\n\nYour team can now view the prototype and leave feedback.`,
668
- },
669
- ],
670
- };
671
- }
672
- case "import_repo": {
673
- const { repo_url, name: importName, prototype_id: importProtoId, collection_id: importCollectionId, parent_project_id: importParentId, description: importDesc, } = args;
674
- // Auto-create prototype if no ID provided
675
- let protoId = importProtoId;
676
- if (!protoId) {
677
- // Derive name from repo URL if not provided
678
- const repoName = repo_url.replace(/\.git$/, "").split("/").pop() || "imported-prototype";
679
- const protoName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, c => c.toUpperCase());
680
- const registered = await client.registerPrototype({
681
- name: protoName,
682
- description: importDesc || `Imported from ${repo_url}`,
683
- collection_id: importCollectionId,
684
- parent_project_id: importParentId,
798
+ case "deploy_prototype": {
799
+ const { code, name, prototype_id } = args;
800
+ // Guardrail: if no existing prototype specified, bounce back with options
801
+ if (!prototype_id) {
802
+ const [guardCollResult, guardProtoResult] = await Promise.all([
803
+ client.listCollections(),
804
+ client.listPrototypes(),
805
+ ]);
806
+ const guardCollections = guardCollResult.collections || [];
807
+ const guardPrototypes = guardProtoResult.prototypes || [];
808
+ const similarProjects = fuzzyMatch(name, guardPrototypes, (p) => p.name).slice(0, 5);
809
+ const sections = [
810
+ `HOLD ON — confirm with the user before deploying "${name}":\n`,
811
+ ];
812
+ if (similarProjects.length > 0) {
813
+ sections.push(`Similar existing projects:\n` +
814
+ similarProjects.map((m) => {
815
+ const p = m.item;
816
+ return ` - "${p.name}" (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`;
817
+ }).join("\n") +
818
+ `\n\nAsk the user: Is this an UPDATE to one of these, or a NEW project?`);
819
+ }
820
+ if (guardCollections.length > 0) {
821
+ sections.push(`\nAvailable collections:\n` +
822
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
823
+ `\n\nAsk the user: Which collection should this go in?`);
824
+ }
825
+ const suggestedSlug = name
826
+ .toLowerCase()
827
+ .replace(/[^a-z0-9]+/g, "-")
828
+ .replace(/^-+|-+$/g, "");
829
+ sections.push(`\nSuggested deploy name: "${suggestedSlug}" https://${suggestedSlug}.vercel.app\n` +
830
+ `Ask the user: Do you want to use this name or choose a different one?`);
831
+ return {
832
+ content: [
833
+ {
834
+ type: "text",
835
+ text: sections.join("\n"),
836
+ },
837
+ ],
838
+ };
839
+ }
840
+ // If no prototype_id, first register a new prototype
841
+ let prototypeId = prototype_id;
842
+ if (!prototypeId) {
843
+ const registered = await client.registerPrototype({ name });
844
+ prototypeId = registered.prototype?.id;
845
+ }
846
+ if (!prototypeId) {
847
+ return {
848
+ content: [
849
+ {
850
+ type: "text",
851
+ text: "Error: Could not create prototype. Please try again.",
852
+ },
853
+ ],
854
+ isError: true,
855
+ };
856
+ }
857
+ const result = await client.deployPrototype({
858
+ code,
859
+ prototypeName: name,
860
+ prototypeId,
685
861
  });
686
- protoId = registered.prototype?.id;
862
+ return {
863
+ content: [
864
+ {
865
+ type: "text",
866
+ text: `Deployed successfully!\n\nLive URL: ${result.deployedUrl}\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${prototypeId}\n\nYour team can now view the prototype and leave feedback.${result.contextImported ? "\n\nProject context was automatically imported." : ""}`,
867
+ },
868
+ ],
869
+ };
687
870
  }
688
- if (!protoId) {
871
+ case "create_collection": {
872
+ const { name: collName, description: collDesc } = args;
873
+ const collResult = await client.createCollection({
874
+ name: collName,
875
+ description: collDesc,
876
+ });
877
+ const coll = collResult.collection;
689
878
  return {
690
879
  content: [
691
880
  {
692
881
  type: "text",
693
- text: "Error: Could not create prototype. Please try again.",
882
+ text: `Collection created!\n\nName: ${coll.name}\nID: ${coll.id}\nSlug: ${coll.slug}\n\nYou can now use this collection_id when registering prototypes with register_prototype.`,
694
883
  },
695
884
  ],
696
- isError: true,
697
885
  };
698
886
  }
699
- const importResult = await client.importRepo(protoId, repo_url);
700
- return {
701
- content: [
702
- {
703
- type: "text",
704
- text: `Repo imported and deploying!\n\nRepo: ${importResult.repoUrl}\nSource: ${repo_url}\nLive URL: ${importResult.deployUrl}\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${protoId}\nFiles imported: ${importResult.fileCount || "unknown"}\n\nPushes to the VibeSharing repo will auto-deploy to Vercel.`,
705
- },
706
- ],
707
- };
708
- }
709
- case "add_context_link": {
710
- const { folder_id, project_id, title, url, note } = args;
711
- if (!folder_id && !project_id) {
887
+ case "deploy_files": {
888
+ const { prototype_id: deployProtoId, files, commit_message, deploy_name: deployName } = args;
889
+ const deployResult = await client.deployFiles(deployProtoId, files, commit_message || "Deploy via MCP", deployName);
712
890
  return {
713
891
  content: [
714
892
  {
715
893
  type: "text",
716
- text: "Error: Either folder_id or project_id is required.",
894
+ text: `Deployed ${files.length} files!\n\n${deployResult.deployName ? `Deploy name: ${deployResult.deployName}\n` : ""}Live URL: ${deployResult.deployUrl || "Deploying..."}\nRepo: ${deployResult.repoUrl || "N/A"}\nCommit: ${deployResult.commitSha || "N/A"}\n\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${deployProtoId}\n\nYour team can now view the prototype and leave feedback.`,
717
895
  },
718
896
  ],
719
- isError: true,
720
897
  };
721
898
  }
722
- const result = await client.addContextLink({
723
- folderId: folder_id,
724
- projectId: project_id,
725
- title,
726
- url,
727
- note,
728
- });
729
- const parentType = folder_id ? "collection" : "project";
730
- const parentId = folder_id || project_id;
731
- return {
732
- content: [
733
- {
734
- type: "text",
735
- text: `Reference link added successfully!\n\nTitle: ${title}${url ? `\nURL: ${url}` : ""}${note ? `\nNote: ${note}` : ""}\nAttached to ${parentType}: ${parentId}\nLink ID: ${result.link?.id}\n\nView it at: ${VIBESHARING_URL}/dashboard/${folder_id ? "folders" : "projects"}/${parentId}`,
736
- },
737
- ],
738
- };
739
- }
740
- case "list_context_links": {
741
- const { folder_id, project_id } = args;
742
- if (!folder_id && !project_id) {
899
+ case "import_repo": {
900
+ const { repo_url, name: importName, prototype_id: importProtoId, collection_id: importCollectionId, parent_project_id: importParentId, description: importDesc, deploy_name: deployName, } = args;
901
+ // Guardrail: if no collection and no existing prototype specified, bounce back with options
902
+ if (!importCollectionId && !importProtoId) {
903
+ const [guardCollResult, guardProtoResult] = await Promise.all([
904
+ client.listCollections(),
905
+ client.listPrototypes(),
906
+ ]);
907
+ const guardCollections = guardCollResult.collections || [];
908
+ const guardPrototypes = guardProtoResult.prototypes || [];
909
+ const repoName = repo_url.replace(/\.git$/, "").split("/").pop() || "imported-prototype";
910
+ const suggestedName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
911
+ // Check for similar existing prototypes
912
+ const similarProjects = importName
913
+ ? fuzzyMatch(importName, guardPrototypes, (p) => p.name).slice(0, 5)
914
+ : [];
915
+ const sections = [
916
+ `HOLD ON — confirm these details with the user before deploying:\n`,
917
+ `Prototype name: "${suggestedName}"`,
918
+ ];
919
+ if (similarProjects.length > 0) {
920
+ sections.push(`\nSimilar existing projects found:\n` +
921
+ similarProjects.map((m) => {
922
+ const p = m.item;
923
+ return ` - "${p.name}" (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`;
924
+ }).join("\n") +
925
+ `\n\nAsk the user: Is this an UPDATE to one of these existing projects, or a NEW project?`);
926
+ }
927
+ if (guardCollections.length > 0) {
928
+ sections.push(`\nNo collection specified. Available collections:\n` +
929
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
930
+ `\n\nAsk the user: Which collection should "${suggestedName}" go in?`);
931
+ }
932
+ if (!deployName) {
933
+ const suggestedSlug = suggestedName
934
+ .toLowerCase()
935
+ .replace(/[^a-z0-9]+/g, "-")
936
+ .replace(/^-+|-+$/g, "");
937
+ sections.push(`\nNo deploy name specified. Suggested: "${suggestedSlug}" → https://${suggestedSlug}.vercel.app\n` +
938
+ `Ask the user: Do you want to use "${suggestedSlug}" or choose a different deploy name?`);
939
+ }
940
+ return {
941
+ content: [
942
+ {
943
+ type: "text",
944
+ text: sections.join("\n"),
945
+ },
946
+ ],
947
+ };
948
+ }
949
+ // Auto-create prototype if no ID provided (collection was confirmed above)
950
+ let protoId = importProtoId;
951
+ if (!protoId) {
952
+ // Derive name from repo URL if not provided
953
+ const repoName = repo_url.replace(/\.git$/, "").split("/").pop() || "imported-prototype";
954
+ const protoName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
955
+ const registered = await client.registerPrototype({
956
+ name: protoName,
957
+ description: importDesc || `Imported from ${repo_url}`,
958
+ external_url: repo_url,
959
+ collection_id: importCollectionId,
960
+ parent_project_id: importParentId,
961
+ });
962
+ protoId = registered.prototype?.id;
963
+ }
964
+ if (!protoId) {
965
+ return {
966
+ content: [
967
+ {
968
+ type: "text",
969
+ text: "Error: Could not create prototype. Please try again.",
970
+ },
971
+ ],
972
+ isError: true,
973
+ };
974
+ }
975
+ const importResult = await client.importRepo(protoId, repo_url, deployName);
743
976
  return {
744
977
  content: [
745
978
  {
746
979
  type: "text",
747
- text: "Error: Either folder_id or project_id is required.",
980
+ text: `Repo imported and deploying!\n\n${importResult.deployName ? `Deploy name: ${importResult.deployName}\n` : ""}Live URL: ${importResult.deployUrl}\nRepo: ${importResult.repoUrl}\nVibeSharing: ${VIBESHARING_URL}/dashboard/projects/${protoId}\nFiles imported: ${importResult.fileCount || "unknown"}\n\nPushes to the VibeSharing repo will auto-deploy to Vercel.`,
748
981
  },
749
982
  ],
750
- isError: true,
751
983
  };
752
984
  }
753
- const result = await client.listContextLinks({
754
- folderId: folder_id,
755
- projectId: project_id,
756
- });
757
- const links = result.links || [];
758
- if (links.length === 0) {
985
+ case "resolve_target": {
986
+ const { collection_name: collName, project_name: projName, deploy_name: desiredDeployName, } = (args || {});
987
+ // Fetch collections and prototypes in parallel
988
+ const [collResult, protoResult] = await Promise.all([
989
+ client.listCollections(),
990
+ client.listPrototypes(),
991
+ ]);
992
+ const allCollections = collResult.collections || [];
993
+ const allPrototypes = protoResult.prototypes || [];
994
+ const sections = [];
995
+ // --- Collection matching ---
996
+ if (collName) {
997
+ const collMatches = fuzzyMatch(collName, allCollections, (c) => c.name);
998
+ if (collMatches.length === 0) {
999
+ sections.push(`COLLECTION: No match for "${collName}".\n` +
1000
+ `Available collections:\n` +
1001
+ allCollections
1002
+ .map((c) => ` - ${c.name} (ID: ${c.id})`)
1003
+ .join("\n") +
1004
+ `\n\nAsk the user: Which collection should this go in? Or create a new one?`);
1005
+ }
1006
+ else if (collMatches[0].score >= 0.9) {
1007
+ const best = collMatches[0].item;
1008
+ sections.push(`COLLECTION: Matched "${collName}" → "${best.name}" (ID: ${best.id})`);
1009
+ // Show prototypes in this collection for context
1010
+ const collProtos = allPrototypes.filter((p) => p.folder_id === best.id);
1011
+ if (collProtos.length > 0) {
1012
+ sections.push(`Existing projects in "${best.name}":\n` +
1013
+ collProtos
1014
+ .map((p) => ` - ${p.name} (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`)
1015
+ .join("\n") +
1016
+ `\n\nAsk the user: Should this be a NEW project in "${best.name}", or an update to one of these existing projects?`);
1017
+ }
1018
+ }
1019
+ else {
1020
+ // Fuzzy matches but not confident
1021
+ sections.push(`COLLECTION: "${collName}" is not an exact match. Did the user mean one of these?\n` +
1022
+ collMatches
1023
+ .slice(0, 5)
1024
+ .map((m) => {
1025
+ const c = m.item;
1026
+ return ` - "${c.name}" (ID: ${c.id}, confidence: ${Math.round(m.score * 100)}%)`;
1027
+ })
1028
+ .join("\n") +
1029
+ `\n\nAsk the user to confirm which collection.`);
1030
+ }
1031
+ }
1032
+ else {
1033
+ // No collection name given — show all options
1034
+ if (allCollections.length > 0) {
1035
+ sections.push(`COLLECTION: Not specified. Available collections:\n` +
1036
+ allCollections
1037
+ .map((c) => ` - ${c.name} (ID: ${c.id})`)
1038
+ .join("\n") +
1039
+ `\n\nAsk the user: Which collection should this prototype go in? Or create a new one?`);
1040
+ }
1041
+ else {
1042
+ sections.push(`COLLECTION: No collections exist yet. Ask the user if they want to create one.`);
1043
+ }
1044
+ }
1045
+ // --- Project matching ---
1046
+ if (projName) {
1047
+ const projMatches = fuzzyMatch(projName, allPrototypes, (p) => p.name);
1048
+ if (projMatches.length === 0) {
1049
+ sections.push(`PROJECT: No match for "${projName}". This will be a new project.\n` +
1050
+ `Ask the user to confirm the name for the new project.`);
1051
+ }
1052
+ else if (projMatches[0].score >= 0.9) {
1053
+ const best = projMatches[0].item;
1054
+ sections.push(`PROJECT: Matched "${projName}" → "${best.name}" (ID: ${best.id})${best.external_url ? `\n Current URL: ${best.external_url}` : ""}\n\n` +
1055
+ `Ask the user: Deploy as an UPDATE to "${best.name}", or create a NEW project?`);
1056
+ }
1057
+ else {
1058
+ sections.push(`PROJECT: "${projName}" is not an exact match. Close matches:\n` +
1059
+ projMatches
1060
+ .slice(0, 5)
1061
+ .map((m) => {
1062
+ const p = m.item;
1063
+ return ` - "${p.name}" (ID: ${p.id}, confidence: ${Math.round(m.score * 100)}%)`;
1064
+ })
1065
+ .join("\n") +
1066
+ `\n\nAsk the user: Is this an update to one of these, or a new project?`);
1067
+ }
1068
+ }
1069
+ // --- Deploy name ---
1070
+ if (desiredDeployName) {
1071
+ const slug = desiredDeployName
1072
+ .toLowerCase()
1073
+ .replace(/[^a-z0-9]/g, "-")
1074
+ .replace(/-{2,}/g, "-")
1075
+ .replace(/^-+|-+$/g, "");
1076
+ sections.push(`DEPLOY NAME: "${slug}" → will deploy to https://${slug}.vercel.app\n` +
1077
+ `(Availability will be checked at deploy time. If taken, a suffix will be added.)\n\n` +
1078
+ `Ask the user: Do you want to name this deployment "${slug}"?`);
1079
+ }
1080
+ else {
1081
+ sections.push(`DEPLOY NAME: Not specified.\n` +
1082
+ `Ask the user: Do you want a custom deploy name (e.g., "erg-v3-teams" → erg-v3-teams.vercel.app), or auto-generate one?`);
1083
+ }
759
1084
  return {
760
1085
  content: [
761
1086
  {
762
1087
  type: "text",
763
- text: "No reference links found. Use add_context_link to attach Figma designs, PRDs, docs, or notes.",
1088
+ text: sections.join("\n\n---\n\n"),
764
1089
  },
765
1090
  ],
766
1091
  };
767
1092
  }
768
- const list = links
769
- .map((l) => `- ${l.title}\n ID: ${l.id}${l.url ? `\n URL: ${l.url}` : " (note)"}${l.note ? `\n Note: ${l.note}` : ""}\n Added: ${new Date(l.created_at).toLocaleDateString()}`)
770
- .join("\n\n");
771
- return {
772
- content: [
773
- {
774
- type: "text",
775
- text: `Found ${links.length} reference link(s):\n\n${list}`,
776
- },
777
- ],
778
- };
779
- }
780
- case "remove_context_link": {
781
- const { link_id } = args;
782
- await client.removeContextLink(link_id);
783
- return {
784
- content: [
785
- {
786
- type: "text",
787
- text: `Reference link ${link_id} removed successfully.`,
788
- },
789
- ],
790
- };
1093
+ case "add_context_link": {
1094
+ const { folder_id, project_id, title, url, note } = args;
1095
+ if (!folder_id && !project_id) {
1096
+ return {
1097
+ content: [
1098
+ {
1099
+ type: "text",
1100
+ text: "Error: Either folder_id or project_id is required.",
1101
+ },
1102
+ ],
1103
+ isError: true,
1104
+ };
1105
+ }
1106
+ const result = await client.addContextLink({
1107
+ folderId: folder_id,
1108
+ projectId: project_id,
1109
+ title,
1110
+ url,
1111
+ note,
1112
+ });
1113
+ const parentType = folder_id ? "collection" : "project";
1114
+ const parentId = folder_id || project_id;
1115
+ return {
1116
+ content: [
1117
+ {
1118
+ type: "text",
1119
+ text: `Reference link added successfully!\n\nTitle: ${title}${url ? `\nURL: ${url}` : ""}${note ? `\nNote: ${note}` : ""}\nAttached to ${parentType}: ${parentId}\nLink ID: ${result.link?.id}\n\nView it at: ${VIBESHARING_URL}/dashboard/${folder_id ? "folders" : "projects"}/${parentId}`,
1120
+ },
1121
+ ],
1122
+ };
1123
+ }
1124
+ case "list_context_links": {
1125
+ const { folder_id, project_id } = args;
1126
+ if (!folder_id && !project_id) {
1127
+ return {
1128
+ content: [
1129
+ {
1130
+ type: "text",
1131
+ text: "Error: Either folder_id or project_id is required.",
1132
+ },
1133
+ ],
1134
+ isError: true,
1135
+ };
1136
+ }
1137
+ const result = await client.listContextLinks({
1138
+ folderId: folder_id,
1139
+ projectId: project_id,
1140
+ });
1141
+ const links = result.links || [];
1142
+ if (links.length === 0) {
1143
+ return {
1144
+ content: [
1145
+ {
1146
+ type: "text",
1147
+ text: "No reference links found. Use add_context_link to attach Figma designs, PRDs, docs, or notes.",
1148
+ },
1149
+ ],
1150
+ };
1151
+ }
1152
+ const list = links
1153
+ .map((l) => `- ${l.title}\n ID: ${l.id}${l.url ? `\n URL: ${l.url}` : " (note)"}${l.note ? `\n Note: ${l.note}` : ""}\n Added: ${new Date(l.created_at).toLocaleDateString()}`)
1154
+ .join("\n\n");
1155
+ return {
1156
+ content: [
1157
+ {
1158
+ type: "text",
1159
+ text: `Found ${links.length} reference link(s):\n\n${list}`,
1160
+ },
1161
+ ],
1162
+ };
1163
+ }
1164
+ case "remove_context_link": {
1165
+ const { link_id } = args;
1166
+ await client.removeContextLink(link_id);
1167
+ return {
1168
+ content: [
1169
+ {
1170
+ type: "text",
1171
+ text: `Reference link ${link_id} removed successfully.`,
1172
+ },
1173
+ ],
1174
+ };
1175
+ }
1176
+ default:
1177
+ return {
1178
+ content: [
1179
+ {
1180
+ type: "text",
1181
+ text: `Unknown tool: ${name}`,
1182
+ },
1183
+ ],
1184
+ isError: true,
1185
+ };
791
1186
  }
792
- default:
793
- return {
794
- content: [
795
- {
796
- type: "text",
797
- text: `Unknown tool: ${name}`,
798
- },
799
- ],
800
- isError: true,
801
- };
802
1187
  }
1188
+ catch (error) {
1189
+ return {
1190
+ content: [
1191
+ {
1192
+ type: "text",
1193
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
1194
+ },
1195
+ ],
1196
+ isError: true,
1197
+ };
1198
+ }
1199
+ })();
1200
+ // Prepend What's New to the first successful tool call after upgrade
1201
+ if (pendingWhatsNew && !toolResult.isError) {
1202
+ toolResult.content.unshift({ type: "text", text: pendingWhatsNew + "\n\n---\n" });
1203
+ pendingWhatsNew = null;
803
1204
  }
804
- catch (error) {
805
- return {
806
- content: [
807
- {
808
- type: "text",
809
- text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
810
- },
811
- ],
812
- isError: true,
813
- };
814
- }
1205
+ return toolResult;
815
1206
  });
816
1207
  // List available resources (prototypes as resources)
817
1208
  server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibesharingapp/mcp-server",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for VibeSharing - register prototypes and get feedback directly from Claude Code",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",