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.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/dist/index.js +265 -0
  3. 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
+ }