@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.
- package/index.mjs +188 -596
- 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 {
|
|
7
|
-
import { createInterface } from "node:readline";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
});
|