uiplug-mcp 1.0.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/README.md +61 -0
- package/dist/index.js +265 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# uiplug-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [UIPlug](https://uiplug.com) — gives Claude and other AI agents instant access to the UIPlug UI component marketplace.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Exposes three tools to any MCP-compatible agent:
|
|
8
|
+
|
|
9
|
+
| Tool | Description |
|
|
10
|
+
|------|-------------|
|
|
11
|
+
| `list_components` | Browse published components, filter by framework / category |
|
|
12
|
+
| `search_components` | Search by name, description, or tag |
|
|
13
|
+
| `get_component` | Get full source code + installation instructions |
|
|
14
|
+
|
|
15
|
+
## Usage with Claude Desktop
|
|
16
|
+
|
|
17
|
+
Add this to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"uiplug": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "uiplug-mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart Claude Desktop. No API key needed — works out of the box.
|
|
31
|
+
|
|
32
|
+
## Example prompts
|
|
33
|
+
|
|
34
|
+
> "Build me a React dashboard. Use UIPlug to find a skeleton loader and a card component."
|
|
35
|
+
|
|
36
|
+
> "Search UIPlug for navigation components for Jetpack Compose."
|
|
37
|
+
|
|
38
|
+
> "Get the Gradient Button component from UIPlug and adapt it to my design system."
|
|
39
|
+
|
|
40
|
+
## Supported frameworks
|
|
41
|
+
|
|
42
|
+
React · Vue · Svelte · Angular · HTML/CSS · Jetpack Compose · Compose Multiplatform · Flutter · SwiftUI · React Native
|
|
43
|
+
|
|
44
|
+
## Self-hosting
|
|
45
|
+
|
|
46
|
+
To point the server at your own Supabase instance:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"uiplug": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "uiplug-mcp"],
|
|
54
|
+
"env": {
|
|
55
|
+
"SUPABASE_URL": "https://your-project.supabase.co",
|
|
56
|
+
"SUPABASE_ANON_KEY": "your-anon-key"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createClient } from "@supabase/supabase-js";
|
|
6
|
+
// ── Supabase client ───────────────────────────────────────────────────────────
|
|
7
|
+
// Public anon key — safe to hardcode (read-only, RLS-protected).
|
|
8
|
+
// Override with env vars to point at your own Supabase instance.
|
|
9
|
+
const SUPABASE_URL = process.env.SUPABASE_URL ?? "https://uuoexpurygmgfouiawuc.supabase.co";
|
|
10
|
+
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ??
|
|
11
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV1b2V4cHVyeWdtZ2ZvdWlhd3VjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2NzI5MTMsImV4cCI6MjA4NzI0ODkxM30.4gogFo90o8lfZ9_iKZfhXQ9QHtx2VKhMu9Hgy_2lI_g";
|
|
12
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
13
|
+
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
14
|
+
const server = new Server({ name: "uiplug", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
15
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
16
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
17
|
+
tools: [
|
|
18
|
+
{
|
|
19
|
+
name: "list_components",
|
|
20
|
+
description: "List published UI components from the UIPlug marketplace. " +
|
|
21
|
+
"Optionally filter by framework (e.g. React, Vue, Jetpack Compose, Flutter, SwiftUI) " +
|
|
22
|
+
"and/or category (e.g. Layout, Navigation, Input, Data Display, Feedback, Sensors).",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
framework: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Filter by framework: React | Vue | Svelte | Angular | HTML / CSS | " +
|
|
29
|
+
"Jetpack Compose | Compose Multiplatform | Flutter | SwiftUI | React Native",
|
|
30
|
+
},
|
|
31
|
+
category: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Filter by category: Layout | Navigation | Input | Data Display | Feedback | Sensors | AR Glasses",
|
|
34
|
+
},
|
|
35
|
+
limit: {
|
|
36
|
+
type: "number",
|
|
37
|
+
description: "Maximum number of results to return (default 20, max 50).",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "search_components",
|
|
44
|
+
description: "Search UIPlug components by name, description, or tag. " +
|
|
45
|
+
"Returns matching components with a summary of their metadata.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
required: ["query"],
|
|
49
|
+
properties: {
|
|
50
|
+
query: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Search term — matched against name, description, and tags.",
|
|
53
|
+
},
|
|
54
|
+
framework: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Narrow results to a specific framework.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_component",
|
|
63
|
+
description: "Get the full source code and metadata for a specific UIPlug component by its ID. " +
|
|
64
|
+
"Use this after list_components or search_components to retrieve the actual code " +
|
|
65
|
+
"you want to use in your project.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
required: ["id"],
|
|
69
|
+
properties: {
|
|
70
|
+
id: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "The component UUID returned by list_components or search_components.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}));
|
|
79
|
+
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
80
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
81
|
+
const { name, arguments: args } = request.params;
|
|
82
|
+
// ── list_components ─────────────────────────────────────────────────────────
|
|
83
|
+
if (name === "list_components") {
|
|
84
|
+
const { framework, category, limit = 20 } = (args ?? {});
|
|
85
|
+
let query = supabase
|
|
86
|
+
.from("components")
|
|
87
|
+
.select("id, name, description, category, framework, downloads, likes, model, " +
|
|
88
|
+
"profiles!components_author_id_fkey(username)")
|
|
89
|
+
.eq("status", "published")
|
|
90
|
+
.order("downloads", { ascending: false })
|
|
91
|
+
.limit(Math.min(limit, 50));
|
|
92
|
+
if (framework)
|
|
93
|
+
query = query.eq("framework", framework);
|
|
94
|
+
if (category)
|
|
95
|
+
query = query.eq("category", category);
|
|
96
|
+
const { data, error } = await query;
|
|
97
|
+
if (error) {
|
|
98
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
const rows = (data ?? []).map((c) => ({
|
|
101
|
+
id: c.id,
|
|
102
|
+
name: c.name,
|
|
103
|
+
description: c.description,
|
|
104
|
+
category: c.category,
|
|
105
|
+
framework: c.framework,
|
|
106
|
+
author: c.profiles?.username ?? "Unknown",
|
|
107
|
+
downloads: c.downloads ?? 0,
|
|
108
|
+
likes: c.likes ?? 0,
|
|
109
|
+
model: c.model ?? null,
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `Found ${rows.length} component(s).\n\n` +
|
|
116
|
+
rows
|
|
117
|
+
.map((c) => `**${c.name}** (${c.framework} · ${c.category})\n` +
|
|
118
|
+
` ID: ${c.id}\n` +
|
|
119
|
+
` ${c.description}\n` +
|
|
120
|
+
` Author: ${c.author} · ↓${c.downloads} ♥${c.likes}` +
|
|
121
|
+
(c.model ? ` · Built with ${c.model}` : ""))
|
|
122
|
+
.join("\n\n"),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ── search_components ───────────────────────────────────────────────────────
|
|
128
|
+
if (name === "search_components") {
|
|
129
|
+
const { query: q, framework } = (args ?? {});
|
|
130
|
+
// Search name + description via ilike, then also fetch tag matches
|
|
131
|
+
let nameQuery = supabase
|
|
132
|
+
.from("components")
|
|
133
|
+
.select("id, name, description, category, framework, downloads, likes, " +
|
|
134
|
+
"profiles!components_author_id_fkey(username)")
|
|
135
|
+
.eq("status", "published")
|
|
136
|
+
.or(`name.ilike.%${q}%,description.ilike.%${q}%`)
|
|
137
|
+
.order("downloads", { ascending: false })
|
|
138
|
+
.limit(20);
|
|
139
|
+
if (framework)
|
|
140
|
+
nameQuery = nameQuery.eq("framework", framework);
|
|
141
|
+
// Also search by tag name
|
|
142
|
+
const { data: tagData } = await supabase
|
|
143
|
+
.from("tags")
|
|
144
|
+
.select("id")
|
|
145
|
+
.ilike("name", `%${q}%`);
|
|
146
|
+
const tagIds = (tagData ?? []).map((t) => t.id);
|
|
147
|
+
let tagComponentIds = [];
|
|
148
|
+
if (tagIds.length > 0) {
|
|
149
|
+
const { data: ctData } = await supabase
|
|
150
|
+
.from("component_tags")
|
|
151
|
+
.select("component_id")
|
|
152
|
+
.in("tag_id", tagIds);
|
|
153
|
+
tagComponentIds = (ctData ?? []).map((ct) => ct.component_id);
|
|
154
|
+
}
|
|
155
|
+
const { data: nameResults, error } = await nameQuery;
|
|
156
|
+
if (error) {
|
|
157
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
158
|
+
}
|
|
159
|
+
// Fetch tag-matched components not already in name results
|
|
160
|
+
let tagResults = [];
|
|
161
|
+
const existingIds = new Set((nameResults ?? []).map((c) => c.id));
|
|
162
|
+
const newTagIds = tagComponentIds.filter((id) => !existingIds.has(id));
|
|
163
|
+
if (newTagIds.length > 0) {
|
|
164
|
+
let tq = supabase
|
|
165
|
+
.from("components")
|
|
166
|
+
.select("id, name, description, category, framework, downloads, likes, " +
|
|
167
|
+
"profiles!components_author_id_fkey(username)")
|
|
168
|
+
.eq("status", "published")
|
|
169
|
+
.in("id", newTagIds)
|
|
170
|
+
.limit(10);
|
|
171
|
+
if (framework)
|
|
172
|
+
tq = tq.eq("framework", framework);
|
|
173
|
+
const { data } = await tq;
|
|
174
|
+
tagResults = data ?? [];
|
|
175
|
+
}
|
|
176
|
+
const all = [...(nameResults ?? []), ...tagResults];
|
|
177
|
+
if (all.length === 0) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: `No components found matching "${q}".` }],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: `Found ${all.length} result(s) for "${q}":\n\n` +
|
|
187
|
+
all
|
|
188
|
+
.map((c) => `**${c.name}** (${c.framework} · ${c.category})\n` +
|
|
189
|
+
` ID: ${c.id}\n` +
|
|
190
|
+
` ${c.description}\n` +
|
|
191
|
+
` Author: ${c.profiles?.username ?? "Unknown"} · ↓${c.downloads ?? 0}`)
|
|
192
|
+
.join("\n\n"),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// ── get_component ───────────────────────────────────────────────────────────
|
|
198
|
+
if (name === "get_component") {
|
|
199
|
+
const { id } = (args ?? {});
|
|
200
|
+
const [compRes, tagRes] = await Promise.all([
|
|
201
|
+
supabase
|
|
202
|
+
.from("components")
|
|
203
|
+
.select("id, name, description, category, framework, downloads, likes, model, " +
|
|
204
|
+
"installation, code_component, preview_key, " +
|
|
205
|
+
"hardware_version, hardware_brand, hardware_compatibility, " +
|
|
206
|
+
"profiles!components_author_id_fkey(username)")
|
|
207
|
+
.eq("id", id)
|
|
208
|
+
.eq("status", "published")
|
|
209
|
+
.single(),
|
|
210
|
+
supabase
|
|
211
|
+
.from("component_tags")
|
|
212
|
+
.select("tags(name)")
|
|
213
|
+
.eq("component_id", id),
|
|
214
|
+
]);
|
|
215
|
+
if (compRes.error || !compRes.data) {
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: `Component not found: ${compRes.error?.message ?? id}` }],
|
|
218
|
+
isError: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const c = compRes.data;
|
|
222
|
+
const tags = (tagRes.data ?? []).map((t) => t.tags?.name).filter(Boolean);
|
|
223
|
+
// Increment download count
|
|
224
|
+
await supabase.rpc("increment_downloads", { component_id: id }).maybeSingle();
|
|
225
|
+
const hwSection = c.hardware_version
|
|
226
|
+
? `\n## Hardware Requirements\n- Brand: ${c.hardware_brand}\n- Version: ${c.hardware_version}\n- Compatibility: ${c.hardware_compatibility}`
|
|
227
|
+
: "";
|
|
228
|
+
const installSection = c.installation
|
|
229
|
+
? `\n## Installation\n\`\`\`\n${c.installation}\n\`\`\``
|
|
230
|
+
: "";
|
|
231
|
+
const ext = c.framework === "Flutter" ? "dart"
|
|
232
|
+
: c.framework === "SwiftUI" ? "swift"
|
|
233
|
+
: c.framework === "React" ? "tsx"
|
|
234
|
+
: c.framework === "Vue" ? "vue"
|
|
235
|
+
: c.framework === "Svelte" ? "svelte"
|
|
236
|
+
: c.framework === "HTML / CSS" ? "html"
|
|
237
|
+
: "kt";
|
|
238
|
+
const output = `# ${c.name}
|
|
239
|
+
|
|
240
|
+
**Framework:** ${c.framework}
|
|
241
|
+
**Category:** ${c.category}
|
|
242
|
+
**Author:** ${c.profiles?.username ?? "Unknown"}
|
|
243
|
+
**Tags:** ${tags.length ? tags.join(", ") : "none"}
|
|
244
|
+
**Downloads:** ${c.downloads ?? 0} · **Likes:** ${c.likes ?? 0}${c.model ? `\n**Built with:** ${c.model}` : ""}
|
|
245
|
+
|
|
246
|
+
## Description
|
|
247
|
+
${c.description}
|
|
248
|
+
${installSection}
|
|
249
|
+
${hwSection}
|
|
250
|
+
|
|
251
|
+
## Code
|
|
252
|
+
\`\`\`${ext}
|
|
253
|
+
${c.code_component}
|
|
254
|
+
\`\`\`
|
|
255
|
+
`;
|
|
256
|
+
return { content: [{ type: "text", text: output }] };
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
264
|
+
const transport = new StdioServerTransport();
|
|
265
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uiplug-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for UIPlug — gives AI agents access to the UIPlug UI component marketplace",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"uiplug-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && node -e \"const fs=require('fs');const f='dist/index.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,'755')\"",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"dev": "tsx index.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"claude",
|
|
20
|
+
"ui-components",
|
|
21
|
+
"uiplug",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"react",
|
|
24
|
+
"jetpack-compose",
|
|
25
|
+
"flutter"
|
|
26
|
+
],
|
|
27
|
+
"author": "UIPlug",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
+
"@supabase/supabase-js": "^2.97.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsx": "^4.21.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
}
|
|
41
|
+
}
|