uiplug-mcp 1.3.3 → 1.3.5
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/README.md +13 -2
- package/dist/index.js +212 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,18 @@ MCP server for [UIPlug](https://uiplug.com) — gives Claude and other AI agents
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
Exposes
|
|
7
|
+
Exposes 8 tools to any MCP-compatible agent:
|
|
8
8
|
|
|
9
9
|
| Tool | Description |
|
|
10
10
|
|------|-------------|
|
|
11
11
|
| `list_components` | Browse published components, filter by framework / category |
|
|
12
|
-
| `search_components` | Search by name, description, or tag |
|
|
12
|
+
| `search_components` | Search public marketplace by name, description, or tag |
|
|
13
13
|
| `get_component` | Get full source code + installation instructions |
|
|
14
|
+
| `get_my_profile` | View your UIPlug profile and component stats |
|
|
15
|
+
| `list_my_components` | List all components you've created (including pending) |
|
|
16
|
+
| `search_my_components` | Search through your own saved components |
|
|
17
|
+
| `list_groups` | List UIPlug groups you're a member of |
|
|
18
|
+
| `create_component` | Submit a new component to the marketplace |
|
|
14
19
|
|
|
15
20
|
## Authentication
|
|
16
21
|
|
|
@@ -44,6 +49,12 @@ Restart Claude Desktop.
|
|
|
44
49
|
|
|
45
50
|
> "Get the Gradient Button component from UIPlug and adapt it to my design system."
|
|
46
51
|
|
|
52
|
+
> "Show me all my UIPlug components that are still pending review."
|
|
53
|
+
|
|
54
|
+
> "Search my saved UIPlug components for anything button-related."
|
|
55
|
+
|
|
56
|
+
> "What's my UIPlug profile and how many downloads do I have?"
|
|
57
|
+
|
|
47
58
|
## Supported frameworks
|
|
48
59
|
|
|
49
60
|
React · Vue · Svelte · Angular · HTML/CSS · Jetpack Compose · Compose Multiplatform · Flutter · SwiftUI · React Native
|
package/dist/index.js
CHANGED
|
@@ -109,6 +109,68 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: "get_my_profile",
|
|
114
|
+
description: "Get your UIPlug profile information including username, bio, avatar, " +
|
|
115
|
+
"and component stats. Requires a valid UIPLUG_API_KEY.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "list_my_components",
|
|
123
|
+
description: "List all UI components you have created on UIPlug, including pending and published ones. " +
|
|
124
|
+
"Requires a valid UIPLUG_API_KEY. Use this to see your own saved components, check their status, " +
|
|
125
|
+
"or find IDs to pass to get_component.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
framework: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Filter by framework (optional).",
|
|
132
|
+
},
|
|
133
|
+
category: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Filter by category (optional).",
|
|
136
|
+
},
|
|
137
|
+
status: {
|
|
138
|
+
type: "string",
|
|
139
|
+
enum: ["pending", "published", "rejected"],
|
|
140
|
+
description: "Filter by status (optional). Omit to return all.",
|
|
141
|
+
},
|
|
142
|
+
limit: {
|
|
143
|
+
type: "number",
|
|
144
|
+
description: "Maximum number of results to return (default 20, max 50).",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "search_my_components",
|
|
151
|
+
description: "Search through your own UIPlug components by name, description, or tag. " +
|
|
152
|
+
"Unlike search_components (which searches the public marketplace), this only searches " +
|
|
153
|
+
"components you have created — including pending or unpublished ones.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
required: ["query"],
|
|
157
|
+
properties: {
|
|
158
|
+
query: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Search term — matched against name, description, and tags.",
|
|
161
|
+
},
|
|
162
|
+
framework: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Narrow results to a specific framework (optional).",
|
|
165
|
+
},
|
|
166
|
+
status: {
|
|
167
|
+
type: "string",
|
|
168
|
+
enum: ["pending", "published", "rejected"],
|
|
169
|
+
description: "Filter by status (optional). Omit to return all.",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
112
174
|
{
|
|
113
175
|
name: "list_groups",
|
|
114
176
|
description: "List the UIPlug groups you are an active member of. " +
|
|
@@ -431,6 +493,156 @@ ${c.code_component}
|
|
|
431
493
|
`;
|
|
432
494
|
return { content: [{ type: "text", text: output }] };
|
|
433
495
|
}
|
|
496
|
+
// ── get_my_profile ──────────────────────────────────────────────────────────
|
|
497
|
+
if (name === "get_my_profile") {
|
|
498
|
+
const [profileRes, statsRes] = await Promise.all([
|
|
499
|
+
supabase
|
|
500
|
+
.from("profiles")
|
|
501
|
+
.select("username, full_name, bio, avatar_url, website, created_at")
|
|
502
|
+
.eq("id", userId)
|
|
503
|
+
.single(),
|
|
504
|
+
supabase
|
|
505
|
+
.from("components")
|
|
506
|
+
.select("id, status, downloads, likes")
|
|
507
|
+
.eq("author_id", userId),
|
|
508
|
+
]);
|
|
509
|
+
if (profileRes.error || !profileRes.data) {
|
|
510
|
+
return { content: [{ type: "text", text: `Profile not found: ${profileRes.error?.message}` }], isError: true };
|
|
511
|
+
}
|
|
512
|
+
const p = profileRes.data;
|
|
513
|
+
const components = statsRes.data ?? [];
|
|
514
|
+
const published = components.filter((c) => c.status === "published").length;
|
|
515
|
+
const pending = components.filter((c) => c.status === "pending").length;
|
|
516
|
+
const totalDownloads = components.reduce((sum, c) => sum + (c.downloads ?? 0), 0);
|
|
517
|
+
const totalLikes = components.reduce((sum, c) => sum + (c.likes ?? 0), 0);
|
|
518
|
+
return {
|
|
519
|
+
content: [{
|
|
520
|
+
type: "text",
|
|
521
|
+
text: `# Your UIPlug Profile\n\n` +
|
|
522
|
+
`**Username:** ${p.username ?? "—"}\n` +
|
|
523
|
+
`**Full Name:** ${p.full_name ?? "—"}\n` +
|
|
524
|
+
`**Bio:** ${p.bio ?? "—"}\n` +
|
|
525
|
+
`**Website:** ${p.website ?? "—"}\n` +
|
|
526
|
+
`**Member since:** ${p.created_at ? new Date(p.created_at).toDateString() : "—"}\n\n` +
|
|
527
|
+
`## Component Stats\n` +
|
|
528
|
+
`- Total components: ${components.length}\n` +
|
|
529
|
+
`- Published: ${published}\n` +
|
|
530
|
+
`- Pending review: ${pending}\n` +
|
|
531
|
+
`- Total downloads: ${totalDownloads}\n` +
|
|
532
|
+
`- Total likes: ${totalLikes}`,
|
|
533
|
+
}],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
// ── list_my_components ──────────────────────────────────────────────────────
|
|
537
|
+
if (name === "list_my_components") {
|
|
538
|
+
const { framework, category, status, limit = 20 } = (args ?? {});
|
|
539
|
+
let query = supabase
|
|
540
|
+
.from("components")
|
|
541
|
+
.select("id, name, description, category, framework, downloads, likes, model, status, visibility, created_at")
|
|
542
|
+
.eq("author_id", userId)
|
|
543
|
+
.order("created_at", { ascending: false })
|
|
544
|
+
.limit(Math.min(limit, 50));
|
|
545
|
+
if (framework)
|
|
546
|
+
query = query.eq("framework", framework);
|
|
547
|
+
if (category)
|
|
548
|
+
query = query.eq("category", category);
|
|
549
|
+
if (status)
|
|
550
|
+
query = query.eq("status", status);
|
|
551
|
+
const { data, error } = await query;
|
|
552
|
+
if (error) {
|
|
553
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
554
|
+
}
|
|
555
|
+
const rows = data ?? [];
|
|
556
|
+
if (rows.length === 0) {
|
|
557
|
+
return { content: [{ type: "text", text: "You have no saved components yet. Use create_component to add one." }] };
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
content: [{
|
|
561
|
+
type: "text",
|
|
562
|
+
text: `You have ${rows.length} component(s):\n\n` +
|
|
563
|
+
rows.map((c) => {
|
|
564
|
+
const visTag = c.visibility === "private" ? " 🔒" : "";
|
|
565
|
+
const statusEmoji = c.status === "published" ? "✅" : c.status === "pending" ? "⏳" : "❌";
|
|
566
|
+
return (`${statusEmoji} **${c.name}**${visTag} (${c.framework} · ${c.category})\n` +
|
|
567
|
+
` ID: ${c.id}\n` +
|
|
568
|
+
` ${c.description}\n` +
|
|
569
|
+
` Status: ${c.status} · ↓${c.downloads ?? 0} ♥${c.likes ?? 0}` +
|
|
570
|
+
(c.model ? ` · Built with ${c.model}` : ""));
|
|
571
|
+
}).join("\n\n"),
|
|
572
|
+
}],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// ── search_my_components ────────────────────────────────────────────────────
|
|
576
|
+
if (name === "search_my_components") {
|
|
577
|
+
const { query: q, framework, status } = (args ?? {});
|
|
578
|
+
// Search name + description
|
|
579
|
+
let nameQuery = supabase
|
|
580
|
+
.from("components")
|
|
581
|
+
.select("id, name, description, category, framework, downloads, likes, status, visibility, model, created_at")
|
|
582
|
+
.eq("author_id", userId)
|
|
583
|
+
.or(`name.ilike.%${q}%,description.ilike.%${q}%`)
|
|
584
|
+
.order("created_at", { ascending: false })
|
|
585
|
+
.limit(30);
|
|
586
|
+
if (framework)
|
|
587
|
+
nameQuery = nameQuery.eq("framework", framework);
|
|
588
|
+
if (status)
|
|
589
|
+
nameQuery = nameQuery.eq("status", status);
|
|
590
|
+
// Also search by tag
|
|
591
|
+
const { data: tagData } = await supabase
|
|
592
|
+
.from("tags")
|
|
593
|
+
.select("id")
|
|
594
|
+
.ilike("name", `%${q}%`);
|
|
595
|
+
const tagIds = (tagData ?? []).map((t) => t.id);
|
|
596
|
+
let tagComponentIds = [];
|
|
597
|
+
if (tagIds.length > 0) {
|
|
598
|
+
const { data: ctData } = await supabase
|
|
599
|
+
.from("component_tags")
|
|
600
|
+
.select("component_id")
|
|
601
|
+
.in("tag_id", tagIds);
|
|
602
|
+
tagComponentIds = (ctData ?? []).map((ct) => ct.component_id);
|
|
603
|
+
}
|
|
604
|
+
const { data: nameResults, error } = await nameQuery;
|
|
605
|
+
if (error) {
|
|
606
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
607
|
+
}
|
|
608
|
+
// Fetch tag-matched components owned by the user not already in name results
|
|
609
|
+
let tagResults = [];
|
|
610
|
+
const existingIds = new Set((nameResults ?? []).map((c) => c.id));
|
|
611
|
+
const newTagIds = tagComponentIds.filter((id) => !existingIds.has(id));
|
|
612
|
+
if (newTagIds.length > 0) {
|
|
613
|
+
let tq = supabase
|
|
614
|
+
.from("components")
|
|
615
|
+
.select("id, name, description, category, framework, downloads, likes, status, visibility, model, created_at")
|
|
616
|
+
.eq("author_id", userId)
|
|
617
|
+
.in("id", newTagIds)
|
|
618
|
+
.limit(10);
|
|
619
|
+
if (framework)
|
|
620
|
+
tq = tq.eq("framework", framework);
|
|
621
|
+
if (status)
|
|
622
|
+
tq = tq.eq("status", status);
|
|
623
|
+
const { data } = await tq;
|
|
624
|
+
tagResults = data ?? [];
|
|
625
|
+
}
|
|
626
|
+
const all = [...(nameResults ?? []), ...tagResults];
|
|
627
|
+
if (all.length === 0) {
|
|
628
|
+
return { content: [{ type: "text", text: `No components found matching "${q}" in your saved components.` }] };
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
content: [{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: `Found ${all.length} of your component(s) matching "${q}":\n\n` +
|
|
634
|
+
all.map((c) => {
|
|
635
|
+
const visTag = c.visibility === "private" ? " 🔒" : "";
|
|
636
|
+
const statusEmoji = c.status === "published" ? "✅" : c.status === "pending" ? "⏳" : "❌";
|
|
637
|
+
return (`${statusEmoji} **${c.name}**${visTag} (${c.framework} · ${c.category})\n` +
|
|
638
|
+
` ID: ${c.id}\n` +
|
|
639
|
+
` ${c.description}\n` +
|
|
640
|
+
` Status: ${c.status} · ↓${c.downloads ?? 0} ♥${c.likes ?? 0}` +
|
|
641
|
+
(c.model ? ` · Built with ${c.model}` : ""));
|
|
642
|
+
}).join("\n\n"),
|
|
643
|
+
}],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
434
646
|
// ── list_groups ─────────────────────────────────────────────────────────────
|
|
435
647
|
if (name === "list_groups") {
|
|
436
648
|
const { createHash: ch } = await import("crypto");
|