uiplug-mcp 1.2.1 → 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 +183 -40
  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,28 @@ 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
- "Use this after building a component to share it with the community.",
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). " +
128
+ "IMPORTANT — code quality standard: the code field must be a complete, self-contained file. " +
129
+ "It must start with all necessary imports (e.g. import { useState } from 'react'), " +
130
+ "contain the full component implementation (not just a snippet or function body), " +
131
+ "support common props like size, variant, disabled, loading, onClick, children, and icon where relevant, " +
132
+ "and end with a default export (e.g. export default MyComponent). " +
133
+ "Do NOT submit partial snippets, TypeScript-only interfaces without implementation, or placeholder code.",
104
134
  inputSchema: {
105
135
  type: "object",
106
136
  required: ["name", "description", "framework", "category", "code"],
@@ -124,7 +154,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
124
154
  },
125
155
  code: {
126
156
  type: "string",
127
- description: "Full component source code.",
157
+ description: "Complete, self-contained component source code. " +
158
+ "Must include: (1) all imports at the top, (2) full component with props and logic, " +
159
+ "(3) export default at the bottom. " +
160
+ "Example structure for React: " +
161
+ "import { useState } from 'react'; " +
162
+ "const MyComponent = ({ size = 'md', disabled = false, onClick, children }) => { ... }; " +
163
+ "export default MyComponent;",
128
164
  },
129
165
  installation: {
130
166
  type: "string",
@@ -139,6 +175,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
139
175
  type: "string",
140
176
  description: "Optional — AI model used to build this (e.g. 'Claude Sonnet 4.6').",
141
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
+ },
142
193
  },
143
194
  },
144
195
  },
@@ -154,44 +205,64 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
154
205
  }
155
206
  // ── list_components ─────────────────────────────────────────────────────────
156
207
  if (name === "list_components") {
157
- const { framework, category, limit = 20 } = (args ?? {});
158
- let query = supabase
159
- .from("components")
160
- .select("id, name, description, category, framework, downloads, likes, model, " +
161
- "profiles!components_author_id_fkey(username)")
162
- .eq("status", "published")
163
- .order("downloads", { ascending: false })
164
- .limit(Math.min(limit, 50));
165
- if (framework)
166
- query = query.eq("framework", framework);
167
- if (category)
168
- query = query.eq("category", category);
169
- const { data, error } = await query;
170
- if (error) {
171
- 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
+ };
172
249
  }
173
- const rows = (data ?? []).map((c) => ({
174
- id: c.id,
175
- name: c.name,
176
- description: c.description,
177
- category: c.category,
178
- framework: c.framework,
179
- author: c.profiles?.username ?? "Unknown",
180
- downloads: c.downloads ?? 0,
181
- likes: c.likes ?? 0,
182
- model: c.model ?? null,
183
- }));
184
250
  return {
185
251
  content: [
186
252
  {
187
253
  type: "text",
188
- text: `Found ${rows.length} component(s).\n\n` +
254
+ text: `Found ${rows.length} component(s)${group_slug ? ` in group "${group_slug}"` : ""}.\n\n` +
189
255
  rows
190
- .map((c) => `**${c.name}** (${c.framework} · ${c.category})\n` +
191
- ` ID: ${c.id}\n` +
192
- ` ${c.description}\n` +
193
- ` Author: ${c.author} · ↓${c.downloads} ♥${c.likes}` +
194
- (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
+ })
195
266
  .join("\n\n"),
196
267
  },
197
268
  ],
@@ -199,13 +270,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
199
270
  }
200
271
  // ── search_components ───────────────────────────────────────────────────────
201
272
  if (name === "search_components") {
202
- 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
+ }
203
304
  // Search name + description via ilike, then also fetch tag matches
204
305
  let nameQuery = supabase
205
306
  .from("components")
206
307
  .select("id, name, description, category, framework, downloads, likes, " +
207
308
  "profiles!components_author_id_fkey(username)")
208
309
  .eq("status", "published")
310
+ .eq("visibility", "public")
209
311
  .or(`name.ilike.%${q}%,description.ilike.%${q}%`)
210
312
  .order("downloads", { ascending: false })
211
313
  .limit(20);
@@ -239,6 +341,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
239
341
  .select("id, name, description, category, framework, downloads, likes, " +
240
342
  "profiles!components_author_id_fkey(username)")
241
343
  .eq("status", "published")
344
+ .eq("visibility", "public")
242
345
  .in("id", newTagIds)
243
346
  .limit(10);
244
347
  if (framework)
@@ -328,9 +431,42 @@ ${c.code_component}
328
431
  `;
329
432
  return { content: [{ type: "text", text: output }] };
330
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
+ }
331
467
  // ── create_component ────────────────────────────────────────────────────────
332
468
  if (name === "create_component") {
333
- 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 ?? {});
334
470
  const { createHash: createHash2 } = await import("crypto");
335
471
  const keyHashForCreate = createHash2("sha256").update(process.env.UIPLUG_API_KEY ?? "").digest("hex");
336
472
  const { data: componentId, error } = await supabase.rpc("create_component", {
@@ -343,10 +479,16 @@ ${c.code_component}
343
479
  p_installation: installation ?? null,
344
480
  p_tags: tags ?? [],
345
481
  p_model: model ?? null,
482
+ p_group_slug: group_slug ?? null,
483
+ p_visibility: group_slug ? (visibility ?? "public") : "public",
346
484
  });
347
485
  if (error) {
348
486
  return { content: [{ type: "text", text: `Error submitting component: ${error.message}` }], isError: true };
349
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
+ : "";
350
492
  return {
351
493
  content: [
352
494
  {
@@ -355,7 +497,8 @@ ${c.code_component}
355
497
  `**Name:** ${compName}\n` +
356
498
  `**Framework:** ${framework}\n` +
357
499
  `**Category:** ${category}\n` +
358
- `**ID:** ${componentId}\n\n` +
500
+ `**ID:** ${componentId}\n` +
501
+ groupNote + `\n` +
359
502
  `View and manage it at: https://uiplug.com/dashboard/components`,
360
503
  },
361
504
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uiplug-mcp",
3
- "version": "1.2.1",
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": {