composter-cli 1.0.4 → 1.0.6
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 +542 -22
- package/package.json +10 -4
- package/src/commands/mcp.js +238 -0
- package/src/index.js +31 -0
- package/src/mcp/server.js +289 -0
package/README.md
CHANGED
|
@@ -4,44 +4,564 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/composter-cli)
|
|
6
6
|
[](https://github.com/binit2-1/Composter/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
7
8
|
|
|
8
9
|
**Composter** is like your personal shadcn/ui — but for YOUR components. Push components from any project, pull them into new ones, and let AI assistants access your vault via MCP.
|
|
9
10
|
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🎯 Why Composter?
|
|
14
|
+
|
|
15
|
+
Ever built an amazing component and wanted to reuse it in another project? Tired of copy-pasting files and forgetting dependencies? Composter solves this:
|
|
16
|
+
|
|
17
|
+
- **No more copy-paste chaos** — Push once, pull anywhere
|
|
18
|
+
- **Dependencies tracked automatically** — Never forget to install packages again
|
|
19
|
+
- **Smart bundling** — Relative imports are bundled, npm packages are tracked
|
|
20
|
+
- **AI-ready** — Let Claude, Cursor, or Copilot access your vault via MCP
|
|
21
|
+
- **Private by default** — Your components, your vault, your control
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
10
25
|
## ✨ Features
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
| Feature | Description |
|
|
28
|
+
|---------|-------------|
|
|
29
|
+
| 📤 **Push** | Upload components with all local dependencies auto-bundled |
|
|
30
|
+
| 📥 **Pull** | Download components with original folder structure preserved |
|
|
31
|
+
| 📁 **Categories** | Organize components into logical groups (UI, Hooks, Utils) |
|
|
32
|
+
| 🔗 **Smart Bundling** | Automatically crawls `./` and `@/` imports |
|
|
33
|
+
| 📦 **Dependency Detection** | Reads your `package.json` and tracks npm packages |
|
|
34
|
+
| ⚠️ **Missing Dep Alerts** | Tells you exactly what to `npm install` after pulling |
|
|
35
|
+
| 🤖 **MCP Integration** | Works with AI assistants via Model Context Protocol |
|
|
36
|
+
|
|
37
|
+
---
|
|
18
38
|
|
|
19
39
|
## 📦 Installation
|
|
20
40
|
|
|
21
|
-
|
|
41
|
+
### Global Install (Recommended)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g composter-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Using npx
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx composter-cli login
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Verify Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
composter --version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🚀 Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Create an Account
|
|
64
|
+
|
|
65
|
+
Visit **[composter.vercel.app](https://composter.vercel.app)** and sign up.
|
|
66
|
+
|
|
67
|
+
### 2. Login via CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
composter login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
=== Composter Login ===
|
|
75
|
+
? Email: you@example.com
|
|
76
|
+
? Password: ********
|
|
22
77
|
|
|
23
|
-
|
|
78
|
+
Logged in successfully!
|
|
79
|
+
Session expires: 2025-01-09T12:00:00.000Z
|
|
80
|
+
```
|
|
24
81
|
|
|
25
|
-
|
|
82
|
+
### 3. Create a Category
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
composter mkcat ui
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Push Your First Component
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
composter push ui "Fancy Button" ./src/components/FancyButton.tsx
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Scanning FancyButton.tsx and its dependencies...
|
|
96
|
+
📦 Bundled 3 file(s) and detected 2 external package(s).
|
|
97
|
+
✅ Success! Component 'Fancy Button' pushed to 'ui'.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Pull It In Another Project
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
composter pull ui "Fancy Button" ./src/components/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
⏳ Fetching 'Fancy Button' from 'ui'...
|
|
108
|
+
📦 Unpacking 3 file(s) into: /Users/you/new-project/src/components
|
|
109
|
+
+ FancyButton.tsx
|
|
110
|
+
+ FancyButton.module.css
|
|
111
|
+
+ useButtonAnimation.ts
|
|
112
|
+
|
|
113
|
+
⚠️ Missing Dependencies (Run this to fix):
|
|
114
|
+
npm install framer-motion clsx
|
|
115
|
+
|
|
116
|
+
✅ Component 'Fancy Button' pulled successfully!
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 📖 Commands Reference
|
|
122
|
+
|
|
123
|
+
### `composter login`
|
|
124
|
+
|
|
125
|
+
Authenticate with your Composter account.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
26
128
|
composter login
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**What happens:**
|
|
132
|
+
- Prompts for email and password
|
|
133
|
+
- Creates a JWT session token
|
|
134
|
+
- Stores session at `~/.config/composter/session.json`
|
|
135
|
+
- Session lasts 30 days
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `composter mkcat <category-name>`
|
|
140
|
+
|
|
141
|
+
Create a new category to organize your components.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
composter mkcat hooks
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Output:**
|
|
148
|
+
```
|
|
149
|
+
Category 'hooks' created successfully!
|
|
150
|
+
ID: clx1234567890
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Rules:**
|
|
154
|
+
| Rule | Example |
|
|
155
|
+
|------|---------|
|
|
156
|
+
| No spaces | ❌ `my buttons` → ✅ `buttons` |
|
|
157
|
+
| Max 10 characters | ❌ `superlongname` → ✅ `utils` |
|
|
158
|
+
| Unique per user | Each category name must be unique |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### `composter ls`
|
|
163
|
+
|
|
164
|
+
List all your categories.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
composter ls
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Output:**
|
|
171
|
+
```
|
|
172
|
+
ui hooks utils layouts forms
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `composter push <category> <title> <file-path>`
|
|
178
|
+
|
|
179
|
+
Push a component to your vault.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
composter push ui "Data Table" ./src/components/DataTable/index.tsx
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Arguments:**
|
|
186
|
+
|
|
187
|
+
| Argument | Description | Example |
|
|
188
|
+
|----------|-------------|---------|
|
|
189
|
+
| `category` | Target category name | `ui` |
|
|
190
|
+
| `title` | Human-readable component name | `"Data Table"` |
|
|
191
|
+
| `file-path` | Entry file path | `./src/components/DataTable.tsx` |
|
|
192
|
+
|
|
193
|
+
**What gets bundled:**
|
|
194
|
+
```
|
|
195
|
+
Your Entry File
|
|
196
|
+
│
|
|
197
|
+
├── ./relative/imports → ✅ Bundled into package
|
|
198
|
+
├── @/alias/imports → ✅ Bundled (assumes @/ = src/)
|
|
199
|
+
├── ./styles.css → ✅ Bundled
|
|
200
|
+
│
|
|
201
|
+
└── External packages → 📦 Tracked as dependencies
|
|
202
|
+
├── react
|
|
203
|
+
├── framer-motion
|
|
204
|
+
└── @tanstack/react-table
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Example Output:**
|
|
208
|
+
```
|
|
209
|
+
Scanning DataTable.tsx and its dependencies...
|
|
210
|
+
📦 Bundled 5 file(s) and detected 4 external package(s).
|
|
211
|
+
✅ Success! Component 'Data Table' pushed to 'ui'.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### `composter pull <category> <title> <target-directory>`
|
|
217
|
+
|
|
218
|
+
Pull a component from your vault.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
composter pull ui "Data Table" ./src/components/
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Arguments:**
|
|
225
|
+
|
|
226
|
+
| Argument | Description | Example |
|
|
227
|
+
|----------|-------------|---------|
|
|
228
|
+
| `category` | Source category name | `ui` |
|
|
229
|
+
| `title` | Component name to pull | `"Data Table"` |
|
|
230
|
+
| `target-directory` | Where to save files | `./src/components/` |
|
|
231
|
+
|
|
232
|
+
**What happens:**
|
|
233
|
+
1. Fetches component bundle from your vault
|
|
234
|
+
2. Recreates original folder structure
|
|
235
|
+
3. Writes all files to target directory
|
|
236
|
+
4. Checks your `package.json` for missing dependencies
|
|
237
|
+
5. Suggests `npm install` command if needed
|
|
238
|
+
|
|
239
|
+
**Example Output:**
|
|
240
|
+
```
|
|
241
|
+
⏳ Fetching 'Data Table' from 'ui'...
|
|
242
|
+
📦 Unpacking 5 file(s) into: /Users/you/project/src/components
|
|
243
|
+
+ DataTable/index.tsx
|
|
244
|
+
+ DataTable/DataTableHeader.tsx
|
|
245
|
+
+ DataTable/DataTableRow.tsx
|
|
246
|
+
+ DataTable/DataTablePagination.tsx
|
|
247
|
+
+ DataTable/data-table.css
|
|
248
|
+
|
|
249
|
+
⚠️ Missing Dependencies (Run this to fix):
|
|
250
|
+
npm install @tanstack/react-table lucide-react
|
|
251
|
+
|
|
252
|
+
✅ Component 'Data Table' pulled successfully!
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 🔧 How Smart Bundling Works
|
|
258
|
+
|
|
259
|
+
When you push a component, the CLI performs intelligent dependency crawling:
|
|
260
|
+
|
|
261
|
+
### Import Resolution
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Entry: src/components/Button.tsx
|
|
265
|
+
|
|
266
|
+
import { cn } from "@/lib/utils" // ✅ Bundled (alias import)
|
|
267
|
+
import { useToggle } from "./hooks/toggle" // ✅ Bundled (relative import)
|
|
268
|
+
import "./button.css" // ✅ Bundled (CSS import)
|
|
269
|
+
import { motion } from "framer-motion" // 📦 Tracked as npm dependency
|
|
270
|
+
import React from "react" // 📦 Tracked as npm dependency
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Alias Support
|
|
274
|
+
|
|
275
|
+
The `@/` alias is automatically resolved to your `src/` directory:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
@/components/Button → src/components/Button
|
|
279
|
+
@/lib/utils → src/lib/utils
|
|
280
|
+
@/hooks/useAuth → src/hooks/useAuth
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### File Extensions
|
|
284
|
+
|
|
285
|
+
The crawler automatically resolves these extensions:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
import { Button } from "./Button"
|
|
289
|
+
↓
|
|
290
|
+
Tries: Button.tsx → Button.ts → Button.jsx → Button.js → Button/index.tsx
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Package Version Detection
|
|
294
|
+
|
|
295
|
+
npm dependencies are tracked with versions from your `package.json`:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
// Your package.json
|
|
299
|
+
{
|
|
300
|
+
"dependencies": {
|
|
301
|
+
"framer-motion": "^10.16.0",
|
|
302
|
+
"clsx": "^2.0.0"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
27
305
|
|
|
28
|
-
|
|
29
|
-
|
|
306
|
+
// Stored with component
|
|
307
|
+
{
|
|
308
|
+
"framer-motion": "^10.16.0",
|
|
309
|
+
"clsx": "^2.0.0"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
30
312
|
|
|
31
|
-
|
|
32
|
-
composter push buttons "Animated Button" ./src/components/Button.tsx
|
|
313
|
+
---
|
|
33
314
|
|
|
34
|
-
|
|
35
|
-
|
|
315
|
+
## 🤖 AI Integration (MCP)
|
|
316
|
+
|
|
317
|
+
Composter includes a built-in Model Context Protocol (MCP) server that lets AI assistants interact with your vault directly.
|
|
318
|
+
|
|
319
|
+
### Quick Setup
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# One command to configure your AI assistant
|
|
323
|
+
npx composter-cli@latest mcp init --client claude
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Supported clients:**
|
|
327
|
+
- `claude` — Claude Desktop
|
|
328
|
+
- `cursor` — Cursor IDE
|
|
329
|
+
- `vscode` — VS Code with Copilot
|
|
330
|
+
- `windsurf` — Windsurf IDE
|
|
331
|
+
|
|
332
|
+
### MCP Commands
|
|
36
333
|
|
|
37
334
|
| Command | Description |
|
|
38
335
|
|---------|-------------|
|
|
39
|
-
| `composter
|
|
40
|
-
| `composter
|
|
41
|
-
| `composter
|
|
42
|
-
|
|
43
|
-
|
|
336
|
+
| `composter mcp init --client <name>` | Configure MCP for an AI assistant |
|
|
337
|
+
| `composter mcp serve` | Start MCP server directly (testing) |
|
|
338
|
+
| `composter mcp info` | Show MCP configuration info |
|
|
339
|
+
|
|
340
|
+
### Options
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Configure for Claude Desktop
|
|
344
|
+
composter mcp init --client claude
|
|
345
|
+
|
|
346
|
+
# Configure for Cursor
|
|
347
|
+
composter mcp init --client cursor
|
|
348
|
+
|
|
349
|
+
# Use localhost backend (development)
|
|
350
|
+
composter mcp init --client claude --dev
|
|
351
|
+
|
|
352
|
+
# Show manual config instructions
|
|
353
|
+
composter mcp info
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Available AI Tools
|
|
357
|
+
|
|
358
|
+
Once configured, your AI assistant can use these tools:
|
|
359
|
+
|
|
360
|
+
| Tool | Description |
|
|
361
|
+
|------|-------------|
|
|
362
|
+
| `search_components` | Search your vault by name or category |
|
|
363
|
+
| `list_categories` | List all your categories |
|
|
364
|
+
| `list_components` | List components in a category |
|
|
365
|
+
| `read_component` | Read full source code of a component |
|
|
366
|
+
|
|
367
|
+
### Example AI Prompts
|
|
368
|
+
|
|
369
|
+
After setup, you can ask your AI:
|
|
370
|
+
- *"Search my Composter vault for button components"*
|
|
371
|
+
- *"What categories do I have in Composter?"*
|
|
372
|
+
- *"Read the DataTable component from my ui category"*
|
|
373
|
+
- *"List all components in my hooks category"*
|
|
374
|
+
|
|
375
|
+
### Development Mode
|
|
376
|
+
|
|
377
|
+
For local development with `localhost:3000` backend:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# Initialize with dev flag
|
|
381
|
+
composter mcp init --client claude --dev
|
|
382
|
+
|
|
383
|
+
# Or set environment variable
|
|
384
|
+
COMPOSTER_DEV=true composter mcp serve
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 📂 File Locations
|
|
390
|
+
|
|
391
|
+
| File | Location | Purpose |
|
|
392
|
+
|------|----------|---------|
|
|
393
|
+
| Session | `~/.config/composter/session.json` | JWT auth token |
|
|
394
|
+
| Config Dir | `~/.config/composter/` | All CLI data |
|
|
395
|
+
|
|
396
|
+
### Session File Structure
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"jwt": "eyJhbGciOiJSUzI1NiIs...",
|
|
401
|
+
"cookies": "session_token=...",
|
|
402
|
+
"createdAt": "2024-12-10T10:00:00.000Z",
|
|
403
|
+
"expiresAt": "2025-01-09T10:00:00.000Z"
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 🐛 Troubleshooting
|
|
410
|
+
|
|
411
|
+
### "You must be logged in"
|
|
412
|
+
|
|
413
|
+
Your session doesn't exist or has expired.
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
composter login
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### "Session expired"
|
|
420
|
+
|
|
421
|
+
Sessions last 30 days. Re-authenticate:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
composter login
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### "Invalid category name"
|
|
428
|
+
|
|
429
|
+
Category names have strict rules:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# ❌ These will fail
|
|
433
|
+
composter mkcat "My Components" # No spaces
|
|
434
|
+
composter mkcat verylongcatname # Max 10 chars
|
|
435
|
+
|
|
436
|
+
# ✅ These work
|
|
437
|
+
composter mkcat ui
|
|
438
|
+
composter mkcat hooks
|
|
439
|
+
composter mkcat utils
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### "File not found" when pushing
|
|
443
|
+
|
|
444
|
+
Check your file path:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
# Make sure you're in the right directory
|
|
448
|
+
pwd
|
|
449
|
+
|
|
450
|
+
# Use relative path from current directory
|
|
451
|
+
composter push ui "Button" ./src/components/Button.tsx
|
|
452
|
+
|
|
453
|
+
# Or use absolute path
|
|
454
|
+
composter push ui "Button" /Users/me/project/src/components/Button.tsx
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### "Component not found" when pulling
|
|
458
|
+
|
|
459
|
+
- Check the exact component title (case-sensitive)
|
|
460
|
+
- Verify the category name
|
|
461
|
+
- List your components on the web dashboard
|
|
462
|
+
|
|
463
|
+
### Missing dependencies after pull
|
|
464
|
+
|
|
465
|
+
The CLI tells you what's missing. Just run:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
npm install package1 package2 package3
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Network errors
|
|
472
|
+
|
|
473
|
+
- Check your internet connection
|
|
474
|
+
- Verify the backend is accessible: `https://composter.onrender.com/api/health`
|
|
475
|
+
- Try logging in again
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 🌐 Web Dashboard
|
|
480
|
+
|
|
481
|
+
Manage your components visually at **[composter.vercel.app](https://composter.vercel.app)**
|
|
482
|
+
|
|
483
|
+
- 📋 Browse all components
|
|
484
|
+
- 👁️ Live code preview with Sandpack
|
|
485
|
+
- ✏️ Edit component metadata
|
|
486
|
+
- 📊 View dependency graphs
|
|
487
|
+
- 📋 Copy code snippets
|
|
488
|
+
- 🗑️ Delete components
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 🔒 Security
|
|
493
|
+
|
|
494
|
+
| Feature | Description |
|
|
495
|
+
|---------|-------------|
|
|
496
|
+
| 🔐 JWT Auth | Secure token-based authentication |
|
|
497
|
+
| 🏠 Local Storage | Tokens stored locally, never shared |
|
|
498
|
+
| 🔒 User Isolation | Each vault is completely private |
|
|
499
|
+
| 🌐 HTTPS | All API traffic encrypted |
|
|
500
|
+
| ⏰ Expiring Sessions | 30-day token lifetime |
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 📋 Examples
|
|
505
|
+
|
|
506
|
+
### Push a Shadcn-style Component
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
# Push a button with all its dependencies
|
|
510
|
+
composter push ui "Shadcn Button" ./src/components/ui/button.tsx
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Push a Custom Hook
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
composter push hooks "useLocalStorage" ./src/hooks/useLocalStorage.ts
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Push a Full Feature
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
# Push a data table that imports multiple files
|
|
523
|
+
composter push features "User Table" ./src/features/users/UserTable.tsx
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Pull Into a New Project
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# Create components folder if needed
|
|
530
|
+
mkdir -p src/components
|
|
531
|
+
|
|
532
|
+
# Pull your button
|
|
533
|
+
composter pull ui "Shadcn Button" ./src/components/ui/
|
|
534
|
+
|
|
535
|
+
# Install any missing deps
|
|
536
|
+
npm install
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 🔗 Links
|
|
542
|
+
|
|
543
|
+
| Resource | URL |
|
|
544
|
+
|----------|-----|
|
|
545
|
+
| 🌐 Web App | [composter.vercel.app](https://composter.vercel.app) |
|
|
546
|
+
| 📦 npm | [npmjs.com/package/composter-cli](https://www.npmjs.com/package/composter-cli) |
|
|
547
|
+
| 💻 GitHub | [github.com/binit2-1/Composter](https://github.com/binit2-1/Composter) |
|
|
548
|
+
| 🐛 Issues | [Report a bug](https://github.com/binit2-1/Composter/issues) |
|
|
549
|
+
| 🤖 MCP Docs | [MCP Setup Guide](https://github.com/binit2-1/Composter/tree/main/mcp) |
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 🤝 Contributing
|
|
554
|
+
|
|
555
|
+
Contributions welcome! See [CONTRIBUTING.md](https://github.com/binit2-1/Composter/blob/main/CONTRIBUTING.md)
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## 📄 License
|
|
560
|
+
|
|
561
|
+
MIT © [binit2-1](https://github.com/binit2-1)
|
|
44
562
|
|
|
45
|
-
|
|
563
|
+
---
|
|
46
564
|
|
|
47
|
-
|
|
565
|
+
<p align="center">
|
|
566
|
+
<b>Built with ❤️ for developers who hate copy-pasting components</b>
|
|
567
|
+
</p>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "composter-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Your personal vault for React components. Push, pull, and sync reusable components across projects — like shadcn/ui but for YOUR code.",
|
|
5
|
+
"description": "Your personal vault for React components. Push, pull, and sync reusable components across projects — like shadcn/ui but for YOUR code. Includes MCP server for AI assistants.",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"composter": "./bin/composter.js"
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
"developer-tools",
|
|
23
23
|
"productivity",
|
|
24
24
|
"mcp",
|
|
25
|
-
"ai-tools"
|
|
25
|
+
"ai-tools",
|
|
26
|
+
"claude",
|
|
27
|
+
"cursor",
|
|
28
|
+
"copilot",
|
|
29
|
+
"model-context-protocol"
|
|
26
30
|
],
|
|
27
31
|
"author": "binit2-1",
|
|
28
32
|
"license": "MIT",
|
|
@@ -38,9 +42,11 @@
|
|
|
38
42
|
"node": ">=18.0.0"
|
|
39
43
|
},
|
|
40
44
|
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
41
46
|
"commander": "^14.0.2",
|
|
42
47
|
"dotenv": "^16.4.5",
|
|
43
48
|
"inquirer": "^13.0.1",
|
|
44
|
-
"node-fetch": "^3.3.2"
|
|
49
|
+
"node-fetch": "^3.3.2",
|
|
50
|
+
"zod": "^3.23.0"
|
|
45
51
|
}
|
|
46
52
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { loadSession } from "../utils/session.js";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Get the path to the MCP server script
|
|
11
|
+
function getMcpServerPath() {
|
|
12
|
+
return path.resolve(__dirname, "../mcp/server.js");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Client configuration templates
|
|
16
|
+
const CLIENT_CONFIGS = {
|
|
17
|
+
claude: {
|
|
18
|
+
name: "Claude Desktop",
|
|
19
|
+
configPath: () => {
|
|
20
|
+
if (process.platform === "darwin") {
|
|
21
|
+
return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
22
|
+
} else if (process.platform === "win32") {
|
|
23
|
+
return path.join(process.env.APPDATA, "Claude", "claude_desktop_config.json");
|
|
24
|
+
} else {
|
|
25
|
+
return path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json");
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
generateConfig: (serverPath, cwd) => ({
|
|
29
|
+
mcpServers: {
|
|
30
|
+
composter: {
|
|
31
|
+
command: "node",
|
|
32
|
+
args: [serverPath],
|
|
33
|
+
cwd: cwd,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
cursor: {
|
|
39
|
+
name: "Cursor",
|
|
40
|
+
configPath: () => {
|
|
41
|
+
return path.join(process.cwd(), ".cursor", "mcp.json");
|
|
42
|
+
},
|
|
43
|
+
generateConfig: (serverPath, cwd) => ({
|
|
44
|
+
composter: {
|
|
45
|
+
command: "node",
|
|
46
|
+
args: [serverPath],
|
|
47
|
+
cwd: cwd,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
vscode: {
|
|
52
|
+
name: "VS Code (Copilot)",
|
|
53
|
+
configPath: () => {
|
|
54
|
+
return path.join(process.cwd(), ".vscode", "mcp.json");
|
|
55
|
+
},
|
|
56
|
+
generateConfig: (serverPath, cwd) => ({
|
|
57
|
+
servers: {
|
|
58
|
+
composter: {
|
|
59
|
+
command: "node",
|
|
60
|
+
args: [serverPath],
|
|
61
|
+
cwd: cwd,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
windsurf: {
|
|
67
|
+
name: "Windsurf",
|
|
68
|
+
configPath: () => {
|
|
69
|
+
return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
70
|
+
},
|
|
71
|
+
generateConfig: (serverPath, cwd) => ({
|
|
72
|
+
mcpServers: {
|
|
73
|
+
composter: {
|
|
74
|
+
command: "node",
|
|
75
|
+
args: [serverPath],
|
|
76
|
+
cwd: cwd,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Initialize MCP for a specific client
|
|
84
|
+
export async function mcpInit(client, options = {}) {
|
|
85
|
+
// Validate client
|
|
86
|
+
const clientConfig = CLIENT_CONFIGS[client?.toLowerCase()];
|
|
87
|
+
if (!clientConfig) {
|
|
88
|
+
console.log(`❌ Unknown client: ${client}`);
|
|
89
|
+
console.log(`\nSupported clients:`);
|
|
90
|
+
Object.entries(CLIENT_CONFIGS).forEach(([key, val]) => {
|
|
91
|
+
console.log(` - ${key} (${val.name})`);
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if user is logged in
|
|
97
|
+
const session = loadSession();
|
|
98
|
+
if (!session?.jwt) {
|
|
99
|
+
console.log("❌ You must be logged in first. Run: composter login");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const serverPath = getMcpServerPath();
|
|
104
|
+
const cwd = options.global ? path.dirname(serverPath) : process.cwd();
|
|
105
|
+
const configPath = clientConfig.configPath();
|
|
106
|
+
const configDir = path.dirname(configPath);
|
|
107
|
+
|
|
108
|
+
console.log(`\n🔧 Setting up Composter MCP for ${clientConfig.name}...\n`);
|
|
109
|
+
|
|
110
|
+
// Generate the config snippet
|
|
111
|
+
const newConfig = clientConfig.generateConfig(serverPath, cwd);
|
|
112
|
+
|
|
113
|
+
// Check if config file exists
|
|
114
|
+
let existingConfig = {};
|
|
115
|
+
if (fs.existsSync(configPath)) {
|
|
116
|
+
try {
|
|
117
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
118
|
+
console.log(`📄 Found existing config at: ${configPath}`);
|
|
119
|
+
} catch {
|
|
120
|
+
console.log(`⚠️ Existing config is invalid, will create new one.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Merge configs
|
|
125
|
+
let mergedConfig;
|
|
126
|
+
if (client === "claude" || client === "windsurf") {
|
|
127
|
+
mergedConfig = {
|
|
128
|
+
...existingConfig,
|
|
129
|
+
mcpServers: {
|
|
130
|
+
...existingConfig.mcpServers,
|
|
131
|
+
...newConfig.mcpServers,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
} else if (client === "vscode") {
|
|
135
|
+
mergedConfig = {
|
|
136
|
+
...existingConfig,
|
|
137
|
+
servers: {
|
|
138
|
+
...existingConfig.servers,
|
|
139
|
+
...newConfig.servers,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
// Cursor - direct merge
|
|
144
|
+
mergedConfig = {
|
|
145
|
+
...existingConfig,
|
|
146
|
+
...newConfig,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create directory if needed
|
|
151
|
+
if (!fs.existsSync(configDir)) {
|
|
152
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
153
|
+
console.log(`📁 Created directory: ${configDir}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Write config
|
|
157
|
+
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
|
|
158
|
+
console.log(`✅ Config written to: ${configPath}`);
|
|
159
|
+
|
|
160
|
+
// Print success message
|
|
161
|
+
console.log(`
|
|
162
|
+
╔════════════════════════════════════════════════════════════════╗
|
|
163
|
+
║ 🎉 MCP Setup Complete! ║
|
|
164
|
+
╠════════════════════════════════════════════════════════════════╣
|
|
165
|
+
║ ║
|
|
166
|
+
║ Composter MCP server has been configured for ${clientConfig.name.padEnd(14)} ║
|
|
167
|
+
║ ║
|
|
168
|
+
║ Next steps: ║
|
|
169
|
+
║ 1. Restart ${clientConfig.name.padEnd(50)} ║
|
|
170
|
+
║ 2. Look for "Composter" in your MCP tools ║
|
|
171
|
+
║ ║
|
|
172
|
+
║ Available tools: ║
|
|
173
|
+
║ • search_components - Search your component vault ║
|
|
174
|
+
║ • list_categories - List all your categories ║
|
|
175
|
+
║ • list_components - List components in a category ║
|
|
176
|
+
║ • read_component - Read full source code ║
|
|
177
|
+
║ ║
|
|
178
|
+
╚════════════════════════════════════════════════════════════════╝
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
// Dev mode hint
|
|
182
|
+
if (options.dev) {
|
|
183
|
+
console.log(`\n💡 Dev mode: Set COMPOSTER_DEV=true to use localhost:3000`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Serve MCP server directly (for testing)
|
|
188
|
+
export async function mcpServe(options = {}) {
|
|
189
|
+
const session = loadSession();
|
|
190
|
+
if (!session?.jwt) {
|
|
191
|
+
console.log("❌ You must be logged in first. Run: composter login");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log("🚀 Starting Composter MCP server...");
|
|
196
|
+
console.log(" Press Ctrl+C to stop\n");
|
|
197
|
+
|
|
198
|
+
// Set dev mode if requested
|
|
199
|
+
if (options.dev) {
|
|
200
|
+
process.env.COMPOSTER_DEV = "true";
|
|
201
|
+
console.log("📡 Dev mode: Using localhost:3000");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Dynamic import and run the server
|
|
205
|
+
const serverPath = getMcpServerPath();
|
|
206
|
+
await import(serverPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show MCP configuration info
|
|
210
|
+
export function mcpInfo() {
|
|
211
|
+
const serverPath = getMcpServerPath();
|
|
212
|
+
const session = loadSession();
|
|
213
|
+
|
|
214
|
+
console.log(`
|
|
215
|
+
╔════════════════════════════════════════════════════════════════╗
|
|
216
|
+
║ Composter MCP Information ║
|
|
217
|
+
╠════════════════════════════════════════════════════════════════╣
|
|
218
|
+
|
|
219
|
+
Server path: ${serverPath}
|
|
220
|
+
Logged in: ${session?.jwt ? "✅ Yes" : "❌ No (run 'composter login')"}
|
|
221
|
+
|
|
222
|
+
Manual configuration:
|
|
223
|
+
|
|
224
|
+
{
|
|
225
|
+
"composter": {
|
|
226
|
+
"command": "node",
|
|
227
|
+
"args": ["${serverPath}"]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Environment variables:
|
|
232
|
+
• COMPOSTER_DEV=true - Use localhost:3000 backend
|
|
233
|
+
• COMPOSTER_API_URL=... - Custom API URL
|
|
234
|
+
|
|
235
|
+
╚════════════════════════════════════════════════════════════════╝
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mkcat } from "./commands/mkcat.js";
|
|
|
6
6
|
import { listCategories } from "./commands/listCat.js";
|
|
7
7
|
import { pushComponent } from "./commands/push.js";
|
|
8
8
|
import { pullComponent } from "./commands/pull.js";
|
|
9
|
+
import { mcpInit, mcpServe, mcpInfo } from "./commands/mcp.js";
|
|
9
10
|
import { createRequire } from "module";
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
@@ -49,4 +50,34 @@ program
|
|
|
49
50
|
pullComponent(category, title, filepath);
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
// MCP Commands
|
|
54
|
+
const mcp = program
|
|
55
|
+
.command("mcp")
|
|
56
|
+
.description("Manage MCP (Model Context Protocol) server for AI assistants");
|
|
57
|
+
|
|
58
|
+
mcp
|
|
59
|
+
.command("init")
|
|
60
|
+
.description("Initialize MCP server for an AI assistant")
|
|
61
|
+
.option("-c, --client <client>", "AI client to configure (claude, cursor, vscode, windsurf)", "claude")
|
|
62
|
+
.option("-g, --global", "Configure globally instead of project-specific")
|
|
63
|
+
.option("-d, --dev", "Use localhost:3000 backend (development mode)")
|
|
64
|
+
.action((options) => {
|
|
65
|
+
mcpInit(options.client, options);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
mcp
|
|
69
|
+
.command("serve")
|
|
70
|
+
.description("Start the MCP server directly (for testing)")
|
|
71
|
+
.option("-d, --dev", "Use localhost:3000 backend (development mode)")
|
|
72
|
+
.action((options) => {
|
|
73
|
+
mcpServe(options);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
mcp
|
|
77
|
+
.command("info")
|
|
78
|
+
.description("Show MCP configuration information")
|
|
79
|
+
.action(() => {
|
|
80
|
+
mcpInfo();
|
|
81
|
+
});
|
|
82
|
+
|
|
52
83
|
program.parse(process.argv);
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadSession } from "../utils/session.js";
|
|
6
|
+
import fetch from "node-fetch";
|
|
7
|
+
|
|
8
|
+
// Redirect console.log to stderr (MCP uses stdout for protocol)
|
|
9
|
+
const originalLog = console.log;
|
|
10
|
+
console.log = (...args) => console.error(...args);
|
|
11
|
+
|
|
12
|
+
// Determine base URL - supports both dev and production
|
|
13
|
+
function getBaseUrl() {
|
|
14
|
+
// Check for explicit env var first
|
|
15
|
+
if (process.env.COMPOSTER_API_URL) {
|
|
16
|
+
return process.env.COMPOSTER_API_URL;
|
|
17
|
+
}
|
|
18
|
+
// Check for dev mode
|
|
19
|
+
if (process.env.COMPOSTER_DEV === "true" || process.env.NODE_ENV === "development") {
|
|
20
|
+
return "http://localhost:3000/api";
|
|
21
|
+
}
|
|
22
|
+
// Default to production
|
|
23
|
+
return "https://composter.onrender.com/api";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BASE_URL = getBaseUrl();
|
|
27
|
+
|
|
28
|
+
// API request helper with JWT auth
|
|
29
|
+
async function apiRequest(path, options = {}) {
|
|
30
|
+
const session = loadSession();
|
|
31
|
+
|
|
32
|
+
if (!session?.jwt) {
|
|
33
|
+
throw new Error("Not authenticated. Run 'composter login' first.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const headers = {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${session.jwt}`,
|
|
39
|
+
...options.headers,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
43
|
+
...options,
|
|
44
|
+
headers,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (res.status === 401) {
|
|
48
|
+
throw new Error("Session expired. Run 'composter login' again.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return res;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create the MCP server with tools
|
|
55
|
+
function createComposterMcpServer() {
|
|
56
|
+
const server = new McpServer({
|
|
57
|
+
name: "Composter",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Tool: Search components
|
|
62
|
+
server.tool(
|
|
63
|
+
"search_components",
|
|
64
|
+
"Search for React components in your Composter vault by title or category name. Returns matching components with IDs and categories.",
|
|
65
|
+
{
|
|
66
|
+
query: z.string().describe("Search term for component title or category name"),
|
|
67
|
+
},
|
|
68
|
+
async ({ query }) => {
|
|
69
|
+
try {
|
|
70
|
+
const res = await apiRequest(`/components/search?q=${encodeURIComponent(query)}`, { method: "GET" });
|
|
71
|
+
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const error = await res.json().catch(() => ({}));
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: `Error searching: ${error.message || res.statusText}` }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const components = data.components || [];
|
|
81
|
+
|
|
82
|
+
if (components.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: "No components found matching that query." }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const formatted = components.map((c) =>
|
|
89
|
+
`- **${c.title}** (Category: ${c.category?.name || "unknown"}) [ID: ${c.id}]`
|
|
90
|
+
).join("\n");
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `Found ${components.length} component(s):\n\n${formatted}` }],
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Tool: List categories
|
|
104
|
+
server.tool(
|
|
105
|
+
"list_categories",
|
|
106
|
+
"List all categories in your Composter vault.",
|
|
107
|
+
{},
|
|
108
|
+
async () => {
|
|
109
|
+
try {
|
|
110
|
+
const res = await apiRequest("/categories");
|
|
111
|
+
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const error = await res.json().catch(() => ({}));
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: `Error: ${error.message || res.statusText}` }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
const categories = data.categories || [];
|
|
121
|
+
|
|
122
|
+
if (categories.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: "No categories found. Create one with 'composter mkcat <name>'." }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const formatted = categories.map((c) => `- ${c.name}`).join("\n");
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: `Your categories:\n\n${formatted}` }],
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Tool: Read component
|
|
142
|
+
server.tool(
|
|
143
|
+
"read_component",
|
|
144
|
+
"Read the full source code of a React component from your vault. Returns the code, category, dependencies, and creation date.",
|
|
145
|
+
{
|
|
146
|
+
category: z.string().describe("The category name the component belongs to"),
|
|
147
|
+
title: z.string().describe("The title/name of the component to read"),
|
|
148
|
+
},
|
|
149
|
+
async ({ category, title }) => {
|
|
150
|
+
try {
|
|
151
|
+
const res = await apiRequest(
|
|
152
|
+
`/components?category=${encodeURIComponent(category)}&title=${encodeURIComponent(title)}`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (res.status === 404) {
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: `Component "${title}" not found in category "${category}".` }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const error = await res.json().catch(() => ({}));
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Error: ${error.message || res.statusText}` }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
const component = data.component;
|
|
170
|
+
|
|
171
|
+
if (!component) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: `Component "${title}" not found.` }],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Parse code - could be JSON (multi-file) or string (single file)
|
|
178
|
+
let codeOutput = "";
|
|
179
|
+
try {
|
|
180
|
+
const files = JSON.parse(component.code);
|
|
181
|
+
codeOutput = Object.entries(files)
|
|
182
|
+
.map(([path, content]) => `### ${path}\n\`\`\`tsx\n${content}\n\`\`\``)
|
|
183
|
+
.join("\n\n");
|
|
184
|
+
} catch {
|
|
185
|
+
codeOutput = `\`\`\`tsx\n${component.code}\n\`\`\``;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Format dependencies
|
|
189
|
+
let depsOutput = "";
|
|
190
|
+
if (component.dependencies && Object.keys(component.dependencies).length > 0) {
|
|
191
|
+
const deps = Object.entries(component.dependencies)
|
|
192
|
+
.map(([pkg, ver]) => `- ${pkg}: ${ver}`)
|
|
193
|
+
.join("\n");
|
|
194
|
+
depsOutput = `\n\n**Dependencies:**\n${deps}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const output = `# ${component.title}
|
|
198
|
+
|
|
199
|
+
**Category:** ${category}
|
|
200
|
+
**Created:** ${new Date(component.createdAt).toLocaleDateString()}
|
|
201
|
+
${depsOutput}
|
|
202
|
+
|
|
203
|
+
## Source Code
|
|
204
|
+
|
|
205
|
+
${codeOutput}`;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: output }],
|
|
209
|
+
};
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Tool: List components in category
|
|
219
|
+
server.tool(
|
|
220
|
+
"list_components",
|
|
221
|
+
"List all components in a specific category.",
|
|
222
|
+
{
|
|
223
|
+
category: z.string().describe("The category name to list components from"),
|
|
224
|
+
},
|
|
225
|
+
async ({ category }) => {
|
|
226
|
+
try {
|
|
227
|
+
const res = await apiRequest(`/components/list-by-category?category=${encodeURIComponent(category)}`, { method: "GET" });
|
|
228
|
+
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
const error = await res.json().catch(() => ({}));
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `Error: ${error.message || res.statusText}` }],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
const components = data.components || [];
|
|
238
|
+
|
|
239
|
+
if (components.length === 0) {
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: "text", text: `No components found in category "${category}".` }],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const formatted = components.map((c) =>
|
|
246
|
+
`- **${c.title}** (created: ${new Date(c.createdAt).toLocaleDateString()})`
|
|
247
|
+
).join("\n");
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text", text: `Components in "${category}":\n\n${formatted}` }],
|
|
251
|
+
};
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return server;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Main entry point
|
|
264
|
+
async function main() {
|
|
265
|
+
try {
|
|
266
|
+
// Verify session exists
|
|
267
|
+
const session = loadSession();
|
|
268
|
+
if (!session?.jwt) {
|
|
269
|
+
console.error("❌ No session found. Please run 'composter login' first.");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.error(`🚀 Composter MCP Server starting...`);
|
|
274
|
+
console.error(`📡 API: ${BASE_URL}`);
|
|
275
|
+
|
|
276
|
+
const server = createComposterMcpServer();
|
|
277
|
+
const transport = new StdioServerTransport();
|
|
278
|
+
await server.connect(transport);
|
|
279
|
+
|
|
280
|
+
console.error("✅ Composter MCP server running on stdio");
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error("❌ Fatal Error:", error.message);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Run if called directly
|
|
288
|
+
main();
|
|
289
|
+
|