@yassinello/create-mymcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.mjs +223 -31
  2. package/package.json +29 -29
package/index.mjs CHANGED
@@ -48,7 +48,7 @@ async function confirm(msg, defaultYes = true) {
48
48
  return answer === "y" || answer === "yes";
49
49
  }
50
50
 
51
- /** Clean user input: strip surrounding quotes, trim whitespace */
51
+ /** Strip surrounding quotes and trim whitespace */
52
52
  function cleanPath(input) {
53
53
  return input.trim().replace(/^["']|["']$/g, "");
54
54
  }
@@ -56,13 +56,143 @@ function cleanPath(input) {
56
56
  /** Check if a directory exists and is non-empty */
57
57
  function isDirNonEmpty(dir) {
58
58
  try {
59
- const entries = readdirSync(dir);
60
- return entries.length > 0;
59
+ return readdirSync(dir).length > 0;
61
60
  } catch {
62
61
  return false;
63
62
  }
64
63
  }
65
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
+
66
196
  // ── Pack definitions ─────────────────────────────────────────────────
67
197
 
68
198
  const PACKS = [
@@ -76,12 +206,13 @@ const PACKS = [
76
206
  prompt: "Google OAuth Client ID",
77
207
  help: "https://console.cloud.google.com/apis/credentials",
78
208
  },
79
- { key: "GOOGLE_CLIENT_SECRET", prompt: "Google OAuth Client Secret" },
209
+ { key: "GOOGLE_CLIENT_SECRET", prompt: "Google OAuth Client Secret", sensitive: true },
80
210
  {
81
211
  key: "GOOGLE_REFRESH_TOKEN",
82
212
  prompt: "Google OAuth Refresh Token",
83
213
  help: "Run the OAuth flow after deploy at /api/auth/google",
84
214
  optional: true,
215
+ sensitive: true,
85
216
  },
86
217
  ],
87
218
  },
@@ -94,10 +225,13 @@ const PACKS = [
94
225
  key: "GITHUB_PAT",
95
226
  prompt: "GitHub PAT (with repo scope)",
96
227
  help: "https://github.com/settings/tokens",
228
+ sensitive: true,
97
229
  },
98
230
  {
99
231
  key: "GITHUB_REPO",
100
- prompt: "GitHub repo (owner/repo format)",
232
+ prompt: "GitHub repo",
233
+ example: "owner/repo or https://github.com/owner/repo",
234
+ transform: normalizeGitHubRepo,
101
235
  },
102
236
  ],
103
237
  },
@@ -110,12 +244,14 @@ const PACKS = [
110
244
  key: "BROWSERBASE_API_KEY",
111
245
  prompt: "Browserbase API key",
112
246
  help: "https://browserbase.com",
247
+ sensitive: true,
113
248
  },
114
249
  { key: "BROWSERBASE_PROJECT_ID", prompt: "Browserbase Project ID" },
115
250
  {
116
251
  key: "OPENROUTER_API_KEY",
117
252
  prompt: "OpenRouter API key",
118
253
  help: "https://openrouter.ai/keys",
254
+ sensitive: true,
119
255
  },
120
256
  ],
121
257
  },
@@ -128,6 +264,7 @@ const PACKS = [
128
264
  key: "SLACK_BOT_TOKEN",
129
265
  prompt: "Slack Bot User OAuth Token",
130
266
  help: "https://api.slack.com/apps → OAuth & Permissions",
267
+ sensitive: true,
131
268
  },
132
269
  ],
133
270
  },
@@ -140,6 +277,7 @@ const PACKS = [
140
277
  key: "NOTION_API_KEY",
141
278
  prompt: "Notion Internal Integration Token",
142
279
  help: "https://www.notion.so/my-integrations",
280
+ sensitive: true,
143
281
  },
144
282
  ],
145
283
  },
@@ -152,6 +290,7 @@ const PACKS = [
152
290
  key: "COMPOSIO_API_KEY",
153
291
  prompt: "Composio API key",
154
292
  help: "https://composio.dev → Settings",
293
+ sensitive: true,
155
294
  },
156
295
  ],
157
296
  },
@@ -179,10 +318,10 @@ async function main() {
179
318
  step("1/5", "Project setup");
180
319
 
181
320
  const defaultDir = "mymcp";
321
+ info(`Just type a folder name, or a full path. Default: ${defaultDir}`);
182
322
  const rawInput = (await ask(` Project directory [${defaultDir}]: `)).trim();
183
323
  const cleaned = cleanPath(rawInput) || defaultDir;
184
324
 
185
- // If it's already absolute, use as-is; otherwise resolve relative to CWD
186
325
  const projectDir = isAbsolute(cleaned) ? cleaned : resolve(cleaned);
187
326
  const projectName = projectDir.split(/[/\\]/).pop();
188
327
 
@@ -216,7 +355,6 @@ async function main() {
216
355
  process.exit(1);
217
356
  }
218
357
 
219
- // Set up upstream remote for easy updates
220
358
  run(`git -C "${projectDir}" remote rename origin upstream`);
221
359
  ok("Cloned and upstream remote configured");
222
360
  info("Run `npm run update` to pull updates anytime");
@@ -224,6 +362,7 @@ async function main() {
224
362
  // ── Step 3: Pick packs ───────────────────────────────────────────
225
363
 
226
364
  step("3/5", "Choose your tool packs");
365
+ info("Press Enter to accept the default (shown in uppercase).");
227
366
  log("");
228
367
 
229
368
  const selectedPacks = [];
@@ -245,39 +384,69 @@ async function main() {
245
384
  // ── Step 4: Collect credentials ──────────────────────────────────
246
385
 
247
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.");
248
389
 
249
390
  const envVars = {};
250
-
251
- // Generate MCP auth token
252
- envVars.MCP_AUTH_TOKEN = randomBytes(32).toString("hex");
253
- ok(`MCP_AUTH_TOKEN generated: ${envVars.MCP_AUTH_TOKEN.slice(0, 8)}...`);
391
+ const mcpToken = randomBytes(32).toString("hex");
392
+ envVars.MCP_AUTH_TOKEN = mcpToken;
393
+ ok(`MCP_AUTH_TOKEN generated: ${maskSecret(mcpToken)}`);
254
394
 
255
395
  // Instance settings
256
396
  log("");
257
- const tz = (await ask(` Timezone [UTC]: `)).trim() || "UTC";
258
- const locale = (await ask(` Locale [en-US]: `)).trim() || "en-US";
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
+
259
409
  const displayName = (await ask(` Display name [User]: `)).trim() || "User";
410
+
260
411
  envVars.MYMCP_TIMEZONE = tz;
261
412
  envVars.MYMCP_LOCALE = locale;
262
413
  envVars.MYMCP_DISPLAY_NAME = displayName;
263
414
 
264
415
  // Pack credentials
416
+ const packStatus = []; // Track for recap
417
+
265
418
  for (const pack of selectedPacks) {
266
419
  log("");
267
420
  log(` ${BOLD}${pack.name}${RESET}`);
421
+ let allSet = true;
422
+
268
423
  for (const v of pack.vars) {
269
424
  if (v.help) info(v.help);
270
- if (v.optional) {
271
- info("(optional — press Enter to skip)");
272
- }
273
- const value = (await ask(` ${v.prompt}: `)).trim();
274
- if (value) {
275
- envVars[v.key] = value;
276
- ok(`${v.key} set`);
277
- } else if (!v.optional) {
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 {
278
444
  warn(`${v.key} skipped — ${pack.name} pack won't activate until set`);
445
+ allSet = false;
279
446
  }
280
447
  }
448
+
449
+ packStatus.push({ name: pack.name, active: allSet });
281
450
  }
282
451
 
283
452
  // ── Write .env ───────────────────────────────────────────────────
@@ -285,11 +454,9 @@ async function main() {
285
454
  const envPath = join(projectDir, ".env");
286
455
  const envExamplePath = join(projectDir, ".env.example");
287
456
 
288
- // Read .env.example as base, then overlay collected values
289
457
  let envContent = "# MyMCP — Generated by create-mymcp\n";
290
458
  envContent += `# Created: ${new Date().toISOString().split("T")[0]}\n\n`;
291
459
 
292
- // Track which vars we've written so we don't duplicate
293
460
  const writtenVars = new Set();
294
461
 
295
462
  if (existsSync(envExamplePath)) {
@@ -306,7 +473,6 @@ async function main() {
306
473
  }
307
474
  }
308
475
 
309
- // Append any remaining vars not in .env.example
310
476
  for (const [key, value] of Object.entries(envVars)) {
311
477
  if (!writtenVars.has(key)) {
312
478
  envContent += `${key}=${value}\n`;
@@ -320,7 +486,6 @@ async function main() {
320
486
 
321
487
  step("5/5", "Install & deploy");
322
488
 
323
- // Install dependencies
324
489
  log("");
325
490
  info("Installing dependencies...");
326
491
  const installResult = spawnSync("npm", ["install"], {
@@ -342,6 +507,8 @@ async function main() {
342
507
  false
343
508
  );
344
509
 
510
+ let deploySucceeded = false;
511
+
345
512
  if (deployVercel) {
346
513
  if (!hasCommand("vercel")) {
347
514
  info("Installing Vercel CLI...");
@@ -351,16 +518,24 @@ async function main() {
351
518
  });
352
519
  }
353
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
+
354
527
  log("");
355
528
  info("Running vercel deploy...");
356
- const vercelResult = spawnSync("vercel", ["--yes"], {
529
+ const vercelResult = spawnSync("vercel", ["--yes", "--name", vercelName], {
357
530
  cwd: projectDir,
358
531
  stdio: "inherit",
359
532
  shell: true,
533
+ env: { ...process.env, NO_UPDATE_NOTIFIER: "1" },
360
534
  });
361
535
 
362
536
  if (vercelResult.status === 0) {
363
537
  ok("Deployed to Vercel!");
538
+ deploySucceeded = true;
364
539
  log("");
365
540
  warn("Don't forget to add your env vars in the Vercel dashboard:");
366
541
  info("Vercel → Project Settings → Environment Variables");
@@ -376,19 +551,36 @@ async function main() {
376
551
  log(`${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
377
552
  log(`${BOLD} ║ ${GREEN}Setup complete!${RESET}${BOLD} ║${RESET}`);
378
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
+
379
571
  log("");
380
572
  log(` ${BOLD}Next steps:${RESET}`);
381
573
  log("");
382
574
  log(` ${CYAN}cd ${projectName}${RESET}`);
383
- if (!deployVercel) {
384
- log(` ${CYAN}npm run dev${RESET} ${DIM}# Start locally${RESET}`);
385
- log(` ${CYAN}vercel${RESET} ${DIM}# Deploy to Vercel${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}`);
386
578
  }
387
- log(` ${CYAN}open /setup${RESET} ${DIM}# Guided setup page${RESET}`);
579
+ log(` ${DIM}Then visit: http://localhost:3000/setup${RESET}`);
388
580
  log("");
389
581
  log(` ${BOLD}Connect to Claude Desktop / Claude Code:${RESET}`);
390
582
  log(` ${DIM}Endpoint: https://your-app.vercel.app/api/mcp${RESET}`);
391
- log(` ${DIM}Token: starts with ${envVars.MCP_AUTH_TOKEN ? envVars.MCP_AUTH_TOKEN.slice(0, 8) + "..." : "(check your .env)"}${RESET}`);
583
+ log(` ${DIM}Token: ${maskSecret(mcpToken)}${RESET}`);
392
584
  log("");
393
585
  log(` ${BOLD}Stay up to date:${RESET}`);
394
586
  log(` ${CYAN}npm run update${RESET}`);
package/package.json CHANGED
@@ -1,29 +1,29 @@
1
- {
2
- "name": "@yassinello/create-mymcp",
3
- "version": "0.2.0",
4
- "description": "Set up your personal MCP server in minutes — interactive installer for MyMCP",
5
- "bin": {
6
- "create-mymcp": "./index.mjs"
7
- },
8
- "keywords": [
9
- "mcp",
10
- "create",
11
- "installer",
12
- "claude",
13
- "chatgpt",
14
- "ai",
15
- "model-context-protocol"
16
- ],
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/Yassinello/mymcp",
20
- "directory": "create-mymcp"
21
- },
22
- "license": "MIT",
23
- "engines": {
24
- "node": ">=18"
25
- },
26
- "files": [
27
- "index.mjs"
28
- ]
29
- }
1
+ {
2
+ "name": "@yassinello/create-mymcp",
3
+ "version": "0.3.0",
4
+ "description": "Set up your personal MCP server in minutes — interactive installer for MyMCP",
5
+ "bin": {
6
+ "create-mymcp": "./index.mjs"
7
+ },
8
+ "keywords": [
9
+ "mcp",
10
+ "create",
11
+ "installer",
12
+ "claude",
13
+ "chatgpt",
14
+ "ai",
15
+ "model-context-protocol"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/Yassinello/mymcp",
20
+ "directory": "create-mymcp"
21
+ },
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "files": [
27
+ "index.mjs"
28
+ ]
29
+ }