@aicanvas/mcp 0.1.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 +144 -0
- package/dist/index.js +328 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# `@aicanvas/mcp`
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [AI Canvas](https://aicanvas.me). Lets your AI editor (Claude Code, Cursor, Claude Desktop, Codex) discover and install AI Canvas components without leaving the chat.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
You: "find me an animated card stack"
|
|
7
|
+
AI: [calls search_components → ranks matches → picks polaroid-stack]
|
|
8
|
+
[calls get_install_command → returns: npx shadcn add @aicanvas/polaroid-stack]
|
|
9
|
+
[runs the command — file lands in your project]
|
|
10
|
+
You: "make the rotation more dramatic"
|
|
11
|
+
AI: [edits the file directly]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What it exposes
|
|
15
|
+
|
|
16
|
+
Five tools, derived from the live [aicanvas.me](https://aicanvas.me) registry — adding a component to the website auto-updates the MCP within minutes:
|
|
17
|
+
|
|
18
|
+
| Tool | Purpose |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `list_categories` | Show every category with component counts. Orient before drilling in. |
|
|
21
|
+
| `list_components` | Browse components, optionally filtered by category. Paginated. |
|
|
22
|
+
| `search_components` | Fuzzy keyword search across slugs, names, descriptions, categories, tags. Ranked results. |
|
|
23
|
+
| `get_component` | Full metadata + complete `.tsx` source code for one component. |
|
|
24
|
+
| `get_install_command` | Get the `npx shadcn add @aicanvas/<slug>` command for a component. |
|
|
25
|
+
|
|
26
|
+
All tools are read-only. Nothing is mutated on AI Canvas's side.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### Claude Desktop
|
|
31
|
+
|
|
32
|
+
Edit `claude_desktop_config.json`:
|
|
33
|
+
|
|
34
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
35
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
36
|
+
- **Linux:** `~/.config/Claude/claude_desktop_config.json`
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"aicanvas": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@aicanvas/mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Restart Claude Desktop. The `aicanvas` tools should appear in the tools list.
|
|
50
|
+
|
|
51
|
+
### Cursor
|
|
52
|
+
|
|
53
|
+
Project-level (`.cursor/mcp.json` in your repo) or global (`~/.cursor/mcp.json`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"aicanvas": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@aicanvas/mcp"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Claude Code
|
|
67
|
+
|
|
68
|
+
Project-level `.mcp.json` (committed alongside your code):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"aicanvas": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "@aicanvas/mcp"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or via the CLI:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add aicanvas -- npx -y @aicanvas/mcp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Codex
|
|
88
|
+
|
|
89
|
+
`~/.codex/config.toml`:
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[mcp_servers.aicanvas]
|
|
93
|
+
command = "npx"
|
|
94
|
+
args = ["-y", "@aicanvas/mcp"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Verify it works
|
|
98
|
+
|
|
99
|
+
Inside any of the AI editors above, ask:
|
|
100
|
+
|
|
101
|
+
> "What categories of components does AI Canvas have?"
|
|
102
|
+
|
|
103
|
+
You should see all 11 categories with counts. If you don't, check the host's MCP logs — typically a "Failed to spawn process" or network error means npx couldn't fetch the package, or the host wasn't restarted after editing the config.
|
|
104
|
+
|
|
105
|
+
To inspect the server outside an editor:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx -y @modelcontextprotocol/inspector npx -y @aicanvas/mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This opens a browser UI where you can call each tool by hand.
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
| Env var | Default | What it does |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `AICANVAS_REGISTRY_BASE` | `https://aicanvas.me/r` | Registry root URL. Override only for local development against a self-hosted mirror. |
|
|
118
|
+
|
|
119
|
+
## How the registry stays fresh
|
|
120
|
+
|
|
121
|
+
The MCP fetches `https://aicanvas.me/r/aicanvas-mcp.json` on first use and caches it for 5 minutes. New components on AI Canvas appear in the MCP automatically — no package update required.
|
|
122
|
+
|
|
123
|
+
The full source code for each component is fetched on demand from `/r/<slug>.json` (the same shadcn registry items the CLI installs).
|
|
124
|
+
|
|
125
|
+
## Local development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/uiNerd16/aicanvas.git
|
|
129
|
+
cd aicanvas/mcp
|
|
130
|
+
npm install
|
|
131
|
+
npm run build
|
|
132
|
+
|
|
133
|
+
# Smoke-test against the deployed registry:
|
|
134
|
+
npm run inspect
|
|
135
|
+
|
|
136
|
+
# Or point at a locally running aicanvas dev server:
|
|
137
|
+
AICANVAS_REGISTRY_BASE=http://localhost:3000/r node dist/index.js
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Edit `src/index.ts`, run `npm run build`, restart your AI editor.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT — same as the AI Canvas project.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AI Canvas MCP server
|
|
4
|
+
* --------------------
|
|
5
|
+
* Exposes the AI Canvas standalone component registry to AI editors
|
|
6
|
+
* (Claude Code, Cursor, Claude Desktop, Codex). The AI can search,
|
|
7
|
+
* inspect, and install components without leaving the chat.
|
|
8
|
+
*
|
|
9
|
+
* Data flow:
|
|
10
|
+
* - aicanvas.me/r/aicanvas-mcp.json → all 64+ components' metadata
|
|
11
|
+
* - aicanvas.me/r/<slug>.json → full source code per component
|
|
12
|
+
*
|
|
13
|
+
* Both are static files, served from Vercel's CDN. Stateless server.
|
|
14
|
+
*/
|
|
15
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
19
|
+
const REGISTRY_BASE = process.env.AICANVAS_REGISTRY_BASE ?? 'https://aicanvas.me/r';
|
|
20
|
+
const META_URL = `${REGISTRY_BASE}/aicanvas-mcp.json`;
|
|
21
|
+
const META_TTL_MS = 5 * 60 * 1000; // 5 minutes — meta updates with deploys
|
|
22
|
+
// ── Cached fetchers ──────────────────────────────────────────────────────────
|
|
23
|
+
let metaCache = null;
|
|
24
|
+
async function fetchMeta() {
|
|
25
|
+
if (metaCache && Date.now() - metaCache.fetchedAt < META_TTL_MS) {
|
|
26
|
+
return metaCache.data;
|
|
27
|
+
}
|
|
28
|
+
const res = await fetch(META_URL, {
|
|
29
|
+
headers: { Accept: 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`Failed to fetch AI Canvas registry meta from ${META_URL}: ${res.status} ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
const data = (await res.json());
|
|
35
|
+
metaCache = { data, fetchedAt: Date.now() };
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
async function fetchComponentSource(slug) {
|
|
39
|
+
const url = `${REGISTRY_BASE}/${encodeURIComponent(slug)}.json`;
|
|
40
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Failed to fetch source for "${slug}" from ${url}: ${res.status} ${res.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
return (await res.json());
|
|
45
|
+
}
|
|
46
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
47
|
+
function normalize(s) {
|
|
48
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
49
|
+
}
|
|
50
|
+
function scoreMatch(query, c) {
|
|
51
|
+
const q = normalize(query);
|
|
52
|
+
if (!q)
|
|
53
|
+
return 0;
|
|
54
|
+
// Drop tokens of <=2 chars — they cause false positives via substring
|
|
55
|
+
// matches in unrelated words (e.g. "no" hits "notification").
|
|
56
|
+
const tokens = q.split(/\s+/).filter((t) => t.length > 2);
|
|
57
|
+
if (tokens.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
// Searchable surface — slug carries the most weight, then name, then
|
|
60
|
+
// description, then categories/tags.
|
|
61
|
+
const fields = [
|
|
62
|
+
{ text: normalize(c.slug), weight: 5 },
|
|
63
|
+
{ text: normalize(c.name), weight: 4 },
|
|
64
|
+
{ text: normalize(c.description), weight: 1 },
|
|
65
|
+
{ text: normalize(c.categories.join(' ')), weight: 2 },
|
|
66
|
+
{ text: normalize(c.tags.join(' ')), weight: 1 },
|
|
67
|
+
];
|
|
68
|
+
let score = 0;
|
|
69
|
+
for (const tok of tokens) {
|
|
70
|
+
for (const f of fields) {
|
|
71
|
+
if (!f.text)
|
|
72
|
+
continue;
|
|
73
|
+
// Whole-word match scores higher than substring.
|
|
74
|
+
if (new RegExp(`\\b${escapeRegExp(tok)}\\b`).test(f.text)) {
|
|
75
|
+
score += f.weight * 2;
|
|
76
|
+
}
|
|
77
|
+
else if (f.text.includes(tok)) {
|
|
78
|
+
score += f.weight;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return score;
|
|
83
|
+
}
|
|
84
|
+
function escapeRegExp(s) {
|
|
85
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
}
|
|
87
|
+
function asTextContent(value) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function errorResult(message) {
|
|
94
|
+
return {
|
|
95
|
+
content: [asTextContent(`Error: ${message}`)],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── Server ───────────────────────────────────────────────────────────────────
|
|
100
|
+
const server = new McpServer({ name: 'aicanvas-mcp', version: '0.1.0' }, { capabilities: { tools: {}, logging: {} } });
|
|
101
|
+
// ── Tool: list_categories ────────────────────────────────────────────────────
|
|
102
|
+
server.registerTool('list_categories', {
|
|
103
|
+
title: 'List AI Canvas categories',
|
|
104
|
+
description: 'Return every category in the AI Canvas standalone component library, with the number of components in each. Use to orient before listing or searching — e.g. "what kinds of components are available?"',
|
|
105
|
+
inputSchema: {},
|
|
106
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
107
|
+
}, async () => {
|
|
108
|
+
try {
|
|
109
|
+
const meta = await fetchMeta();
|
|
110
|
+
const lines = [
|
|
111
|
+
`AI Canvas — ${meta.componentCount} components across ${meta.categories.length} categories.`,
|
|
112
|
+
'',
|
|
113
|
+
...meta.categories.map((c) => ` ${c.label.padEnd(24)} ${String(c.count).padStart(3)} components`),
|
|
114
|
+
];
|
|
115
|
+
return {
|
|
116
|
+
content: [asTextContent(lines.join('\n'))],
|
|
117
|
+
structuredContent: {
|
|
118
|
+
componentCount: meta.componentCount,
|
|
119
|
+
categories: meta.categories,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// ── Tool: list_components ────────────────────────────────────────────────────
|
|
128
|
+
server.registerTool('list_components', {
|
|
129
|
+
title: 'List AI Canvas components',
|
|
130
|
+
description: 'Return AI Canvas components, optionally filtered by category. Use after `list_categories` to drill into one, or pass no filter to browse everything. Each result includes slug, name, description, categories, screenshot URL, install command, and homepage URL — enough to evaluate without a separate `get_component` call. Pagination via `limit` and `offset`.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
category: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Optional category label, e.g. "Cards & Modals", "Backgrounds", "Typography". Case-insensitive. Omit to list all components.'),
|
|
136
|
+
limit: z
|
|
137
|
+
.number()
|
|
138
|
+
.int()
|
|
139
|
+
.min(1)
|
|
140
|
+
.max(200)
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('Max number of components to return. Default 25.'),
|
|
143
|
+
offset: z
|
|
144
|
+
.number()
|
|
145
|
+
.int()
|
|
146
|
+
.min(0)
|
|
147
|
+
.optional()
|
|
148
|
+
.describe('Number of components to skip (for pagination). Default 0.'),
|
|
149
|
+
},
|
|
150
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
151
|
+
}, async ({ category, limit = 25, offset = 0 }) => {
|
|
152
|
+
try {
|
|
153
|
+
const meta = await fetchMeta();
|
|
154
|
+
let pool = meta.components;
|
|
155
|
+
if (category) {
|
|
156
|
+
const wanted = normalize(category);
|
|
157
|
+
pool = pool.filter((c) => c.categories.some((cat) => normalize(cat) === wanted));
|
|
158
|
+
}
|
|
159
|
+
const total = pool.length;
|
|
160
|
+
const slice = pool.slice(offset, offset + limit);
|
|
161
|
+
const summary = category != null
|
|
162
|
+
? `${total} components in category "${category}"` +
|
|
163
|
+
(total > slice.length
|
|
164
|
+
? ` (showing ${slice.length} from offset ${offset})`
|
|
165
|
+
: '')
|
|
166
|
+
: `${total} components total` +
|
|
167
|
+
(total > slice.length
|
|
168
|
+
? ` (showing ${slice.length} from offset ${offset})`
|
|
169
|
+
: '');
|
|
170
|
+
return {
|
|
171
|
+
content: [asTextContent(summary + '\n\n' + JSON.stringify(slice, null, 2))],
|
|
172
|
+
structuredContent: {
|
|
173
|
+
total,
|
|
174
|
+
offset,
|
|
175
|
+
limit,
|
|
176
|
+
returned: slice.length,
|
|
177
|
+
components: slice,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// ── Tool: search_components ──────────────────────────────────────────────────
|
|
186
|
+
server.registerTool('search_components', {
|
|
187
|
+
title: 'Search AI Canvas components',
|
|
188
|
+
description: 'Fuzzy keyword search across component slug, name, description, categories, and tags. Returns best matches ranked by relevance. Use when the user describes what they want in their own words, e.g. "an animated card stack", "background with waves", "typography that reveals on scroll". Results include screenshot URLs and install commands for immediate evaluation.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
query: z
|
|
191
|
+
.string()
|
|
192
|
+
.min(1)
|
|
193
|
+
.describe('Free-text search query. Multiple words are tokenized and matched independently.'),
|
|
194
|
+
limit: z
|
|
195
|
+
.number()
|
|
196
|
+
.int()
|
|
197
|
+
.min(1)
|
|
198
|
+
.max(50)
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Max number of matches to return. Default 10.'),
|
|
201
|
+
},
|
|
202
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
203
|
+
}, async ({ query, limit = 10 }) => {
|
|
204
|
+
try {
|
|
205
|
+
const meta = await fetchMeta();
|
|
206
|
+
const ranked = meta.components
|
|
207
|
+
.map((c) => ({ component: c, score: scoreMatch(query, c) }))
|
|
208
|
+
.filter((r) => r.score > 0)
|
|
209
|
+
.sort((a, b) => b.score - a.score)
|
|
210
|
+
.slice(0, limit)
|
|
211
|
+
.map((r) => r.component);
|
|
212
|
+
const summary = ranked.length === 0
|
|
213
|
+
? `No matches for "${query}". Try \`list_categories\` to browse what exists.`
|
|
214
|
+
: `${ranked.length} match${ranked.length === 1 ? '' : 'es'} for "${query}"`;
|
|
215
|
+
return {
|
|
216
|
+
content: [asTextContent(summary + '\n\n' + JSON.stringify(ranked, null, 2))],
|
|
217
|
+
structuredContent: { query, count: ranked.length, components: ranked },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// ── Tool: get_component ──────────────────────────────────────────────────────
|
|
225
|
+
server.registerTool('get_component', {
|
|
226
|
+
title: 'Get an AI Canvas component (full metadata + source code)',
|
|
227
|
+
description: 'Return the complete record for a single component: metadata + the entire .tsx source code as a string. Use after the user picks one from a search/list result and wants to see the implementation, or before installing if they want to inspect first. The source is the exact file the CLI install would write into a project.',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
slug: z
|
|
230
|
+
.string()
|
|
231
|
+
.min(1)
|
|
232
|
+
.describe('The component slug, e.g. "ai-job-cards", "wave-lines", "halo-type". Use exact slugs from list/search results.'),
|
|
233
|
+
},
|
|
234
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
235
|
+
}, async ({ slug }) => {
|
|
236
|
+
try {
|
|
237
|
+
const meta = await fetchMeta();
|
|
238
|
+
const component = meta.components.find((c) => c.slug === slug);
|
|
239
|
+
if (!component) {
|
|
240
|
+
return errorResult(`No component found with slug "${slug}". Use \`search_components\` or \`list_components\` to find valid slugs.`);
|
|
241
|
+
}
|
|
242
|
+
const source = await fetchComponentSource(slug);
|
|
243
|
+
const code = source.files[0]?.content ?? '';
|
|
244
|
+
const filePath = source.files[0]?.path ?? `components/aicanvas/${slug}.tsx`;
|
|
245
|
+
const result = {
|
|
246
|
+
...component,
|
|
247
|
+
filePath,
|
|
248
|
+
code,
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
asTextContent([
|
|
253
|
+
`# ${component.name}`,
|
|
254
|
+
'',
|
|
255
|
+
component.description,
|
|
256
|
+
'',
|
|
257
|
+
`Categories: ${component.categories.join(', ') || '(none)'}`,
|
|
258
|
+
`Dependencies: ${component.dependencies.join(', ') || '(none)'}`,
|
|
259
|
+
`Install: ${component.installCommand}`,
|
|
260
|
+
`Homepage: ${component.homepageUrl}`,
|
|
261
|
+
'',
|
|
262
|
+
`--- ${filePath} ---`,
|
|
263
|
+
code,
|
|
264
|
+
].join('\n')),
|
|
265
|
+
],
|
|
266
|
+
structuredContent: result,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// ── Tool: get_install_command ────────────────────────────────────────────────
|
|
274
|
+
server.registerTool('get_install_command', {
|
|
275
|
+
title: 'Get the AI Canvas install command for a component',
|
|
276
|
+
description: 'Return the shadcn CLI command to install a single component into the user\'s project. After calling this, suggest the user run the command (or run it yourself if you have shell access). Requires shadcn-aware setup (Tailwind v4, components.json) — for projects without that, fall back to `get_component` and write the file manually.',
|
|
277
|
+
inputSchema: {
|
|
278
|
+
slug: z
|
|
279
|
+
.string()
|
|
280
|
+
.min(1)
|
|
281
|
+
.describe('The component slug, e.g. "ai-job-cards".'),
|
|
282
|
+
},
|
|
283
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
284
|
+
}, async ({ slug }) => {
|
|
285
|
+
try {
|
|
286
|
+
const meta = await fetchMeta();
|
|
287
|
+
const component = meta.components.find((c) => c.slug === slug);
|
|
288
|
+
if (!component) {
|
|
289
|
+
return errorResult(`No component found with slug "${slug}". Use \`search_components\` to find valid slugs.`);
|
|
290
|
+
}
|
|
291
|
+
const out = {
|
|
292
|
+
slug: component.slug,
|
|
293
|
+
name: component.name,
|
|
294
|
+
installCommand: component.installCommand,
|
|
295
|
+
sourceUrl: component.sourceUrl,
|
|
296
|
+
homepageUrl: component.homepageUrl,
|
|
297
|
+
dependencies: component.dependencies,
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
asTextContent([
|
|
302
|
+
`Install ${component.name}:`,
|
|
303
|
+
'',
|
|
304
|
+
` ${component.installCommand}`,
|
|
305
|
+
'',
|
|
306
|
+
`Dependencies: ${component.dependencies.join(', ') || '(none — React only)'}`,
|
|
307
|
+
`Source: ${component.sourceUrl}`,
|
|
308
|
+
`Preview: ${component.homepageUrl}`,
|
|
309
|
+
].join('\n')),
|
|
310
|
+
],
|
|
311
|
+
structuredContent: out,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// ── Boot ─────────────────────────────────────────────────────────────────────
|
|
319
|
+
async function main() {
|
|
320
|
+
const transport = new StdioServerTransport();
|
|
321
|
+
await server.connect(transport);
|
|
322
|
+
// stdio transport runs forever — closes when host kills stdin
|
|
323
|
+
}
|
|
324
|
+
main().catch((err) => {
|
|
325
|
+
// stderr only — stdout is reserved for JSON-RPC
|
|
326
|
+
console.error('aicanvas-mcp fatal:', err);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aicanvas/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for AI Canvas — search, inspect, and install components from the aicanvas.me registry directly from your AI editor.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"aicanvas",
|
|
9
|
+
"shadcn",
|
|
10
|
+
"react",
|
|
11
|
+
"components",
|
|
12
|
+
"framer-motion"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://aicanvas.me",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/uiNerd16/aicanvas.git",
|
|
18
|
+
"directory": "mcp"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "AI Canvas <https://aicanvas.me>",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"aicanvas-mcp": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"test": "npm run build && node test/smoke.mjs && node test/offline.mjs",
|
|
37
|
+
"test:smoke": "npm run build && node test/smoke.mjs",
|
|
38
|
+
"test:offline": "npm run build && node test/offline.mjs",
|
|
39
|
+
"inspect": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
|
+
"zod": "^3.25.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"typescript": "^5.6.0"
|
|
49
|
+
}
|
|
50
|
+
}
|