composter-cli 1.0.10 → 1.0.11

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.
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ // Session path (same as CLI)
6
+ const SESSION_PATH = path.join(os.homedir(), ".config", "composter", "session.json");
7
+
8
+ // Get base URL - supports both dev and production
9
+ export function getBaseUrl() {
10
+ // Check for explicit env var first
11
+ if (process.env.COMPOSTER_API_URL) {
12
+ return process.env.COMPOSTER_API_URL;
13
+ }
14
+ // Check for dev mode
15
+ if (process.env.COMPOSTER_DEV === "true" || process.env.NODE_ENV === "development") {
16
+ return "http://localhost:3000/api";
17
+ }
18
+ // Default to production
19
+ return "https://composter.onrender.com/api";
20
+ }
21
+
22
+ // Load session from CLI's session file
23
+ export function loadSession() {
24
+ if (!fs.existsSync(SESSION_PATH)) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ const raw = fs.readFileSync(SESSION_PATH, "utf-8");
30
+ const session = JSON.parse(raw);
31
+
32
+ // Check if session is expired
33
+ if (session.expiresAt && new Date(session.expiresAt) < new Date()) {
34
+ return null;
35
+ }
36
+
37
+ return session;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ // Get JWT token from session
44
+ export function getAuthToken() {
45
+ const session = loadSession();
46
+ if (!session) {
47
+ throw new Error("No session found. Please run 'composter login' first.");
48
+ }
49
+
50
+ const token = session.jwt || session.token || session.accessToken;
51
+ if (!token) {
52
+ throw new Error("Session file missing token. Please run 'composter login' again.");
53
+ }
54
+
55
+ return token;
56
+ }
57
+
58
+ // Verify session is valid by making a test API call
59
+ export async function verifySession() {
60
+ const token = getAuthToken();
61
+ const baseUrl = getBaseUrl();
62
+
63
+ const res = await fetch(`${baseUrl.replace('/api', '')}/api/me`, {
64
+ headers: {
65
+ "Authorization": `Bearer ${token}`,
66
+ },
67
+ });
68
+
69
+ if (!res.ok) {
70
+ if (res.status === 401) {
71
+ throw new Error("Session expired. Please run 'composter login' again.");
72
+ }
73
+ throw new Error(`Authentication failed: ${res.statusText}`);
74
+ }
75
+
76
+ const data = await res.json();
77
+ return data?.user?.id || data?.session?.userId;
78
+ }
@@ -0,0 +1,389 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getAuthToken, getBaseUrl } from "./auth.js";
4
+
5
+ // API request helper with JWT auth
6
+ async function apiRequest(path, options = {}) {
7
+ const token = getAuthToken();
8
+ const baseUrl = getBaseUrl();
9
+
10
+ const headers = {
11
+ "Content-Type": "application/json",
12
+ "Authorization": `Bearer ${token}`,
13
+ ...options.headers,
14
+ };
15
+
16
+ const res = await fetch(`${baseUrl}${path}`, {
17
+ ...options,
18
+ headers,
19
+ });
20
+
21
+ return res;
22
+ }
23
+
24
+ // Shared helpers
25
+ async function fetchCategories() {
26
+ const res = await apiRequest("/categories", { method: "GET" });
27
+ if (!res.ok) {
28
+ const error = await res.json().catch(() => ({}));
29
+ throw new Error(error.message || error.error || res.statusText);
30
+ }
31
+ const data = await res.json();
32
+ return data.categories || [];
33
+ }
34
+
35
+ async function fetchComponentsByCategory(category) {
36
+ const res = await apiRequest(`/components/list-by-category?category=${encodeURIComponent(category)}`, { method: "GET" });
37
+ if (res.status === 404) throw new Error(`Category "${category}" not found.`);
38
+ if (!res.ok) {
39
+ const error = await res.json().catch(() => ({}));
40
+ throw new Error(error.message || error.error || res.statusText);
41
+ }
42
+ const data = await res.json();
43
+ return data.components || [];
44
+ }
45
+
46
+ async function fetchComponent(category, title) {
47
+ const res = await apiRequest(
48
+ `/components?category=${encodeURIComponent(category)}&title=${encodeURIComponent(title)}`,
49
+ { method: "GET" }
50
+ );
51
+ if (res.status === 404) throw new Error(`Component "${title}" not found in category "${category}".`);
52
+ if (!res.ok) {
53
+ const error = await res.json().catch(() => ({}));
54
+ throw new Error(error.message || error.error || res.statusText);
55
+ }
56
+ const data = await res.json();
57
+ return data.component;
58
+ }
59
+
60
+ async function searchComponents(query) {
61
+ const res = await apiRequest(`/components/search?q=${encodeURIComponent(query)}`, { method: "GET" });
62
+ if (!res.ok) {
63
+ const error = await res.json().catch(() => ({}));
64
+ throw new Error(error.message || error.error || res.statusText);
65
+ }
66
+ const data = await res.json();
67
+ return data.components || [];
68
+ }
69
+
70
+ function renderComponent(category, component) {
71
+ if (!component) return "Component not found.";
72
+
73
+ let codeOutput = "";
74
+ try {
75
+ const files = JSON.parse(component.code);
76
+ codeOutput = Object.entries(files)
77
+ .map(([path, content]) => `### ${path}\n\`\`\`tsx\n${content}\n\`\`\``)
78
+ .join("\n\n");
79
+ } catch {
80
+ codeOutput = `\`\`\`tsx\n${component.code}\n\`\`\``;
81
+ }
82
+
83
+ let depsOutput = "";
84
+ if (component.dependencies && Object.keys(component.dependencies).length > 0) {
85
+ const deps = Object.entries(component.dependencies)
86
+ .map(([pkg, ver]) => `- ${pkg}: ${ver}`)
87
+ .join("\n");
88
+ depsOutput = `\n\n**Dependencies:**\n${deps}`;
89
+ }
90
+
91
+ return `# ${component.title}
92
+
93
+ **Category:** ${category}
94
+ **Created:** ${new Date(component.createdAt).toLocaleDateString()}${depsOutput}
95
+
96
+ ## Source Code
97
+
98
+ ${codeOutput}`;
99
+ }
100
+
101
+ export function createMcpServer() {
102
+ const server = new McpServer({
103
+ name: "Composter",
104
+ version: "1.0.0",
105
+ });
106
+
107
+ // Tool: Search components
108
+ server.tool(
109
+ "search_components",
110
+ "Search vault components by name or topic. Triggers on queries like 'find button components', 'search cards', 'look up forms'. Returns matches with IDs and categories.",
111
+ {
112
+ query: z.string().describe("Search term for component title or category name"),
113
+ },
114
+ async ({ query }) => {
115
+ try {
116
+ const res = await apiRequest(`/components/search?q=${encodeURIComponent(query)}`, { method: "GET" });
117
+
118
+ if (!res.ok) {
119
+ const error = await res.json().catch(() => ({}));
120
+ return {
121
+ content: [{ type: "text", text: `Error searching: ${error.message || error.error || res.statusText}` }],
122
+ };
123
+ }
124
+
125
+ const data = await res.json();
126
+ const components = data.components || [];
127
+
128
+ if (components.length === 0) {
129
+ return {
130
+ content: [{ type: "text", text: "No components found matching that query." }],
131
+ };
132
+ }
133
+
134
+ const formatted = components.map((c) =>
135
+ `- **${c.title}** (Category: ${c.category?.name || "unknown"}) [ID: ${c.id}]`
136
+ ).join("\n");
137
+
138
+ return {
139
+ content: [{ type: "text", text: `Found ${components.length} component(s):\n\n${formatted}` }],
140
+ };
141
+ } catch (err) {
142
+ return {
143
+ content: [{ type: "text", text: `Error: ${err.message}` }],
144
+ };
145
+ }
146
+ }
147
+ );
148
+
149
+ // Tool: Natural language helper (catch-all)
150
+ server.tool(
151
+ "ask_composter",
152
+ "Ask in plain English to list categories, show components in a category, search components, or read a component (e.g., 'list categories', 'show components in ui', 'read Simple Card from ui', 'find button components').",
153
+ {
154
+ query: z.string().describe("e.g. 'list categories', 'show components in ui', 'read Simple Card from ui', 'find button components'"),
155
+ },
156
+ async ({ query }) => {
157
+ const q = query.trim();
158
+ const qLower = q.toLowerCase();
159
+ const respond = (text) => ({ content: [{ type: "text", text }] });
160
+
161
+ try {
162
+ // List categories
163
+ if (/\b(list|show|what)\b.*\bcategories\b/.test(qLower)) {
164
+ const categories = await fetchCategories();
165
+ if (!categories.length) return respond("No categories found. Create one with 'composter mkcat <name>'.");
166
+ const formatted = categories.map((c) => `- ${c.name}`).join("\n");
167
+ return respond(`Your categories:\n\n${formatted}`);
168
+ }
169
+
170
+ // List components in a category
171
+ const listCatMatch = qLower.match(/(?:list|show|what).*(?:components|items).*(?:in|for)\s+([a-z0-9_-]+)/);
172
+ if (listCatMatch) {
173
+ const cat = listCatMatch[1];
174
+ const components = await fetchComponentsByCategory(cat);
175
+ if (!components.length) return respond(`No components found in category "${cat}".`);
176
+ const formatted = components
177
+ .map((c) => `- ${c.title} (created: ${new Date(c.createdAt).toLocaleDateString()})`)
178
+ .join("\n");
179
+ return respond(`Components in "${cat}":\n\n${formatted}`);
180
+ }
181
+
182
+ // Read component with optional category
183
+ const readMatch = q.match(/(?:read|show|get|open|fetch)\s+(.+?)(?:\s+from\s+([a-z0-9_-]+))?$/i);
184
+ if (readMatch) {
185
+ const titleRaw = readMatch[1].trim();
186
+ const categoryRaw = readMatch[2]?.trim();
187
+
188
+ if (!categoryRaw) {
189
+ const hits = await searchComponents(titleRaw);
190
+ if (!hits.length) return respond(`No components found matching "${titleRaw}".`);
191
+ if (hits.length > 1) {
192
+ const list = hits
193
+ .slice(0, 5)
194
+ .map((c) => `- ${c.title} (category: ${c.category?.name || "unknown"})`)
195
+ .join("\n");
196
+ return respond(
197
+ `Found multiple components matching "${titleRaw}". Please specify a category:\n\n${list}${
198
+ hits.length > 5 ? "\n(and more...)" : ""
199
+ }`
200
+ );
201
+ }
202
+ const hit = hits[0];
203
+ const comp = await fetchComponent(hit.category?.name, hit.title);
204
+ return respond(renderComponent(hit.category?.name, comp));
205
+ }
206
+
207
+ const comp = await fetchComponent(categoryRaw, titleRaw);
208
+ return respond(renderComponent(categoryRaw, comp));
209
+ }
210
+
211
+ // Fallback: search
212
+ const hits = await searchComponents(q);
213
+ if (!hits.length) return respond("No components found matching that query.");
214
+ const formatted = hits
215
+ .map((c) => `- **${c.title}** (Category: ${c.category?.name || "unknown"}) [ID: ${c.id}]`)
216
+ .join("\n");
217
+ return respond(`Found ${hits.length} component(s):\n\n${formatted}`);
218
+ } catch (err) {
219
+ return respond(`Error: ${err.message}`);
220
+ }
221
+ }
222
+ );
223
+
224
+ // Tool: List categories
225
+ server.tool(
226
+ "list_categories",
227
+ "List all categories in the vault. Trigger when user asks 'what categories do I have', 'show my categories', 'list vault categories'.",
228
+ {},
229
+ async () => {
230
+ try {
231
+ const res = await apiRequest("/categories", { method: "GET" });
232
+
233
+ if (!res.ok) {
234
+ const error = await res.json().catch(() => ({}));
235
+ return {
236
+ content: [{ type: "text", text: `Error: ${error.message || error.error || res.statusText}` }],
237
+ };
238
+ }
239
+
240
+ const data = await res.json();
241
+ const categories = data.categories || [];
242
+
243
+ if (categories.length === 0) {
244
+ return {
245
+ content: [{ type: "text", text: "No categories found. Create one with 'composter mkcat <name>'." }],
246
+ };
247
+ }
248
+
249
+ const formatted = categories.map((c) => `- ${c.name}`).join("\n");
250
+
251
+ return {
252
+ content: [{ type: "text", text: `Your categories:\n\n${formatted}` }],
253
+ };
254
+ } catch (err) {
255
+ return {
256
+ content: [{ type: "text", text: `Error: ${err.message}` }],
257
+ };
258
+ }
259
+ }
260
+ );
261
+
262
+ // Tool: List components in category
263
+ server.tool(
264
+ "list_components",
265
+ "List components inside a given category. Trigger on requests like 'show components in ui', 'what's in forms', 'list items in buttons'.",
266
+ {
267
+ category: z.string().describe("The category name to list components from"),
268
+ },
269
+ async ({ category }) => {
270
+ try {
271
+ const res = await apiRequest(`/components/list-by-category?category=${encodeURIComponent(category)}`, { method: "GET" });
272
+
273
+ if (res.status === 404) {
274
+ return {
275
+ content: [{ type: "text", text: `Category "${category}" not found.` }],
276
+ };
277
+ }
278
+
279
+ if (!res.ok) {
280
+ const error = await res.json().catch(() => ({}));
281
+ return {
282
+ content: [{ type: "text", text: `Error: ${error.message || error.error || res.statusText}` }],
283
+ };
284
+ }
285
+
286
+ const data = await res.json();
287
+ const components = data.components || [];
288
+
289
+ if (components.length === 0) {
290
+ return {
291
+ content: [{ type: "text", text: `No components found in category "${category}".` }],
292
+ };
293
+ }
294
+
295
+ const formatted = components.map((c) =>
296
+ `- **${c.title}** (created: ${new Date(c.createdAt).toLocaleDateString()})`
297
+ ).join("\n");
298
+
299
+ return {
300
+ content: [{ type: "text", text: `Components in "${category}":\n\n${formatted}` }],
301
+ };
302
+ } catch (err) {
303
+ return {
304
+ content: [{ type: "text", text: `Error: ${err.message}` }],
305
+ };
306
+ }
307
+ }
308
+ );
309
+
310
+ // Tool: Read component
311
+ server.tool(
312
+ "read_component",
313
+ "Read a component's full source. Trigger on 'read/open/show/get <component> from <category>' or similar. Returns code, category, dependencies, and creation date.",
314
+ {
315
+ category: z.string().describe("The category name the component belongs to"),
316
+ title: z.string().describe("The title/name of the component to read"),
317
+ },
318
+ async ({ category, title }) => {
319
+ try {
320
+ const res = await apiRequest(
321
+ `/components?category=${encodeURIComponent(category)}&title=${encodeURIComponent(title)}`,
322
+ { method: "GET" }
323
+ );
324
+
325
+ if (res.status === 404) {
326
+ return {
327
+ content: [{ type: "text", text: `Component "${title}" not found in category "${category}".` }],
328
+ };
329
+ }
330
+
331
+ if (!res.ok) {
332
+ const error = await res.json().catch(() => ({}));
333
+ return {
334
+ content: [{ type: "text", text: `Error: ${error.message || error.error || res.statusText}` }],
335
+ };
336
+ }
337
+
338
+ const data = await res.json();
339
+ const component = data.component;
340
+
341
+ if (!component) {
342
+ return {
343
+ content: [{ type: "text", text: `Component "${title}" not found.` }],
344
+ };
345
+ }
346
+
347
+ // Parse code - could be JSON (multi-file) or string (single file)
348
+ let codeOutput = "";
349
+ try {
350
+ const files = JSON.parse(component.code);
351
+ codeOutput = Object.entries(files)
352
+ .map(([path, content]) => `### ${path}\n\`\`\`tsx\n${content}\n\`\`\``)
353
+ .join("\n\n");
354
+ } catch {
355
+ codeOutput = `\`\`\`tsx\n${component.code}\n\`\`\``;
356
+ }
357
+
358
+ // Format dependencies
359
+ let depsOutput = "";
360
+ if (component.dependencies && Object.keys(component.dependencies).length > 0) {
361
+ const deps = Object.entries(component.dependencies)
362
+ .map(([pkg, ver]) => `- ${pkg}: ${ver}`)
363
+ .join("\n");
364
+ depsOutput = `\n\n**Dependencies:**\n${deps}`;
365
+ }
366
+
367
+ const output = `# ${component.title}
368
+
369
+ **Category:** ${category}
370
+ **Created:** ${new Date(component.createdAt).toLocaleDateString()}
371
+ ${depsOutput}
372
+
373
+ ## Source Code
374
+
375
+ ${codeOutput}`;
376
+
377
+ return {
378
+ content: [{ type: "text", text: output }],
379
+ };
380
+ } catch (err) {
381
+ return {
382
+ content: [{ type: "text", text: `Error: ${err.message}` }],
383
+ };
384
+ }
385
+ }
386
+ );
387
+
388
+ return server;
389
+ }
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+
6
+ // Client configuration templates
7
+ const CLIENT_CONFIGS = {
8
+ claude: {
9
+ name: "Claude Desktop",
10
+ configPath: () => {
11
+ if (process.platform === "darwin") {
12
+ return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
13
+ } else if (process.platform === "win32") {
14
+ return path.join(process.env.APPDATA, "Claude", "claude_desktop_config.json");
15
+ } else {
16
+ return path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json");
17
+ }
18
+ },
19
+ generateConfig: () => ({
20
+ mcpServers: {
21
+ composter: {
22
+ command: "npx",
23
+ args: ["composter-mcp"],
24
+ },
25
+ },
26
+ }),
27
+ mergeKey: "mcpServers",
28
+ },
29
+ cursor: {
30
+ name: "Cursor",
31
+ configPath: () => path.join(process.cwd(), ".cursor", "mcp.json"),
32
+ generateConfig: () => ({
33
+ mcpServers: {
34
+ composter: {
35
+ command: "npx",
36
+ args: ["composter-mcp"],
37
+ },
38
+ },
39
+ }),
40
+ mergeKey: "mcpServers",
41
+ },
42
+ vscode: {
43
+ name: "VS Code (Copilot)",
44
+ configPath: () => path.join(process.cwd(), ".vscode", "mcp.json"),
45
+ generateConfig: () => ({
46
+ mcpServers: {
47
+ composter: {
48
+ command: "npx",
49
+ args: ["composter-mcp"],
50
+ },
51
+ },
52
+ }),
53
+ mergeKey: "mcpServers",
54
+ },
55
+ windsurf: {
56
+ name: "Windsurf",
57
+ configPath: () => {
58
+ if (process.platform === "darwin") {
59
+ return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
60
+ } else if (process.platform === "win32") {
61
+ return path.join(process.env.APPDATA, "Codeium", "windsurf", "mcp_config.json");
62
+ } else {
63
+ return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
64
+ }
65
+ },
66
+ generateConfig: () => ({
67
+ mcpServers: {
68
+ composter: {
69
+ command: "npx",
70
+ args: ["composter-mcp"],
71
+ },
72
+ },
73
+ }),
74
+ mergeKey: "mcpServers",
75
+ },
76
+ };
77
+
78
+ function printUsage() {
79
+ console.log(`
80
+ 🤖 Composter MCP - Setup Tool
81
+
82
+ Usage:
83
+ npx composter-mcp init <client>
84
+
85
+ Supported clients:
86
+ claude - Claude Desktop
87
+ cursor - Cursor IDE
88
+ vscode - VS Code with Copilot
89
+ windsurf - Windsurf IDE
90
+
91
+ Examples:
92
+ npx composter-mcp init claude
93
+ npx composter-mcp init cursor
94
+ `);
95
+ }
96
+
97
+ function initClient(clientName) {
98
+ const client = CLIENT_CONFIGS[clientName?.toLowerCase()];
99
+
100
+ if (!client) {
101
+ console.log(`❌ Unknown client: ${clientName}`);
102
+ console.log(`\nSupported clients: ${Object.keys(CLIENT_CONFIGS).join(", ")}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ const configPath = client.configPath();
107
+ const configDir = path.dirname(configPath);
108
+
109
+ console.log(`\n🔧 Setting up Composter MCP for ${client.name}...\n`);
110
+
111
+ // Read existing config if it exists
112
+ let existingConfig = {};
113
+ if (fs.existsSync(configPath)) {
114
+ try {
115
+ existingConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
116
+ console.log(`📄 Found existing config at: ${configPath}`);
117
+ } catch {
118
+ console.log(`⚠️ Existing config is invalid JSON, will create new one.`);
119
+ }
120
+ }
121
+
122
+ // Generate new config
123
+ const newConfig = client.generateConfig();
124
+
125
+ // Merge configs
126
+ let mergedConfig;
127
+ if (client.mergeKey) {
128
+ mergedConfig = {
129
+ ...existingConfig,
130
+ [client.mergeKey]: {
131
+ ...existingConfig[client.mergeKey],
132
+ ...newConfig[client.mergeKey],
133
+ },
134
+ };
135
+ } else {
136
+ // Direct merge at root (e.g., Cursor)
137
+ mergedConfig = {
138
+ ...existingConfig,
139
+ ...newConfig,
140
+ };
141
+ }
142
+
143
+ // Create directory if needed
144
+ if (!fs.existsSync(configDir)) {
145
+ fs.mkdirSync(configDir, { recursive: true });
146
+ console.log(`📁 Created directory: ${configDir}`);
147
+ }
148
+
149
+ // Write config
150
+ fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
151
+ console.log(`✅ Config written to: ${configPath}`);
152
+
153
+ // Print success message
154
+ console.log(`
155
+ ╔═══════════════════════════════════════════════════════════════════╗
156
+ ║ 🎉 Setup Complete! ║
157
+ ╠═══════════════════════════════════════════════════════════════════╣
158
+ ║ ║
159
+ ║ Composter MCP has been configured for ${client.name.padEnd(24)} ║
160
+ ║ ║
161
+ ║ Next steps: ║
162
+ ║ 1. Make sure you're logged in: composter login ║
163
+ ║ 2. Restart ${client.name.padEnd(47)} ║
164
+ ║ 3. Look for "Composter" in your MCP tools ║
165
+ ║ ║
166
+ ║ Available tools: ║
167
+ ║ • search_components - Search your component vault ║
168
+ ║ • list_categories - List all your categories ║
169
+ ║ • list_components - List components in a category ║
170
+ ║ • read_component - Read full source code ║
171
+ ║ ║
172
+ ╚═══════════════════════════════════════════════════════════════════╝
173
+ `);
174
+ }
175
+
176
+ // Main
177
+ const args = process.argv.slice(2);
178
+ const command = args[0];
179
+ const clientArg = args[1];
180
+
181
+ if (command === "init") {
182
+ if (!clientArg) {
183
+ console.log("❌ Please specify a client to configure.");
184
+ printUsage();
185
+ process.exit(1);
186
+ }
187
+ initClient(clientArg);
188
+ } else if (command === "--help" || command === "-h" || !command) {
189
+ printUsage();
190
+ } else {
191
+ console.log(`❌ Unknown command: ${command}`);
192
+ printUsage();
193
+ process.exit(1);
194
+ }
195
+
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createMcpServer } from "../lib/factory.js";
4
+ import { loadSession, getBaseUrl } from "../lib/auth.js";
5
+
6
+ // Redirect console.log to stderr (MCP uses stdout for protocol communication)
7
+ console.log = (...args) => console.error(...args);
8
+
9
+ async function main() {
10
+ try {
11
+ // Check if user is logged in via CLI
12
+ const session = loadSession();
13
+ if (!session) {
14
+ console.error("❌ No session found. Please run 'composter login' first.");
15
+ process.exit(1);
16
+ }
17
+
18
+ const baseUrl = getBaseUrl();
19
+ console.error(`🚀 Composter MCP Server starting...`);
20
+ console.error(`📡 API: ${baseUrl}`);
21
+
22
+ // Create and start MCP server
23
+ const server = createMcpServer();
24
+ const transport = new StdioServerTransport();
25
+ await server.connect(transport);
26
+
27
+ console.error("✅ Composter MCP server running on stdio");
28
+ } catch (error) {
29
+ console.error("❌ Fatal Error:", error.message);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composter-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "description": "Your personal vault for React components. Push, pull, and sync reusable components across projects — like shadcn/ui but for YOUR code.",
6
6
  "main": "src/index.js",
@@ -36,9 +36,15 @@
36
36
  "node": ">=18.0.0"
37
37
  },
38
38
  "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.24.3",
39
40
  "commander": "^14.0.2",
40
41
  "dotenv": "^16.4.5",
41
42
  "inquirer": "^13.0.1",
42
43
  "node-fetch": "^3.3.2"
43
- }
44
+ },
45
+ "files": [
46
+ "src/",
47
+ "bin/",
48
+ "mcp/"
49
+ ]
44
50
  }
@@ -0,0 +1,47 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { dirname } from "path";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ export async function initVscode() {
10
+ const cwd = process.cwd();
11
+ const vscodeDir = path.join(cwd, ".vscode");
12
+ const mcpConfigPath = path.join(vscodeDir, "mcp.json");
13
+
14
+ // Find the globally installed composter-cli MCP server
15
+ // When installed globally, the MCP server is in:
16
+ // /usr/local/lib/node_modules/composter-cli/mcp/src/server.js
17
+ const globalMcpServerPath = path.join(
18
+ path.dirname(path.dirname(path.dirname(__dirname))),
19
+ "mcp",
20
+ "src",
21
+ "server.js"
22
+ );
23
+
24
+ const mcpConfig = {
25
+ servers: {
26
+ "composter-mcp": {
27
+ type: "stdio",
28
+ command: "node",
29
+ args: [globalMcpServerPath],
30
+ }
31
+ },
32
+ inputs: []
33
+ };
34
+
35
+ // Create .vscode directory if it doesn't exist
36
+ if (!fs.existsSync(vscodeDir)) {
37
+ fs.mkdirSync(vscodeDir, { recursive: true });
38
+ console.log("✓ Created .vscode directory");
39
+ }
40
+
41
+ // Write mcp.json
42
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
43
+ console.log("✓ Created .vscode/mcp.json");
44
+ console.log("\n📝 MCP Server configured for this project!");
45
+ console.log("🔄 Reload VS Code window to activate the MCP server");
46
+ console.log(" Press: Ctrl+Shift+P → 'Developer: Reload Window'");
47
+ }
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 { initVscode } from "./commands/init.js";
9
10
  import { createRequire } from "module";
10
11
 
11
12
  const require = createRequire(import.meta.url);
@@ -23,6 +24,18 @@ program
23
24
  .description("Log into your Composter account")
24
25
  .action(login);
25
26
 
27
+ program
28
+ .command("init <editor>")
29
+ .description("Initialize MCP configuration for your editor (vscode)")
30
+ .action((editor) => {
31
+ if (editor.toLowerCase() === "vscode") {
32
+ initVscode();
33
+ } else {
34
+ console.error("❌ Only 'vscode' is supported currently");
35
+ process.exit(1);
36
+ }
37
+ });
38
+
26
39
  program
27
40
  .command("mkcat <category-name>")
28
41
  .description("Create a new category")