@yassinello/create-mymcp 0.3.0 → 0.3.1

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 (2) hide show
  1. package/index.mjs +188 -596
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1,596 +1,188 @@
1
- #!/usr/bin/env node
2
-
3
- // create-mymcp — Interactive installer for MyMCP
4
- // Usage: npx @yassinello/create-mymcp@latest
5
-
6
- import { execSync, spawnSync } from "node:child_process";
7
- import { createInterface } from "node:readline";
8
- import { randomBytes } from "node:crypto";
9
- import { existsSync, writeFileSync, readFileSync, readdirSync } from "node:fs";
10
- import { join, resolve, isAbsolute } from "node:path";
11
-
12
- // ── Helpers ──────────────────────────────────────────────────────────
13
-
14
- const rl = createInterface({ input: process.stdin, output: process.stdout });
15
- const ask = (q) => new Promise((r) => rl.question(q, r));
16
-
17
- const BOLD = "\x1b[1m";
18
- const DIM = "\x1b[2m";
19
- const GREEN = "\x1b[32m";
20
- const CYAN = "\x1b[36m";
21
- const YELLOW = "\x1b[33m";
22
- const RED = "\x1b[31m";
23
- const RESET = "\x1b[0m";
24
-
25
- const log = (msg) => console.log(msg);
26
- const step = (n, msg) => log(`\n${CYAN}[${n}]${RESET} ${BOLD}${msg}${RESET}`);
27
- const ok = (msg) => log(` ${GREEN}✓${RESET} ${msg}`);
28
- const warn = (msg) => log(` ${YELLOW}!${RESET} ${msg}`);
29
- const info = (msg) => log(` ${DIM}${msg}${RESET}`);
30
-
31
- function run(cmd, opts = {}) {
32
- try {
33
- return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts }).trim();
34
- } catch {
35
- return null;
36
- }
37
- }
38
-
39
- function hasCommand(cmd) {
40
- const check = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
41
- return run(check) !== null;
42
- }
43
-
44
- async function confirm(msg, defaultYes = true) {
45
- const hint = defaultYes ? "Y/n" : "y/N";
46
- const answer = (await ask(` ${msg} [${hint}] `)).trim().toLowerCase();
47
- if (answer === "") return defaultYes;
48
- return answer === "y" || answer === "yes";
49
- }
50
-
51
- /** Strip surrounding quotes and trim whitespace */
52
- function cleanPath(input) {
53
- return input.trim().replace(/^["']|["']$/g, "");
54
- }
55
-
56
- /** Check if a directory exists and is non-empty */
57
- function isDirNonEmpty(dir) {
58
- try {
59
- return readdirSync(dir).length > 0;
60
- } catch {
61
- return false;
62
- }
63
- }
64
-
65
- /**
66
- * Clean a credential value:
67
- * - Strip "KEY=" prefix if user pasted "GOOGLE_CLIENT_ID=value"
68
- * - Return null for non-values like "NA", "N/A", "none", "skip", "-", ""
69
- */
70
- function cleanCredential(value, expectedKey) {
71
- let cleaned = value.trim();
72
-
73
- // Strip "KEY=" prefix if user pasted the whole line
74
- const prefixPattern = new RegExp(`^${expectedKey}\\s*=\\s*`, "i");
75
- cleaned = cleaned.replace(prefixPattern, "");
76
-
77
- // Also strip any generic KEY= prefix (e.g., user pasted from .env)
78
- cleaned = cleaned.replace(/^[A-Z_]+=/, "");
79
-
80
- // Detect non-values
81
- const skipValues = ["na", "n/a", "none", "skip", "-", "no", ""];
82
- if (skipValues.includes(cleaned.toLowerCase())) {
83
- return null;
84
- }
85
-
86
- return cleaned;
87
- }
88
-
89
- /**
90
- * Normalize a timezone input:
91
- * - "Paris" "Europe/Paris"
92
- * - "New York" → "America/New_York"
93
- * - "UTC" → "UTC"
94
- */
95
- const TIMEZONE_SHORTCUTS = {
96
- paris: "Europe/Paris",
97
- london: "Europe/London",
98
- berlin: "Europe/Berlin",
99
- amsterdam: "Europe/Amsterdam",
100
- brussels: "Europe/Brussels",
101
- madrid: "Europe/Madrid",
102
- rome: "Europe/Rome",
103
- lisbon: "Europe/Lisbon",
104
- zurich: "Europe/Zurich",
105
- tokyo: "Asia/Tokyo",
106
- shanghai: "Asia/Shanghai",
107
- singapore: "Asia/Singapore",
108
- dubai: "Asia/Dubai",
109
- mumbai: "Asia/Kolkata",
110
- sydney: "Australia/Sydney",
111
- auckland: "Pacific/Auckland",
112
- "new york": "America/New_York",
113
- "new_york": "America/New_York",
114
- "los angeles": "America/Los_Angeles",
115
- "los_angeles": "America/Los_Angeles",
116
- chicago: "America/Chicago",
117
- denver: "America/Denver",
118
- toronto: "America/Toronto",
119
- "sao paulo": "America/Sao_Paulo",
120
- "são paulo": "America/Sao_Paulo",
121
- montreal: "America/Montreal",
122
- utc: "UTC",
123
- gmt: "UTC",
124
- };
125
-
126
- function normalizeTimezone(input) {
127
- const lower = input.trim().toLowerCase();
128
- if (TIMEZONE_SHORTCUTS[lower]) return TIMEZONE_SHORTCUTS[lower];
129
- // If it already looks like a valid IANA timezone (contains /), return as-is
130
- if (input.includes("/")) return input.trim();
131
- // Default
132
- return input.trim();
133
- }
134
-
135
- /**
136
- * Normalize a locale input:
137
- * - "fr" "fr-FR"
138
- * - "en" → "en-US"
139
- * - "de" "de-DE"
140
- */
141
- const LOCALE_SHORTCUTS = {
142
- fr: "fr-FR",
143
- en: "en-US",
144
- de: "de-DE",
145
- es: "es-ES",
146
- it: "it-IT",
147
- pt: "pt-PT",
148
- nl: "nl-NL",
149
- ja: "ja-JP",
150
- zh: "zh-CN",
151
- ko: "ko-KR",
152
- ar: "ar-SA",
153
- ru: "ru-RU",
154
- };
155
-
156
- function normalizeLocale(input) {
157
- const trimmed = input.trim();
158
- const lower = trimmed.toLowerCase();
159
- if (LOCALE_SHORTCUTS[lower]) return LOCALE_SHORTCUTS[lower];
160
- return trimmed;
161
- }
162
-
163
- /**
164
- * Extract owner/repo from a GitHub URL or return as-is
165
- * - "https://github.com/Yassinello/obsidyass" → "Yassinello/obsidyass"
166
- * - "Yassinello/obsidyass" → "Yassinello/obsidyass"
167
- */
168
- function normalizeGitHubRepo(input) {
169
- const cleaned = input.trim().replace(/\/+$/, ""); // strip trailing slashes
170
- const urlMatch = cleaned.match(/github\.com\/([^/]+\/[^/]+)/);
171
- if (urlMatch) return urlMatch[1];
172
- return cleaned;
173
- }
174
-
175
- /**
176
- * Slugify a project name for Vercel:
177
- * - Lowercase, replace spaces with dashes, strip invalid chars
178
- */
179
- function slugify(name) {
180
- return name
181
- .toLowerCase()
182
- .replace(/\s+/g, "-")
183
- .replace(/[^a-z0-9._-]/g, "")
184
- .replace(/---+/g, "--") // Vercel doesn't allow ---
185
- .slice(0, 100);
186
- }
187
-
188
- /**
189
- * Mask a secret for display: show first 4 chars, then ***
190
- */
191
- function maskSecret(value) {
192
- if (value.length <= 8) return "****";
193
- return value.slice(0, 4) + "****" + value.slice(-4);
194
- }
195
-
196
- // ── Pack definitions ─────────────────────────────────────────────────
197
-
198
- const PACKS = [
199
- {
200
- id: "google",
201
- name: "Google Workspace",
202
- tools: "Gmail, Calendar, Contacts, Drive (18 tools)",
203
- vars: [
204
- {
205
- key: "GOOGLE_CLIENT_ID",
206
- prompt: "Google OAuth Client ID",
207
- help: "https://console.cloud.google.com/apis/credentials",
208
- },
209
- { key: "GOOGLE_CLIENT_SECRET", prompt: "Google OAuth Client Secret", sensitive: true },
210
- {
211
- key: "GOOGLE_REFRESH_TOKEN",
212
- prompt: "Google OAuth Refresh Token",
213
- help: "Run the OAuth flow after deploy at /api/auth/google",
214
- optional: true,
215
- sensitive: true,
216
- },
217
- ],
218
- },
219
- {
220
- id: "vault",
221
- name: "Obsidian Vault",
222
- tools: "Read, write, search, backlinks, web clipper (15 tools)",
223
- vars: [
224
- {
225
- key: "GITHUB_PAT",
226
- prompt: "GitHub PAT (with repo scope)",
227
- help: "https://github.com/settings/tokens",
228
- sensitive: true,
229
- },
230
- {
231
- key: "GITHUB_REPO",
232
- prompt: "GitHub repo",
233
- example: "owner/repo or https://github.com/owner/repo",
234
- transform: normalizeGitHubRepo,
235
- },
236
- ],
237
- },
238
- {
239
- id: "browser",
240
- name: "Browser Automation",
241
- tools: "Web browse, extract, act, LinkedIn feed (4 tools)",
242
- vars: [
243
- {
244
- key: "BROWSERBASE_API_KEY",
245
- prompt: "Browserbase API key",
246
- help: "https://browserbase.com",
247
- sensitive: true,
248
- },
249
- { key: "BROWSERBASE_PROJECT_ID", prompt: "Browserbase Project ID" },
250
- {
251
- key: "OPENROUTER_API_KEY",
252
- prompt: "OpenRouter API key",
253
- help: "https://openrouter.ai/keys",
254
- sensitive: true,
255
- },
256
- ],
257
- },
258
- {
259
- id: "slack",
260
- name: "Slack",
261
- tools: "Channels, messages, threads, profiles, search (6 tools)",
262
- vars: [
263
- {
264
- key: "SLACK_BOT_TOKEN",
265
- prompt: "Slack Bot User OAuth Token",
266
- help: "https://api.slack.com/apps → OAuth & Permissions",
267
- sensitive: true,
268
- },
269
- ],
270
- },
271
- {
272
- id: "notion",
273
- name: "Notion",
274
- tools: "Search, read, create, update, query databases (5 tools)",
275
- vars: [
276
- {
277
- key: "NOTION_API_KEY",
278
- prompt: "Notion Internal Integration Token",
279
- help: "https://www.notion.so/my-integrations",
280
- sensitive: true,
281
- },
282
- ],
283
- },
284
- {
285
- id: "composio",
286
- name: "Composio",
287
- tools: "1000+ app integrations — Jira, HubSpot, Salesforce, Airtable... (2 tools)",
288
- vars: [
289
- {
290
- key: "COMPOSIO_API_KEY",
291
- prompt: "Composio API key",
292
- help: "https://composio.dev → Settings",
293
- sensitive: true,
294
- },
295
- ],
296
- },
297
- ];
298
-
299
- // ── Main ─────────────────────────────────────────────────────────────
300
-
301
- async function main() {
302
- log("");
303
- log(
304
- `${BOLD} ╔══════════════════════════════════════════╗${RESET}`
305
- );
306
- log(
307
- `${BOLD} ║ ${CYAN}create-mymcp${RESET}${BOLD} ║${RESET}`
308
- );
309
- log(
310
- `${BOLD} ║ Your personal AI backend in minutes ║${RESET}`
311
- );
312
- log(
313
- `${BOLD} ╚══════════════════════════════════════════╝${RESET}`
314
- );
315
-
316
- // ── Step 1: Project directory ────────────────────────────────────
317
-
318
- step("1/5", "Project setup");
319
-
320
- const defaultDir = "mymcp";
321
- info(`Just type a folder name, or a full path. Default: ${defaultDir}`);
322
- const rawInput = (await ask(` Project directory [${defaultDir}]: `)).trim();
323
- const cleaned = cleanPath(rawInput) || defaultDir;
324
-
325
- const projectDir = isAbsolute(cleaned) ? cleaned : resolve(cleaned);
326
- const projectName = projectDir.split(/[/\\]/).pop();
327
-
328
- if (existsSync(projectDir) && isDirNonEmpty(projectDir)) {
329
- log(` ${RED}✗${RESET} Directory "${projectDir}" already exists and is not empty.`);
330
- rl.close();
331
- process.exit(1);
332
- }
333
-
334
- info(`Will create: ${projectDir}`);
335
-
336
- // ── Step 2: Clone ────────────────────────────────────────────────
337
-
338
- step("2/5", "Cloning MyMCP");
339
-
340
- if (!hasCommand("git")) {
341
- log(` ${RED}✗${RESET} git is required. Install it from https://git-scm.com`);
342
- rl.close();
343
- process.exit(1);
344
- }
345
-
346
- const cloneResult = spawnSync(
347
- "git",
348
- ["clone", "https://github.com/Yassinello/mymcp.git", projectDir],
349
- { stdio: "inherit" }
350
- );
351
-
352
- if (cloneResult.status !== 0) {
353
- log(` ${RED}✗${RESET} Failed to clone repository.`);
354
- rl.close();
355
- process.exit(1);
356
- }
357
-
358
- run(`git -C "${projectDir}" remote rename origin upstream`);
359
- ok("Cloned and upstream remote configured");
360
- info("Run `npm run update` to pull updates anytime");
361
-
362
- // ── Step 3: Pick packs ───────────────────────────────────────────
363
-
364
- step("3/5", "Choose your tool packs");
365
- info("Press Enter to accept the default (shown in uppercase).");
366
- log("");
367
-
368
- const selectedPacks = [];
369
- for (const pack of PACKS) {
370
- const defaultOn = pack.id === "vault" || pack.id === "google";
371
- const yes = await confirm(
372
- `${BOLD}${pack.name}${RESET} — ${pack.tools}?`,
373
- defaultOn
374
- );
375
- if (yes) selectedPacks.push(pack);
376
- }
377
-
378
- if (selectedPacks.length === 0) {
379
- warn("No packs selected. You can add them later in your .env file.");
380
- } else {
381
- ok(`Selected: ${selectedPacks.map((p) => p.name).join(", ")}`);
382
- }
383
-
384
- // ── Step 4: Collect credentials ──────────────────────────────────
385
-
386
- step("4/5", "Configure credentials");
387
- info("Paste just the value — not the KEY=value format.");
388
- info("Type 'skip' or press Enter on optional fields to skip.");
389
-
390
- const envVars = {};
391
- const mcpToken = randomBytes(32).toString("hex");
392
- envVars.MCP_AUTH_TOKEN = mcpToken;
393
- ok(`MCP_AUTH_TOKEN generated: ${maskSecret(mcpToken)}`);
394
-
395
- // Instance settings
396
- log("");
397
- const tzRaw = (await ask(` Timezone (e.g. Europe/Paris, Tokyo, UTC) [UTC]: `)).trim() || "UTC";
398
- const tz = normalizeTimezone(tzRaw);
399
- if (tz !== tzRaw && tzRaw.toLowerCase() !== "utc") {
400
- info(`Normalized to: ${tz}`);
401
- }
402
-
403
- const localeRaw = (await ask(` Locale (e.g. fr, en-US, de) [en-US]: `)).trim() || "en-US";
404
- const locale = normalizeLocale(localeRaw);
405
- if (locale !== localeRaw) {
406
- info(`Normalized to: ${locale}`);
407
- }
408
-
409
- const displayName = (await ask(` Display name [User]: `)).trim() || "User";
410
-
411
- envVars.MYMCP_TIMEZONE = tz;
412
- envVars.MYMCP_LOCALE = locale;
413
- envVars.MYMCP_DISPLAY_NAME = displayName;
414
-
415
- // Pack credentials
416
- const packStatus = []; // Track for recap
417
-
418
- for (const pack of selectedPacks) {
419
- log("");
420
- log(` ${BOLD}${pack.name}${RESET}`);
421
- let allSet = true;
422
-
423
- for (const v of pack.vars) {
424
- if (v.help) info(v.help);
425
- if (v.example) info(`Format: ${v.example}`);
426
- if (v.optional) info("(optional — press Enter to skip)");
427
-
428
- const rawValue = (await ask(` ${v.prompt}: `)).trim();
429
- const cleaned = cleanCredential(rawValue, v.key);
430
-
431
- if (cleaned) {
432
- // Apply transform if defined (e.g., GitHub URL → owner/repo)
433
- const finalValue = v.transform ? v.transform(cleaned) : cleaned;
434
- envVars[v.key] = finalValue;
435
-
436
- if (v.sensitive) {
437
- ok(`${v.key} set (${maskSecret(finalValue)})`);
438
- } else {
439
- ok(`${v.key} = ${finalValue}`);
440
- }
441
- } else if (v.optional) {
442
- info(`${v.key} skipped`);
443
- } else {
444
- warn(`${v.key} skipped — ${pack.name} pack won't activate until set`);
445
- allSet = false;
446
- }
447
- }
448
-
449
- packStatus.push({ name: pack.name, active: allSet });
450
- }
451
-
452
- // ── Write .env ───────────────────────────────────────────────────
453
-
454
- const envPath = join(projectDir, ".env");
455
- const envExamplePath = join(projectDir, ".env.example");
456
-
457
- let envContent = "# MyMCP — Generated by create-mymcp\n";
458
- envContent += `# Created: ${new Date().toISOString().split("T")[0]}\n\n`;
459
-
460
- const writtenVars = new Set();
461
-
462
- if (existsSync(envExamplePath)) {
463
- const example = readFileSync(envExamplePath, "utf-8");
464
- const lines = example.split("\n");
465
- for (const line of lines) {
466
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
467
- if (match && envVars[match[1]] !== undefined) {
468
- envContent += `${match[1]}=${envVars[match[1]]}\n`;
469
- writtenVars.add(match[1]);
470
- } else {
471
- envContent += line + "\n";
472
- }
473
- }
474
- }
475
-
476
- for (const [key, value] of Object.entries(envVars)) {
477
- if (!writtenVars.has(key)) {
478
- envContent += `${key}=${value}\n`;
479
- }
480
- }
481
-
482
- writeFileSync(envPath, envContent);
483
- ok(".env file created");
484
-
485
- // ── Step 5: Install & Deploy ─────────────────────────────────────
486
-
487
- step("5/5", "Install & deploy");
488
-
489
- log("");
490
- info("Installing dependencies...");
491
- const installResult = spawnSync("npm", ["install"], {
492
- cwd: projectDir,
493
- stdio: "inherit",
494
- shell: true,
495
- });
496
-
497
- if (installResult.status !== 0) {
498
- warn("npm install failed — you can run it manually later");
499
- } else {
500
- ok("Dependencies installed");
501
- }
502
-
503
- // Offer Vercel deploy
504
- log("");
505
- const deployVercel = await confirm(
506
- "Deploy to Vercel now? (requires Vercel CLI)",
507
- false
508
- );
509
-
510
- let deploySucceeded = false;
511
-
512
- if (deployVercel) {
513
- if (!hasCommand("vercel")) {
514
- info("Installing Vercel CLI...");
515
- spawnSync("npm", ["install", "-g", "vercel"], {
516
- stdio: "inherit",
517
- shell: true,
518
- });
519
- }
520
-
521
- // Slugify project name for Vercel
522
- const vercelName = slugify(projectName);
523
- if (vercelName !== projectName) {
524
- info(`Vercel project name: ${vercelName} (slugified from "${projectName}")`);
525
- }
526
-
527
- log("");
528
- info("Running vercel deploy...");
529
- const vercelResult = spawnSync("vercel", ["--yes", "--name", vercelName], {
530
- cwd: projectDir,
531
- stdio: "inherit",
532
- shell: true,
533
- env: { ...process.env, NO_UPDATE_NOTIFIER: "1" },
534
- });
535
-
536
- if (vercelResult.status === 0) {
537
- ok("Deployed to Vercel!");
538
- deploySucceeded = true;
539
- log("");
540
- warn("Don't forget to add your env vars in the Vercel dashboard:");
541
- info("Vercel → Project Settings → Environment Variables");
542
- info("Or run: vercel env add MCP_AUTH_TOKEN");
543
- } else {
544
- warn("Deploy failed — you can run `vercel` manually in your project dir");
545
- }
546
- }
547
-
548
- // ── Done ─────────────────────────────────────────────────────────
549
-
550
- log("");
551
- log(`${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
552
- log(`${BOLD} ║ ${GREEN}Setup complete!${RESET}${BOLD} ║${RESET}`);
553
- log(`${BOLD} ╚══════════════════════════════════════════╝${RESET}`);
554
-
555
- // Recap table
556
- log("");
557
- log(` ${BOLD}Pack status:${RESET}`);
558
- for (const ps of packStatus) {
559
- const icon = ps.active ? `${GREEN}✓${RESET}` : `${YELLOW}○${RESET}`;
560
- const status = ps.active ? "ready" : "needs credentials";
561
- log(` ${icon} ${ps.name} — ${status}`);
562
- }
563
-
564
- // Packs not selected
565
- const selectedIds = new Set(selectedPacks.map((p) => p.id));
566
- const skippedPacks = PACKS.filter((p) => !selectedIds.has(p.id));
567
- for (const sp of skippedPacks) {
568
- log(` ${DIM}– ${sp.name} — not selected${RESET}`);
569
- }
570
-
571
- log("");
572
- log(` ${BOLD}Next steps:${RESET}`);
573
- log("");
574
- log(` ${CYAN}cd ${projectName}${RESET}`);
575
- if (!deploySucceeded) {
576
- log(` ${CYAN}npm run dev${RESET} ${DIM}# Start locally at http://localhost:3000${RESET}`);
577
- log(` ${CYAN}vercel${RESET} ${DIM}# Deploy to Vercel when ready${RESET}`);
578
- }
579
- log(` ${DIM}Then visit: http://localhost:3000/setup${RESET}`);
580
- log("");
581
- log(` ${BOLD}Connect to Claude Desktop / Claude Code:${RESET}`);
582
- log(` ${DIM}Endpoint: https://your-app.vercel.app/api/mcp${RESET}`);
583
- log(` ${DIM}Token: ${maskSecret(mcpToken)}${RESET}`);
584
- log("");
585
- log(` ${BOLD}Stay up to date:${RESET}`);
586
- log(` ${CYAN}npm run update${RESET}`);
587
- log("");
588
-
589
- rl.close();
590
- }
591
-
592
- main().catch((err) => {
593
- console.error(`\n${RED}Error:${RESET} ${err.message}`);
594
- rl.close();
595
- process.exit(1);
596
- });
1
+ #!/usr/bin/env node
2
+
3
+ // create-mymcp — Interactive installer for MyMCP
4
+ // Usage: npx @yassinello/create-mymcp@latest
5
+
6
+ import { spawnSync, spawn } from "node:child_process";
7
+ import { createInterface } from "node:readline";
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { resolve, isAbsolute } from "node:path";
10
+
11
+ // ── Helpers ──────────────────────────────────────────────────────────
12
+
13
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
14
+ const ask = (q) => new Promise((r) => rl.question(q, r));
15
+
16
+ const BOLD = "\x1b[1m";
17
+ const DIM = "\x1b[2m";
18
+ const GREEN = "\x1b[32m";
19
+ const CYAN = "\x1b[36m";
20
+ const YELLOW = "\x1b[33m";
21
+ const RED = "\x1b[31m";
22
+ const RESET = "\x1b[0m";
23
+
24
+ const log = (msg) => console.log(msg);
25
+ const step = (n, msg) => log(`\n${CYAN}[${n}]${RESET} ${BOLD}${msg}${RESET}`);
26
+ const ok = (msg) => log(` ${GREEN}✓${RESET} ${msg}`);
27
+ const warn = (msg) => log(` ${YELLOW}!${RESET} ${msg}`);
28
+ const info = (msg) => log(` ${DIM}${msg}${RESET}`);
29
+
30
+ function openBrowserUrl(url) {
31
+ const cmd =
32
+ process.platform === "win32"
33
+ ? "start"
34
+ : process.platform === "darwin"
35
+ ? "open"
36
+ : "xdg-open";
37
+ spawnSync(cmd, [url], { shell: true, stdio: "ignore" });
38
+ }
39
+
40
+ function cleanPath(input) {
41
+ return input.trim().replace(/^["']|["']$/g, "");
42
+ }
43
+
44
+ function isDirNonEmpty(dir) {
45
+ try {
46
+ return readdirSync(dir).length > 0;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+
53
+ // ── Main ─────────────────────────────────────────────────────────────
54
+
55
+ async function main() {
56
+ log("");
57
+ log(`${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
58
+ log(`${BOLD} ║ ${CYAN}create-mymcp${RESET}${BOLD} ║${RESET}`);
59
+ log(`${BOLD} ║ Your personal AI backend in minutes ║${RESET}`);
60
+ log(`${BOLD} ╚══════════════════════════════════════════╝${RESET}`);
61
+
62
+ // ── Step 1: Project directory ────────────────────────────────────
63
+
64
+ step("1/3", "Project setup");
65
+ info("Just type a folder name, or a full path.");
66
+
67
+ const rawInput = (await ask(` Project directory [mymcp]: `)).trim();
68
+ const cleaned = cleanPath(rawInput) || "mymcp";
69
+ const projectDir = isAbsolute(cleaned) ? cleaned : resolve(cleaned);
70
+ const projectName = projectDir.split(/[/\\]/).pop();
71
+
72
+ if (existsSync(projectDir) && isDirNonEmpty(projectDir)) {
73
+ log(` ${RED}✗${RESET} Directory "${projectDir}" already exists and is not empty.`);
74
+ rl.close();
75
+ process.exit(1);
76
+ }
77
+
78
+ info(`Will create: ${projectDir}`);
79
+
80
+ // ── Step 2: Clone ────────────────────────────────────────────────
81
+
82
+ step("2/3", "Cloning MyMCP");
83
+
84
+ const cloneResult = spawnSync(
85
+ "git",
86
+ ["clone", "https://github.com/Yassinello/mymcp.git", projectDir],
87
+ { stdio: "inherit" }
88
+ );
89
+
90
+ if (cloneResult.status !== 0) {
91
+ log(` ${RED}✗${RESET} Clone failed. Is git installed?`);
92
+ rl.close();
93
+ process.exit(1);
94
+ }
95
+
96
+ // Set up upstream for updates
97
+ spawnSync("git", ["-C", projectDir, "remote", "rename", "origin", "upstream"], {
98
+ stdio: "pipe",
99
+ });
100
+ ok("Cloned and upstream remote configured");
101
+
102
+ // ── Step 3: Install + Launch Setup Wizard ────────────────────────
103
+
104
+ step("3/3", "Installing & launching setup wizard");
105
+
106
+ log("");
107
+ info("Installing dependencies (this takes ~1 minute)...");
108
+ const installResult = spawnSync("npm", ["install"], {
109
+ cwd: projectDir,
110
+ stdio: "inherit",
111
+ shell: true,
112
+ });
113
+
114
+ if (installResult.status !== 0) {
115
+ warn("npm install failed — try running it manually:");
116
+ log(` ${CYAN}cd ${projectName} && npm install${RESET}`);
117
+ rl.close();
118
+ process.exit(1);
119
+ }
120
+ ok("Dependencies installed");
121
+
122
+ // Start dev server in background
123
+ log("");
124
+ info("Starting dev server...");
125
+
126
+ const devServer = spawn("npm", ["run", "dev"], {
127
+ cwd: projectDir,
128
+ shell: true,
129
+ stdio: "pipe",
130
+ detached: true,
131
+ });
132
+
133
+ // Wait for the server to be ready
134
+ let serverReady = false;
135
+ const startTime = Date.now();
136
+
137
+ while (!serverReady && Date.now() - startTime < 30000) {
138
+ try {
139
+ const res = await fetch("http://localhost:3000/api/health");
140
+ if (res.ok) serverReady = true;
141
+ } catch {
142
+ // Not ready yet
143
+ }
144
+ if (!serverReady) await new Promise((r) => setTimeout(r, 1000));
145
+ }
146
+
147
+ if (serverReady) {
148
+ ok("Dev server running at http://localhost:3000");
149
+
150
+ // Open setup wizard in browser
151
+ log("");
152
+ info("Opening setup wizard in your browser...");
153
+ openBrowserUrl("http://localhost:3000/setup");
154
+
155
+ log("");
156
+ log(`${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
157
+ log(`${BOLD} ║ ${GREEN}Complete the setup in your browser${RESET}${BOLD} ║${RESET}`);
158
+ log(`${BOLD} ╚══════════════════════════════════════════╝${RESET}`);
159
+ log("");
160
+ log(` ${BOLD}Setup wizard:${RESET} ${CYAN}http://localhost:3000/setup${RESET}`);
161
+ log("");
162
+ log(` ${DIM}The wizard will help you:${RESET}`);
163
+ log(` ${DIM} 1. Choose your tool packs${RESET}`);
164
+ log(` ${DIM} 2. Enter credentials (with live testing)${RESET}`);
165
+ log(` ${DIM} 3. Configure your instance${RESET}`);
166
+ log(` ${DIM} 4. Save your .env file${RESET}`);
167
+ log("");
168
+ log(` ${DIM}Press Ctrl+C to stop the dev server when done.${RESET}`);
169
+ log(` ${DIM}Run ${CYAN}npm run update${RESET}${DIM} anytime to pull updates.${RESET}`);
170
+ log("");
171
+
172
+ // Keep process alive while server runs
173
+ devServer.unref();
174
+ } else {
175
+ warn("Server didn't start in time. Start it manually:");
176
+ log(` ${CYAN}cd ${projectName} && npm run dev${RESET}`);
177
+ log(` Then open: ${CYAN}http://localhost:3000/setup${RESET}`);
178
+ devServer.kill();
179
+ }
180
+
181
+ rl.close();
182
+ }
183
+
184
+ main().catch((err) => {
185
+ console.error(`\n${RED}Error:${RESET} ${err.message}`);
186
+ rl.close();
187
+ process.exit(1);
188
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yassinello/create-mymcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Set up your personal MCP server in minutes — interactive installer for MyMCP",
5
5
  "bin": {
6
6
  "create-mymcp": "./index.mjs"