@vibesharingapp/mcp-server 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +355 -28
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -44,10 +44,10 @@ class VibesharingClient {
44
44
  body: JSON.stringify(params),
45
45
  });
46
46
  }
47
- async deployFiles(prototypeId, files, commitMessage) {
47
+ async deployFiles(prototypeId, files, commitMessage, deployName) {
48
48
  return this.request(`/api/prototypes/${prototypeId}/deploy-code`, {
49
49
  method: "POST",
50
- body: JSON.stringify({ files, commitMessage }),
50
+ body: JSON.stringify({ files, commitMessage, deployName }),
51
51
  });
52
52
  }
53
53
  async listPrototypes() {
@@ -101,10 +101,10 @@ class VibesharingClient {
101
101
  }),
102
102
  });
103
103
  }
104
- async importRepo(prototypeId, repoUrl) {
104
+ async importRepo(prototypeId, repoUrl, deployName) {
105
105
  return this.request("/api/git/import-repo", {
106
106
  method: "POST",
107
- body: JSON.stringify({ prototypeId, repoUrl }),
107
+ body: JSON.stringify({ prototypeId, repoUrl, deployName }),
108
108
  });
109
109
  }
110
110
  async addContextLink(params) {
@@ -131,8 +131,51 @@ class VibesharingClient {
131
131
  });
132
132
  }
133
133
  }
134
+ /**
135
+ * Simple fuzzy text matching. Scores based on:
136
+ * - Exact match → 1.0
137
+ * - Case-insensitive exact → 0.95
138
+ * - Query is a substring → 0.7–0.9 (bonus for matching at word boundaries)
139
+ * - Token overlap (words in common) → 0.3–0.7
140
+ * - Otherwise → 0
141
+ */
142
+ function fuzzyScore(query, target) {
143
+ const q = query.toLowerCase().trim();
144
+ const t = target.toLowerCase().trim();
145
+ if (q === t)
146
+ return 1.0;
147
+ if (t === q)
148
+ return 0.95;
149
+ // Substring match
150
+ if (t.includes(q)) {
151
+ // Bonus if it matches at a word boundary
152
+ const wordBoundary = t.startsWith(q) || t.includes(` ${q}`);
153
+ return wordBoundary ? 0.9 : 0.75;
154
+ }
155
+ if (q.includes(t))
156
+ return 0.7;
157
+ // Token overlap
158
+ const qTokens = q.split(/[\s\-_]+/).filter(Boolean);
159
+ const tTokens = t.split(/[\s\-_]+/).filter(Boolean);
160
+ if (qTokens.length === 0 || tTokens.length === 0)
161
+ return 0;
162
+ let matches = 0;
163
+ for (const qt of qTokens) {
164
+ if (tTokens.some((tt) => tt.includes(qt) || qt.includes(tt))) {
165
+ matches++;
166
+ }
167
+ }
168
+ const overlap = matches / Math.max(qTokens.length, tTokens.length);
169
+ return overlap * 0.7;
170
+ }
171
+ function fuzzyMatch(query, items, getName, threshold = 0.3) {
172
+ return items
173
+ .map((item) => ({ item, score: fuzzyScore(query, getName(item)) }))
174
+ .filter((m) => m.score >= threshold)
175
+ .sort((a, b) => b.score - a.score);
176
+ }
134
177
  // Get configuration from environment
135
- const VIBESHARING_URL = process.env.VIBESHARING_URL || "https://www.vibesharing.app";
178
+ const VIBESHARING_URL = process.env.VIBESHARING_URL || "https://vibesharing.app";
136
179
  const VIBESHARING_TOKEN = process.env.VIBESHARING_TOKEN;
137
180
  if (!VIBESHARING_TOKEN) {
138
181
  console.error("Error: VIBESHARING_TOKEN environment variable is required");
@@ -143,7 +186,7 @@ const client = new VibesharingClient(VIBESHARING_URL, VIBESHARING_TOKEN);
143
186
  // Create MCP server
144
187
  const server = new index_js_1.Server({
145
188
  name: "vibesharing",
146
- version: "0.2.0",
189
+ version: "0.4.0",
147
190
  }, {
148
191
  capabilities: {
149
192
  tools: {},
@@ -156,7 +199,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
156
199
  tools: [
157
200
  {
158
201
  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.",
202
+ 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
203
  inputSchema: {
161
204
  type: "object",
162
205
  properties: {
@@ -194,18 +237,28 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
194
237
  },
195
238
  {
196
239
  name: "list_prototypes",
197
- description: "List all prototypes in your VibeSharing organization. Shows name, URL, and recent activity.",
240
+ description: "List all prototypes in your VibeSharing organization. Shows name, URL, and recent activity. Optionally filter by search query (fuzzy matched).",
198
241
  inputSchema: {
199
242
  type: "object",
200
- properties: {},
243
+ properties: {
244
+ search: {
245
+ type: "string",
246
+ description: "Optional: fuzzy search query to filter prototypes by name (e.g., 'erg' or 'dashboard')",
247
+ },
248
+ },
201
249
  },
202
250
  },
203
251
  {
204
252
  name: "list_collections",
205
- description: "List all collections (folders) in your VibeSharing organization. Use this to find the collection_id when registering prototypes.",
253
+ 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
254
  inputSchema: {
207
255
  type: "object",
208
- properties: {},
256
+ properties: {
257
+ search: {
258
+ type: "string",
259
+ description: "Optional: fuzzy search query to filter collections by name (e.g., 'hero' or 'compliance')",
260
+ },
261
+ },
209
262
  },
210
263
  },
211
264
  {
@@ -276,7 +329,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
276
329
  },
277
330
  {
278
331
  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.",
332
+ 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
333
  inputSchema: {
281
334
  type: "object",
282
335
  properties: {
@@ -316,7 +369,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
316
369
  },
317
370
  {
318
371
  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.",
372
+ 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
373
  inputSchema: {
321
374
  type: "object",
322
375
  properties: {
@@ -346,13 +399,17 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
346
399
  type: "string",
347
400
  description: "Optional: Git commit message (default: 'Deploy via MCP')",
348
401
  },
402
+ deploy_name: {
403
+ type: "string",
404
+ 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.",
405
+ },
349
406
  },
350
407
  required: ["prototype_id", "files"],
351
408
  },
352
409
  },
353
410
  {
354
411
  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.",
412
+ 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
413
  inputSchema: {
357
414
  type: "object",
358
415
  properties: {
@@ -380,10 +437,35 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
380
437
  type: "string",
381
438
  description: "Optional: Description of the prototype.",
382
439
  },
440
+ deploy_name: {
441
+ type: "string",
442
+ 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.",
443
+ },
383
444
  },
384
445
  required: ["repo_url"],
385
446
  },
386
447
  },
448
+ {
449
+ name: "resolve_target",
450
+ 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.",
451
+ inputSchema: {
452
+ type: "object",
453
+ properties: {
454
+ collection_name: {
455
+ type: "string",
456
+ description: "Approximate collection name to search for (e.g., 'hero use cases'). Fuzzy matched.",
457
+ },
458
+ project_name: {
459
+ type: "string",
460
+ description: "Approximate project/prototype name to search for (e.g., 'ERG v3'). Fuzzy matched.",
461
+ },
462
+ deploy_name: {
463
+ type: "string",
464
+ description: "Desired Vercel deploy name to check availability for (e.g., 'erg-v3-teams').",
465
+ },
466
+ },
467
+ },
468
+ },
387
469
  {
388
470
  name: "add_context_link",
389
471
  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.",
@@ -455,6 +537,40 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
455
537
  switch (name) {
456
538
  case "register_prototype": {
457
539
  const params = args;
540
+ // Guardrail: if no collection specified, bounce back with options
541
+ if (!params.collection_id) {
542
+ const [guardCollResult, guardProtoResult] = await Promise.all([
543
+ client.listCollections(),
544
+ client.listPrototypes(),
545
+ ]);
546
+ const guardCollections = guardCollResult.collections || [];
547
+ const guardPrototypes = guardProtoResult.prototypes || [];
548
+ const similarProjects = fuzzyMatch(params.name, guardPrototypes, (p) => p.name).slice(0, 5);
549
+ const sections = [
550
+ `HOLD ON — confirm with the user before registering "${params.name}":\n`,
551
+ ];
552
+ if (similarProjects.length > 0) {
553
+ sections.push(`Similar existing projects:\n` +
554
+ similarProjects.map((m) => {
555
+ const p = m.item;
556
+ return ` - "${p.name}" (ID: ${p.id})`;
557
+ }).join("\n") +
558
+ `\n\nAsk the user: Is this an update to one of these, or a new project?`);
559
+ }
560
+ if (guardCollections.length > 0) {
561
+ sections.push(`\nNo collection specified. Available collections:\n` +
562
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
563
+ `\n\nAsk the user: Which collection should "${params.name}" go in?`);
564
+ }
565
+ return {
566
+ content: [
567
+ {
568
+ type: "text",
569
+ text: sections.join("\n"),
570
+ },
571
+ ],
572
+ };
573
+ }
458
574
  const { source_code, source_filename, ...registerParams } = params;
459
575
  const result = await client.registerPrototype(registerParams);
460
576
  const protoId = result.prototype?.id;
@@ -479,14 +595,21 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
479
595
  };
480
596
  }
481
597
  case "list_prototypes": {
598
+ const { search: protoSearch } = (args || {});
482
599
  const result = await client.listPrototypes();
483
- const prototypes = result.prototypes || [];
600
+ let prototypes = result.prototypes || [];
601
+ if (protoSearch) {
602
+ const matches = fuzzyMatch(protoSearch, prototypes, (p) => p.name);
603
+ prototypes = matches.map((m) => m.item);
604
+ }
484
605
  if (prototypes.length === 0) {
485
606
  return {
486
607
  content: [
487
608
  {
488
609
  type: "text",
489
- text: "No prototypes found. Use register_prototype to add your first one!",
610
+ text: protoSearch
611
+ ? `No prototypes matching "${protoSearch}". Use list_prototypes without search to see all.`
612
+ : "No prototypes found. Use register_prototype to add your first one!",
490
613
  },
491
614
  ],
492
615
  };
@@ -498,20 +621,27 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
498
621
  content: [
499
622
  {
500
623
  type: "text",
501
- text: `Found ${prototypes.length} prototype(s):\n\n${list}`,
624
+ text: `Found ${prototypes.length} prototype(s)${protoSearch ? ` matching "${protoSearch}"` : ""}:\n\n${list}`,
502
625
  },
503
626
  ],
504
627
  };
505
628
  }
506
629
  case "list_collections": {
630
+ const { search: collSearch } = (args || {});
507
631
  const result = await client.listCollections();
508
- const collections = result.collections || [];
632
+ let collections = result.collections || [];
633
+ if (collSearch) {
634
+ const matches = fuzzyMatch(collSearch, collections, (c) => c.name);
635
+ collections = matches.map((m) => m.item);
636
+ }
509
637
  if (collections.length === 0) {
510
638
  return {
511
639
  content: [
512
640
  {
513
641
  type: "text",
514
- text: "No collections found. Create one in the VibeSharing dashboard first.",
642
+ text: collSearch
643
+ ? `No collections matching "${collSearch}". Use list_collections without search to see all.`
644
+ : "No collections found. Create one in the VibeSharing dashboard first.",
515
645
  },
516
646
  ],
517
647
  };
@@ -523,7 +653,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
523
653
  content: [
524
654
  {
525
655
  type: "text",
526
- text: `Found ${collections.length} collection(s):\n\n${list}`,
656
+ text: `Found ${collections.length} collection(s)${collSearch ? ` matching "${collSearch}"` : ""}:\n\n${list}`,
527
657
  },
528
658
  ],
529
659
  };
@@ -610,6 +740,46 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
610
740
  }
611
741
  case "deploy_prototype": {
612
742
  const { code, name, prototype_id } = args;
743
+ // Guardrail: if no existing prototype specified, bounce back with options
744
+ if (!prototype_id) {
745
+ const [guardCollResult, guardProtoResult] = await Promise.all([
746
+ client.listCollections(),
747
+ client.listPrototypes(),
748
+ ]);
749
+ const guardCollections = guardCollResult.collections || [];
750
+ const guardPrototypes = guardProtoResult.prototypes || [];
751
+ const similarProjects = fuzzyMatch(name, guardPrototypes, (p) => p.name).slice(0, 5);
752
+ const sections = [
753
+ `HOLD ON — confirm with the user before deploying "${name}":\n`,
754
+ ];
755
+ if (similarProjects.length > 0) {
756
+ sections.push(`Similar existing projects:\n` +
757
+ similarProjects.map((m) => {
758
+ const p = m.item;
759
+ return ` - "${p.name}" (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`;
760
+ }).join("\n") +
761
+ `\n\nAsk the user: Is this an UPDATE to one of these, or a NEW project?`);
762
+ }
763
+ if (guardCollections.length > 0) {
764
+ sections.push(`\nAvailable collections:\n` +
765
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
766
+ `\n\nAsk the user: Which collection should this go in?`);
767
+ }
768
+ const suggestedSlug = name
769
+ .toLowerCase()
770
+ .replace(/[^a-z0-9]+/g, "-")
771
+ .replace(/^-+|-+$/g, "");
772
+ sections.push(`\nSuggested deploy name: "${suggestedSlug}" → https://${suggestedSlug}.vercel.app\n` +
773
+ `Ask the user: Do you want to use this name or choose a different one?`);
774
+ return {
775
+ content: [
776
+ {
777
+ type: "text",
778
+ text: sections.join("\n"),
779
+ },
780
+ ],
781
+ };
782
+ }
613
783
  // If no prototype_id, first register a new prototype
614
784
  let prototypeId = prototype_id;
615
785
  if (!prototypeId) {
@@ -658,28 +828,77 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
658
828
  };
659
829
  }
660
830
  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");
831
+ const { prototype_id: deployProtoId, files, commit_message, deploy_name: deployName } = args;
832
+ const deployResult = await client.deployFiles(deployProtoId, files, commit_message || "Deploy via MCP", deployName);
663
833
  return {
664
834
  content: [
665
835
  {
666
836
  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.`,
837
+ 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.`,
668
838
  },
669
839
  ],
670
840
  };
671
841
  }
672
842
  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
843
+ const { repo_url, name: importName, prototype_id: importProtoId, collection_id: importCollectionId, parent_project_id: importParentId, description: importDesc, deploy_name: deployName, } = args;
844
+ // Guardrail: if no collection and no existing prototype specified, bounce back with options
845
+ if (!importCollectionId && !importProtoId) {
846
+ const [guardCollResult, guardProtoResult] = await Promise.all([
847
+ client.listCollections(),
848
+ client.listPrototypes(),
849
+ ]);
850
+ const guardCollections = guardCollResult.collections || [];
851
+ const guardPrototypes = guardProtoResult.prototypes || [];
852
+ const repoName = repo_url.replace(/\.git$/, "").split("/").pop() || "imported-prototype";
853
+ const suggestedName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
854
+ // Check for similar existing prototypes
855
+ const similarProjects = importName
856
+ ? fuzzyMatch(importName, guardPrototypes, (p) => p.name).slice(0, 5)
857
+ : [];
858
+ const sections = [
859
+ `HOLD ON — confirm these details with the user before deploying:\n`,
860
+ `Prototype name: "${suggestedName}"`,
861
+ ];
862
+ if (similarProjects.length > 0) {
863
+ sections.push(`\nSimilar existing projects found:\n` +
864
+ similarProjects.map((m) => {
865
+ const p = m.item;
866
+ return ` - "${p.name}" (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`;
867
+ }).join("\n") +
868
+ `\n\nAsk the user: Is this an UPDATE to one of these existing projects, or a NEW project?`);
869
+ }
870
+ if (guardCollections.length > 0) {
871
+ sections.push(`\nNo collection specified. Available collections:\n` +
872
+ guardCollections.map((c) => ` - ${c.name} (ID: ${c.id})`).join("\n") +
873
+ `\n\nAsk the user: Which collection should "${suggestedName}" go in?`);
874
+ }
875
+ if (!deployName) {
876
+ const suggestedSlug = suggestedName
877
+ .toLowerCase()
878
+ .replace(/[^a-z0-9]+/g, "-")
879
+ .replace(/^-+|-+$/g, "");
880
+ sections.push(`\nNo deploy name specified. Suggested: "${suggestedSlug}" → https://${suggestedSlug}.vercel.app\n` +
881
+ `Ask the user: Do you want to use "${suggestedSlug}" or choose a different deploy name?`);
882
+ }
883
+ return {
884
+ content: [
885
+ {
886
+ type: "text",
887
+ text: sections.join("\n"),
888
+ },
889
+ ],
890
+ };
891
+ }
892
+ // Auto-create prototype if no ID provided (collection was confirmed above)
675
893
  let protoId = importProtoId;
676
894
  if (!protoId) {
677
895
  // Derive name from repo URL if not provided
678
896
  const repoName = repo_url.replace(/\.git$/, "").split("/").pop() || "imported-prototype";
679
- const protoName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, c => c.toUpperCase());
897
+ const protoName = importName || repoName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
680
898
  const registered = await client.registerPrototype({
681
899
  name: protoName,
682
900
  description: importDesc || `Imported from ${repo_url}`,
901
+ external_url: repo_url,
683
902
  collection_id: importCollectionId,
684
903
  parent_project_id: importParentId,
685
904
  });
@@ -696,12 +915,120 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
696
915
  isError: true,
697
916
  };
698
917
  }
699
- const importResult = await client.importRepo(protoId, repo_url);
918
+ const importResult = await client.importRepo(protoId, repo_url, deployName);
919
+ return {
920
+ content: [
921
+ {
922
+ type: "text",
923
+ 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.`,
924
+ },
925
+ ],
926
+ };
927
+ }
928
+ case "resolve_target": {
929
+ const { collection_name: collName, project_name: projName, deploy_name: desiredDeployName, } = (args || {});
930
+ // Fetch collections and prototypes in parallel
931
+ const [collResult, protoResult] = await Promise.all([
932
+ client.listCollections(),
933
+ client.listPrototypes(),
934
+ ]);
935
+ const allCollections = collResult.collections || [];
936
+ const allPrototypes = protoResult.prototypes || [];
937
+ const sections = [];
938
+ // --- Collection matching ---
939
+ if (collName) {
940
+ const collMatches = fuzzyMatch(collName, allCollections, (c) => c.name);
941
+ if (collMatches.length === 0) {
942
+ sections.push(`COLLECTION: No match for "${collName}".\n` +
943
+ `Available collections:\n` +
944
+ allCollections
945
+ .map((c) => ` - ${c.name} (ID: ${c.id})`)
946
+ .join("\n") +
947
+ `\n\nAsk the user: Which collection should this go in? Or create a new one?`);
948
+ }
949
+ else if (collMatches[0].score >= 0.9) {
950
+ const best = collMatches[0].item;
951
+ sections.push(`COLLECTION: Matched "${collName}" → "${best.name}" (ID: ${best.id})`);
952
+ // Show prototypes in this collection for context
953
+ const collProtos = allPrototypes.filter((p) => p.folder_id === best.id);
954
+ if (collProtos.length > 0) {
955
+ sections.push(`Existing projects in "${best.name}":\n` +
956
+ collProtos
957
+ .map((p) => ` - ${p.name} (ID: ${p.id})${p.external_url ? ` → ${p.external_url}` : ""}`)
958
+ .join("\n") +
959
+ `\n\nAsk the user: Should this be a NEW project in "${best.name}", or an update to one of these existing projects?`);
960
+ }
961
+ }
962
+ else {
963
+ // Fuzzy matches but not confident
964
+ sections.push(`COLLECTION: "${collName}" is not an exact match. Did the user mean one of these?\n` +
965
+ collMatches
966
+ .slice(0, 5)
967
+ .map((m) => {
968
+ const c = m.item;
969
+ return ` - "${c.name}" (ID: ${c.id}, confidence: ${Math.round(m.score * 100)}%)`;
970
+ })
971
+ .join("\n") +
972
+ `\n\nAsk the user to confirm which collection.`);
973
+ }
974
+ }
975
+ else {
976
+ // No collection name given — show all options
977
+ if (allCollections.length > 0) {
978
+ sections.push(`COLLECTION: Not specified. Available collections:\n` +
979
+ allCollections
980
+ .map((c) => ` - ${c.name} (ID: ${c.id})`)
981
+ .join("\n") +
982
+ `\n\nAsk the user: Which collection should this prototype go in? Or create a new one?`);
983
+ }
984
+ else {
985
+ sections.push(`COLLECTION: No collections exist yet. Ask the user if they want to create one.`);
986
+ }
987
+ }
988
+ // --- Project matching ---
989
+ if (projName) {
990
+ const projMatches = fuzzyMatch(projName, allPrototypes, (p) => p.name);
991
+ if (projMatches.length === 0) {
992
+ sections.push(`PROJECT: No match for "${projName}". This will be a new project.\n` +
993
+ `Ask the user to confirm the name for the new project.`);
994
+ }
995
+ else if (projMatches[0].score >= 0.9) {
996
+ const best = projMatches[0].item;
997
+ sections.push(`PROJECT: Matched "${projName}" → "${best.name}" (ID: ${best.id})${best.external_url ? `\n Current URL: ${best.external_url}` : ""}\n\n` +
998
+ `Ask the user: Deploy as an UPDATE to "${best.name}", or create a NEW project?`);
999
+ }
1000
+ else {
1001
+ sections.push(`PROJECT: "${projName}" is not an exact match. Close matches:\n` +
1002
+ projMatches
1003
+ .slice(0, 5)
1004
+ .map((m) => {
1005
+ const p = m.item;
1006
+ return ` - "${p.name}" (ID: ${p.id}, confidence: ${Math.round(m.score * 100)}%)`;
1007
+ })
1008
+ .join("\n") +
1009
+ `\n\nAsk the user: Is this an update to one of these, or a new project?`);
1010
+ }
1011
+ }
1012
+ // --- Deploy name ---
1013
+ if (desiredDeployName) {
1014
+ const slug = desiredDeployName
1015
+ .toLowerCase()
1016
+ .replace(/[^a-z0-9]/g, "-")
1017
+ .replace(/-{2,}/g, "-")
1018
+ .replace(/^-+|-+$/g, "");
1019
+ sections.push(`DEPLOY NAME: "${slug}" → will deploy to https://${slug}.vercel.app\n` +
1020
+ `(Availability will be checked at deploy time. If taken, a suffix will be added.)\n\n` +
1021
+ `Ask the user: Do you want to name this deployment "${slug}"?`);
1022
+ }
1023
+ else {
1024
+ sections.push(`DEPLOY NAME: Not specified.\n` +
1025
+ `Ask the user: Do you want a custom deploy name (e.g., "erg-v3-teams" → erg-v3-teams.vercel.app), or auto-generate one?`);
1026
+ }
700
1027
  return {
701
1028
  content: [
702
1029
  {
703
1030
  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.`,
1031
+ text: sections.join("\n\n---\n\n"),
705
1032
  },
706
1033
  ],
707
1034
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibesharingapp/mcp-server",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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",