uiplug-mcp 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +169 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -40,8 +40,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
40
  {
41
41
  name: "list_components",
42
42
  description: "List published UI components from the UIPlug marketplace. " +
43
- "Optionally filter by framework (e.g. React, Vue, Jetpack Compose, Flutter, SwiftUI) " +
44
- "and/or category (e.g. Layout, Navigation, Input, Data Display, Feedback, Sensors).",
43
+ "Optionally filter by framework, category, or group_slug. " +
44
+ "When group_slug is set, returns all components in that group (including private ones if you are a member). " +
45
+ "Use list_groups to find your group slugs.",
45
46
  inputSchema: {
46
47
  type: "object",
47
48
  properties: {
@@ -58,13 +59,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
58
59
  type: "number",
59
60
  description: "Maximum number of results to return (default 20, max 50).",
60
61
  },
62
+ group_slug: {
63
+ type: "string",
64
+ description: "Filter to a specific group (e.g. 'acme-ui-team'). " +
65
+ "Members of the group will also see private components. " +
66
+ "Use list_groups to find your group slugs.",
67
+ },
61
68
  },
62
69
  },
63
70
  },
64
71
  {
65
72
  name: "search_components",
66
73
  description: "Search UIPlug components by name, description, or tag. " +
67
- "Returns matching components with a summary of their metadata.",
74
+ "Returns matching components with a summary of their metadata. " +
75
+ "To search within a specific group (including private components), provide group_slug.",
68
76
  inputSchema: {
69
77
  type: "object",
70
78
  required: ["query"],
@@ -77,6 +85,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
77
85
  type: "string",
78
86
  description: "Narrow results to a specific framework.",
79
87
  },
88
+ group_slug: {
89
+ type: "string",
90
+ description: "Narrow results to a specific group. " +
91
+ "Members of the group will also see private components.",
92
+ },
80
93
  },
81
94
  },
82
95
  },
@@ -96,11 +109,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
96
109
  },
97
110
  },
98
111
  },
112
+ {
113
+ name: "list_groups",
114
+ description: "List the UIPlug groups you are an active member of. " +
115
+ "Returns each group's name and slug. Use the slug in create_component to submit a component on behalf of a group.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {},
119
+ },
120
+ },
99
121
  {
100
122
  name: "create_component",
101
123
  description: "Submit a new UI component to the UIPlug marketplace. " +
102
124
  "The component will be submitted for review (status: pending) and visible in your dashboard at uiplug.com/dashboard/components. " +
103
125
  "Use this after building a component to share it with the community. " +
126
+ "To submit on behalf of a group, provide group_slug (use list_groups to find it). " +
127
+ "Group components can be public (visible to everyone) or private (visible only to group members). " +
104
128
  "IMPORTANT — code quality standard: the code field must be a complete, self-contained file. " +
105
129
  "It must start with all necessary imports (e.g. import { useState } from 'react'), " +
106
130
  "contain the full component implementation (not just a snippet or function body), " +
@@ -151,6 +175,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
151
175
  type: "string",
152
176
  description: "Optional — AI model used to build this (e.g. 'Claude Sonnet 4.6').",
153
177
  },
178
+ group_slug: {
179
+ type: "string",
180
+ description: "Optional — submit this component on behalf of a group. " +
181
+ "Use the slug from list_groups (e.g. 'acme-ui-team'). " +
182
+ "The component will go to the group admin for review. " +
183
+ "You must be an active member of the group.",
184
+ },
185
+ visibility: {
186
+ type: "string",
187
+ enum: ["public", "private"],
188
+ description: "Only relevant when group_slug is set. " +
189
+ "'public' = visible to everyone in Explore (default). " +
190
+ "'private' = visible only to group members. " +
191
+ "Personal components (no group_slug) are always public.",
192
+ },
154
193
  },
155
194
  },
156
195
  },
@@ -166,44 +205,64 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
166
205
  }
167
206
  // ── list_components ─────────────────────────────────────────────────────────
168
207
  if (name === "list_components") {
169
- const { framework, category, limit = 20 } = (args ?? {});
170
- let query = supabase
171
- .from("components")
172
- .select("id, name, description, category, framework, downloads, likes, model, " +
173
- "profiles!components_author_id_fkey(username)")
174
- .eq("status", "published")
175
- .order("downloads", { ascending: false })
176
- .limit(Math.min(limit, 50));
177
- if (framework)
178
- query = query.eq("framework", framework);
179
- if (category)
180
- query = query.eq("category", category);
181
- const { data, error } = await query;
182
- if (error) {
183
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
208
+ const { framework, category, limit = 20, group_slug } = (args ?? {});
209
+ let rows = [];
210
+ if (group_slug) {
211
+ // Use SECURITY DEFINER RPC so private group components are visible to members
212
+ const { createHash: ch } = await import("crypto");
213
+ const kh = ch("sha256").update(process.env.UIPLUG_API_KEY ?? "").digest("hex");
214
+ const { data, error } = await supabase.rpc("get_group_components", {
215
+ p_key_hash: kh,
216
+ p_group_slug: group_slug,
217
+ p_search: null,
218
+ p_limit: Math.min(limit, 50),
219
+ });
220
+ if (error) {
221
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
222
+ }
223
+ rows = (data ?? []).filter((c) => (!framework || c.framework === framework) &&
224
+ (!category || c.category === category));
225
+ }
226
+ else {
227
+ let query = supabase
228
+ .from("components")
229
+ .select("id, name, description, category, framework, downloads, likes, model, status, visibility, " +
230
+ "profiles!components_author_id_fkey(username)")
231
+ .eq("status", "published")
232
+ .eq("visibility", "public")
233
+ .order("downloads", { ascending: false })
234
+ .limit(Math.min(limit, 50));
235
+ if (framework)
236
+ query = query.eq("framework", framework);
237
+ if (category)
238
+ query = query.eq("category", category);
239
+ const { data, error } = await query;
240
+ if (error) {
241
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
242
+ }
243
+ rows = (data ?? []).map((c) => ({ ...c, author: c.profiles?.username ?? "Unknown" }));
244
+ }
245
+ if (rows.length === 0) {
246
+ return {
247
+ content: [{ type: "text", text: group_slug ? `No components found in group "${group_slug}".` : "No components found." }],
248
+ };
184
249
  }
185
- const rows = (data ?? []).map((c) => ({
186
- id: c.id,
187
- name: c.name,
188
- description: c.description,
189
- category: c.category,
190
- framework: c.framework,
191
- author: c.profiles?.username ?? "Unknown",
192
- downloads: c.downloads ?? 0,
193
- likes: c.likes ?? 0,
194
- model: c.model ?? null,
195
- }));
196
250
  return {
197
251
  content: [
198
252
  {
199
253
  type: "text",
200
- text: `Found ${rows.length} component(s).\n\n` +
254
+ text: `Found ${rows.length} component(s)${group_slug ? ` in group "${group_slug}"` : ""}.\n\n` +
201
255
  rows
202
- .map((c) => `**${c.name}** (${c.framework} · ${c.category})\n` +
203
- ` ID: ${c.id}\n` +
204
- ` ${c.description}\n` +
205
- ` Author: ${c.author} · ↓${c.downloads} ♥${c.likes}` +
206
- (c.model ? ` · Built with ${c.model}` : ""))
256
+ .map((c) => {
257
+ const author = c.author ?? c.author_username ?? "Unknown";
258
+ const visTag = c.visibility === "private" ? " 🔒" : "";
259
+ const statusTag = c.status !== "published" ? ` [${c.status}]` : "";
260
+ return (`**${c.name}**${visTag}${statusTag} (${c.framework} · ${c.category})\n` +
261
+ ` ID: ${c.id}\n` +
262
+ ` ${c.description}\n` +
263
+ ` Author: ${author} · ↓${c.downloads ?? 0} ♥${c.likes ?? 0}` +
264
+ (c.model ? ` · Built with ${c.model}` : ""));
265
+ })
207
266
  .join("\n\n"),
208
267
  },
209
268
  ],
@@ -211,13 +270,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
211
270
  }
212
271
  // ── search_components ───────────────────────────────────────────────────────
213
272
  if (name === "search_components") {
214
- const { query: q, framework } = (args ?? {});
273
+ const { query: q, framework, group_slug } = (args ?? {});
274
+ // Group-scoped search (includes private components for members)
275
+ if (group_slug) {
276
+ const { createHash: ch } = await import("crypto");
277
+ const kh = ch("sha256").update(process.env.UIPLUG_API_KEY ?? "").digest("hex");
278
+ const { data, error } = await supabase.rpc("get_group_components", {
279
+ p_key_hash: kh,
280
+ p_group_slug: group_slug,
281
+ p_search: q,
282
+ p_limit: 30,
283
+ });
284
+ if (error) {
285
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
286
+ }
287
+ const results = (data ?? []).filter((c) => !framework || c.framework === framework);
288
+ if (results.length === 0) {
289
+ return { content: [{ type: "text", text: `No components found in group "${group_slug}" matching "${q}".` }] };
290
+ }
291
+ return {
292
+ content: [{
293
+ type: "text",
294
+ text: `Found ${results.length} result(s) in group "${group_slug}" for "${q}":\n\n` +
295
+ results.map((c) => {
296
+ const visTag = c.visibility === "private" ? " 🔒" : "";
297
+ const statusTag = c.status !== "published" ? ` [${c.status}]` : "";
298
+ return (`**${c.name}**${visTag}${statusTag} (${c.framework} · ${c.category})\n` +
299
+ ` ID: ${c.id}\n ${c.description}\n Author: ${c.author_username ?? "Unknown"}`);
300
+ }).join("\n\n"),
301
+ }],
302
+ };
303
+ }
215
304
  // Search name + description via ilike, then also fetch tag matches
216
305
  let nameQuery = supabase
217
306
  .from("components")
218
307
  .select("id, name, description, category, framework, downloads, likes, " +
219
308
  "profiles!components_author_id_fkey(username)")
220
309
  .eq("status", "published")
310
+ .eq("visibility", "public")
221
311
  .or(`name.ilike.%${q}%,description.ilike.%${q}%`)
222
312
  .order("downloads", { ascending: false })
223
313
  .limit(20);
@@ -251,6 +341,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
251
341
  .select("id, name, description, category, framework, downloads, likes, " +
252
342
  "profiles!components_author_id_fkey(username)")
253
343
  .eq("status", "published")
344
+ .eq("visibility", "public")
254
345
  .in("id", newTagIds)
255
346
  .limit(10);
256
347
  if (framework)
@@ -340,9 +431,42 @@ ${c.code_component}
340
431
  `;
341
432
  return { content: [{ type: "text", text: output }] };
342
433
  }
434
+ // ── list_groups ─────────────────────────────────────────────────────────────
435
+ if (name === "list_groups") {
436
+ const { createHash: ch } = await import("crypto");
437
+ const kh = ch("sha256").update(process.env.UIPLUG_API_KEY ?? "").digest("hex");
438
+ const { data, error } = await supabase.rpc("list_user_groups", { p_key_hash: kh });
439
+ if (error) {
440
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
441
+ }
442
+ const groups = (data ?? []);
443
+ if (groups.length === 0) {
444
+ return {
445
+ content: [
446
+ {
447
+ type: "text",
448
+ text: "You are not a member of any groups yet.\n\n" +
449
+ "Create or join a group at: https://uiplug.com/dashboard/groups",
450
+ },
451
+ ],
452
+ };
453
+ }
454
+ return {
455
+ content: [
456
+ {
457
+ type: "text",
458
+ text: `You are a member of ${groups.length} group(s):\n\n` +
459
+ groups
460
+ .map((g) => `**${g.group_name}** (slug: \`${g.group_slug}\`) — ${g.role}`)
461
+ .join("\n") +
462
+ `\n\nUse the slug in create_component to submit components on behalf of a group.`,
463
+ },
464
+ ],
465
+ };
466
+ }
343
467
  // ── create_component ────────────────────────────────────────────────────────
344
468
  if (name === "create_component") {
345
- const { name: compName, description, framework, category, code, installation, tags, model, } = (args ?? {});
469
+ const { name: compName, description, framework, category, code, installation, tags, model, group_slug, visibility = "public", } = (args ?? {});
346
470
  const { createHash: createHash2 } = await import("crypto");
347
471
  const keyHashForCreate = createHash2("sha256").update(process.env.UIPLUG_API_KEY ?? "").digest("hex");
348
472
  const { data: componentId, error } = await supabase.rpc("create_component", {
@@ -355,10 +479,16 @@ ${c.code_component}
355
479
  p_installation: installation ?? null,
356
480
  p_tags: tags ?? [],
357
481
  p_model: model ?? null,
482
+ p_group_slug: group_slug ?? null,
483
+ p_visibility: group_slug ? (visibility ?? "public") : "public",
358
484
  });
359
485
  if (error) {
360
486
  return { content: [{ type: "text", text: `Error submitting component: ${error.message}` }], isError: true };
361
487
  }
488
+ const groupNote = group_slug
489
+ ? `\n**Group:** ${group_slug} (${visibility === "private" ? "🔒 Private — visible to group members only" : "🌐 Public — visible to everyone"})\n` +
490
+ `The group admin will review it at: https://uiplug.com/dashboard/groups`
491
+ : "";
362
492
  return {
363
493
  content: [
364
494
  {
@@ -367,7 +497,8 @@ ${c.code_component}
367
497
  `**Name:** ${compName}\n` +
368
498
  `**Framework:** ${framework}\n` +
369
499
  `**Category:** ${category}\n` +
370
- `**ID:** ${componentId}\n\n` +
500
+ `**ID:** ${componentId}\n` +
501
+ groupNote + `\n` +
371
502
  `View and manage it at: https://uiplug.com/dashboard/components`,
372
503
  },
373
504
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uiplug-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for UIPlug — gives AI agents access to the UIPlug UI component marketplace",
5
5
  "type": "module",
6
6
  "bin": {