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.
- package/dist/index.js +183 -40
- 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
|
|
44
|
-
"
|
|
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: "
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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) =>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
(c.
|
|
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
|
|
500
|
+
`**ID:** ${componentId}\n` +
|
|
501
|
+
groupNote + `\n` +
|
|
359
502
|
`View and manage it at: https://uiplug.com/dashboard/components`,
|
|
360
503
|
},
|
|
361
504
|
],
|