@hellocrossman/mcp-sdk 0.2.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/dist/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import readline from "readline";
5
+ import { scanAllRoutes } from "./route-scan.js";
4
6
  const IMPORT_LINE_ESM = `import { createMcpServer } from '@hellocrossman/mcp-sdk';`;
5
7
  const IMPORT_LINE_CJS = `const { createMcpServer } = require('@hellocrossman/mcp-sdk');`;
6
- const SETUP_LINE = `createMcpServer({ app });`;
7
8
  const COMMON_ENTRY_FILES = [
8
9
  "server/index.ts",
9
10
  "server/index.js",
@@ -28,12 +29,49 @@ const EXPRESS_PATTERNS = [
28
29
  ];
29
30
  const APP_VAR_PATTERN = /(?:const|let|var)\s+(\w+)\s*=\s*express\(\)/;
30
31
  const LISTEN_PATTERN = /(?:app|server|httpServer)\s*\.listen\s*\(/;
32
+ const COLORS = {
33
+ reset: "\x1b[0m",
34
+ bold: "\x1b[1m",
35
+ dim: "\x1b[2m",
36
+ green: "\x1b[32m",
37
+ yellow: "\x1b[33m",
38
+ blue: "\x1b[34m",
39
+ magenta: "\x1b[35m",
40
+ cyan: "\x1b[36m",
41
+ white: "\x1b[37m",
42
+ gray: "\x1b[90m",
43
+ bgGreen: "\x1b[42m",
44
+ bgYellow: "\x1b[43m",
45
+ };
46
+ function c(color, text) {
47
+ return `${COLORS[color]}${text}${COLORS.reset}`;
48
+ }
49
+ function createPrompt() {
50
+ const rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout,
53
+ });
54
+ return {
55
+ ask: (question) => new Promise((resolve) => {
56
+ rl.question(question, (answer) => resolve(answer.trim()));
57
+ }),
58
+ confirm: async (question, defaultYes = true) => {
59
+ const hint = defaultYes ? "Y/n" : "y/N";
60
+ const answer = await new Promise((resolve) => {
61
+ rl.question(`${question} ${c("dim", `(${hint})`)} `, (a) => resolve(a.trim().toLowerCase()));
62
+ });
63
+ if (answer === "")
64
+ return defaultYes;
65
+ return answer === "y" || answer === "yes";
66
+ },
67
+ close: () => rl.close(),
68
+ };
69
+ }
31
70
  function findProjectRoot() {
32
71
  let dir = process.cwd();
33
72
  while (dir !== path.dirname(dir)) {
34
- if (fs.existsSync(path.join(dir, "package.json"))) {
73
+ if (fs.existsSync(path.join(dir, "package.json")))
35
74
  return dir;
36
- }
37
75
  dir = path.dirname(dir);
38
76
  }
39
77
  return process.cwd();
@@ -43,9 +81,8 @@ function findExpressEntryFile(root) {
43
81
  const fullPath = path.join(root, file);
44
82
  if (fs.existsSync(fullPath)) {
45
83
  const content = fs.readFileSync(fullPath, "utf-8");
46
- if (EXPRESS_PATTERNS.some((p) => p.test(content))) {
84
+ if (EXPRESS_PATTERNS.some((p) => p.test(content)))
47
85
  return fullPath;
48
- }
49
86
  }
50
87
  }
51
88
  const srcDirs = ["server", "src", "."];
@@ -57,14 +94,13 @@ function findExpressEntryFile(root) {
57
94
  for (const file of files) {
58
95
  const fullPath = path.join(dirPath, file);
59
96
  const content = fs.readFileSync(fullPath, "utf-8");
60
- if (EXPRESS_PATTERNS.some((p) => p.test(content))) {
97
+ if (EXPRESS_PATTERNS.some((p) => p.test(content)))
61
98
  return fullPath;
62
- }
63
99
  }
64
100
  }
65
101
  return null;
66
102
  }
67
- function isESM(filePath, root) {
103
+ function checkIsESM(filePath, root) {
68
104
  if (filePath.endsWith(".ts") || filePath.endsWith(".mjs"))
69
105
  return true;
70
106
  try {
@@ -76,20 +112,322 @@ function isESM(filePath, root) {
76
112
  }
77
113
  }
78
114
  function alreadySetup(content) {
79
- return (content.includes("createMcpServer") ||
80
- content.includes("@hellocrossman/mcp-sdk") ||
81
- content.includes("hellocrossman/mcp-sdk"));
115
+ return content.includes("createMcpServer") || content.includes("@hellocrossman/mcp-sdk");
82
116
  }
83
- function injectMcpSetup(filePath, root) {
84
- const content = fs.readFileSync(filePath, "utf-8");
117
+ function checkPgInstalled(root) {
118
+ return fs.existsSync(path.join(root, "node_modules", "pg"));
119
+ }
120
+ function detectPackageManager(root) {
121
+ if (fs.existsSync(path.join(root, "pnpm-lock.yaml")))
122
+ return "pnpm";
123
+ if (fs.existsSync(path.join(root, "yarn.lock")))
124
+ return "yarn";
125
+ return "npm";
126
+ }
127
+ function printHeader() {
128
+ console.log();
129
+ console.log(` ${c("bold", "@hellocrossman/mcp-sdk")}`);
130
+ console.log(` ${c("dim", "Turn your Express API into an MCP server")}`);
131
+ console.log();
132
+ }
133
+ function printStep(step, total, title) {
134
+ console.log();
135
+ console.log(` ${c("cyan", `[${step}/${total}]`)} ${c("bold", title)}`);
136
+ console.log(` ${c("dim", "─".repeat(50))}`);
137
+ }
138
+ function printToolTable(tools) {
139
+ const routeTools = tools.filter((t) => t.source === "route");
140
+ const dbTools = tools.filter((t) => t.source === "database");
141
+ if (routeTools.length > 0) {
142
+ console.log(`\n ${c("dim", "API Routes:")}`);
143
+ for (const t of routeTools) {
144
+ const status = t.enabled ? c("green", "ON ") : c("dim", "OFF");
145
+ const method = t.method.padEnd(6);
146
+ console.log(` ${status} ${c("yellow", method)} ${t.name} ${c("dim", `- ${t.description}`)}`);
147
+ }
148
+ }
149
+ if (dbTools.length > 0) {
150
+ console.log(`\n ${c("dim", "Database Tables:")}`);
151
+ for (const t of dbTools) {
152
+ const status = t.enabled ? c("green", "ON ") : c("dim", "OFF");
153
+ const method = t.method.padEnd(6);
154
+ console.log(` ${status} ${c("magenta", method)} ${t.name} ${c("dim", `- ${t.description}`)}`);
155
+ }
156
+ }
157
+ }
158
+ async function stepDetectApp(state, prompt, specifiedFile) {
159
+ printStep(1, 5, "Detecting Express app");
160
+ let entryFile = null;
161
+ if (specifiedFile) {
162
+ const resolved = path.resolve(state.root, specifiedFile);
163
+ if (fs.existsSync(resolved)) {
164
+ entryFile = resolved;
165
+ }
166
+ else {
167
+ console.log(` ${c("yellow", "!")} File not found: ${specifiedFile}`);
168
+ return false;
169
+ }
170
+ }
171
+ else {
172
+ entryFile = findExpressEntryFile(state.root);
173
+ }
174
+ if (!entryFile) {
175
+ console.log(` ${c("yellow", "!")} Could not find an Express app file.`);
176
+ const custom = await prompt.ask(` Enter the path to your Express app file: `);
177
+ if (custom) {
178
+ const resolved = path.resolve(state.root, custom);
179
+ if (fs.existsSync(resolved)) {
180
+ entryFile = resolved;
181
+ }
182
+ else {
183
+ console.log(` ${c("yellow", "!")} File not found: ${custom}`);
184
+ return false;
185
+ }
186
+ }
187
+ else {
188
+ return false;
189
+ }
190
+ }
191
+ const content = fs.readFileSync(entryFile, "utf-8");
85
192
  if (alreadySetup(content)) {
86
- return { success: true, message: "MCP SDK is already set up in this file." };
193
+ console.log(` ${c("green", "+")} MCP SDK is already set up in ${path.relative(state.root, entryFile)}`);
194
+ console.log(` ${c("dim", " Remove the existing createMcpServer() call to reconfigure.")}`);
195
+ return false;
87
196
  }
88
- const esm = isESM(filePath, root);
89
- const importLine = esm ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
90
197
  const appMatch = content.match(APP_VAR_PATTERN);
91
- const appVarName = appMatch ? appMatch[1] : "app";
92
- const setupCode = `createMcpServer({ app: ${appVarName} });`;
198
+ state.entryFile = entryFile;
199
+ state.appVarName = appMatch ? appMatch[1] : "app";
200
+ state.isESM = checkIsESM(entryFile, state.root);
201
+ console.log(` ${c("green", "+")} Found Express app: ${c("bold", path.relative(state.root, entryFile))}`);
202
+ console.log(` ${c("dim", ` Variable: ${state.appVarName}, Module: ${state.isESM ? "ESM" : "CommonJS"}`)}`);
203
+ return true;
204
+ }
205
+ async function stepScanRoutes(state) {
206
+ printStep(2, 5, "Scanning API routes");
207
+ state.routes = scanAllRoutes(state.entryFile, state.routePrefix);
208
+ if (state.routes.length === 0) {
209
+ console.log(` ${c("dim", " No routes found under")} ${state.routePrefix}`);
210
+ console.log(` ${c("dim", " Routes will be discovered at runtime when your app starts.")}`);
211
+ return;
212
+ }
213
+ console.log(` ${c("green", "+")} Found ${c("bold", String(state.routes.length))} API routes:\n`);
214
+ for (const route of state.routes) {
215
+ const method = route.method.padEnd(6);
216
+ console.log(` ${c("yellow", method)} ${route.path}`);
217
+ }
218
+ for (const route of state.routes) {
219
+ const name = generateToolName(route.method, route.path);
220
+ const description = generateToolDescription(route.method, route.path);
221
+ state.tools.push({
222
+ name,
223
+ description,
224
+ method: route.method,
225
+ path: route.path,
226
+ source: "route",
227
+ enabled: true,
228
+ });
229
+ }
230
+ }
231
+ async function stepScanDatabase(state, prompt) {
232
+ printStep(3, 5, "Database discovery");
233
+ const wantDb = await prompt.confirm(` Scan your database for tables to expose as tools?`);
234
+ if (!wantDb) {
235
+ state.databaseEnabled = false;
236
+ console.log(` ${c("dim", " Skipping database discovery.")}`);
237
+ return;
238
+ }
239
+ let dbUrl = process.env.DATABASE_URL || process.env.POSTGRES_URL || process.env.PG_CONNECTION_STRING;
240
+ if (!dbUrl) {
241
+ console.log(` ${c("yellow", "!")} No DATABASE_URL found in environment.`);
242
+ dbUrl = (await prompt.ask(` Enter your PostgreSQL connection string (or press Enter to skip): `)) || undefined;
243
+ if (!dbUrl) {
244
+ state.databaseEnabled = false;
245
+ console.log(` ${c("dim", " Skipping database discovery.")}`);
246
+ return;
247
+ }
248
+ }
249
+ if (!checkPgInstalled(state.root)) {
250
+ console.log(` ${c("yellow", "!")} The 'pg' package is required for database features.`);
251
+ const pm = detectPackageManager(state.root);
252
+ const installCmd = pm === "yarn" ? "yarn add pg" : pm === "pnpm" ? "pnpm add pg" : "npm install pg";
253
+ const installPg = await prompt.confirm(` Install pg now? (${c("dim", installCmd)})`);
254
+ if (installPg) {
255
+ console.log(` ${c("dim", ` Running ${installCmd}...`)}`);
256
+ const { execSync } = await import("child_process");
257
+ try {
258
+ execSync(installCmd, { cwd: state.root, stdio: "pipe" });
259
+ console.log(` ${c("green", "+")} pg installed successfully.`);
260
+ }
261
+ catch {
262
+ console.log(` ${c("yellow", "!")} Failed to install pg. Run '${installCmd}' manually.`);
263
+ state.databaseEnabled = false;
264
+ return;
265
+ }
266
+ }
267
+ else {
268
+ console.log(` ${c("dim", ` Run '${installCmd}' to enable database features later.`)}`);
269
+ state.databaseEnabled = false;
270
+ return;
271
+ }
272
+ }
273
+ try {
274
+ const { introspectDatabase } = await import("./db-introspect.js");
275
+ const { isSensitiveTable } = await import("./db-tool-generator.js");
276
+ console.log(` ${c("dim", " Connecting to database...")}`);
277
+ const result = await introspectDatabase(dbUrl);
278
+ const safeTables = result.tables.filter((t) => !isSensitiveTable(t.name));
279
+ const hiddenTables = result.tables.filter((t) => isSensitiveTable(t.name));
280
+ state.dbTables = safeTables;
281
+ state.databaseEnabled = true;
282
+ console.log(` ${c("green", "+")} Found ${c("bold", String(result.tables.length))} tables (${safeTables.length} safe, ${hiddenTables.length} hidden)\n`);
283
+ if (hiddenTables.length > 0) {
284
+ console.log(` ${c("dim", " Auto-hidden (sensitive):")} ${hiddenTables.map((t) => t.name).join(", ")}`);
285
+ }
286
+ console.log(`\n ${c("dim", " Available tables:")}`);
287
+ for (const table of safeTables) {
288
+ const colCount = table.columns.length;
289
+ const pk = table.columns.find((col) => col.isPrimary);
290
+ console.log(` ${c("magenta", table.name)} ${c("dim", `(${colCount} columns${pk ? `, pk: ${pk.name}` : ""})`)}`);
291
+ state.tools.push({
292
+ name: `list_${table.name}`,
293
+ description: `List all ${formatTableName(table.name)} records with filtering and pagination`,
294
+ method: "QUERY",
295
+ path: `db://${table.name}`,
296
+ source: "database",
297
+ enabled: true,
298
+ tableName: table.name,
299
+ });
300
+ if (pk) {
301
+ state.tools.push({
302
+ name: `get_${table.name}_by_${pk.name}`,
303
+ description: `Get a specific ${formatTableName(table.name)} record by ${pk.name}`,
304
+ method: "QUERY",
305
+ path: `db://${table.name}/${pk.name}`,
306
+ source: "database",
307
+ enabled: true,
308
+ tableName: table.name,
309
+ });
310
+ }
311
+ }
312
+ }
313
+ catch (err) {
314
+ const msg = err instanceof Error ? err.message : String(err);
315
+ console.log(` ${c("yellow", "!")} Database scan failed: ${msg}`);
316
+ state.databaseEnabled = false;
317
+ }
318
+ }
319
+ async function stepEnrichAndReview(state, prompt) {
320
+ printStep(4, 5, "AI enrichment & tool review");
321
+ if (state.tools.length === 0) {
322
+ console.log(` ${c("dim", " No tools discovered. Routes will be discovered at runtime.")}`);
323
+ state.enrichmentEnabled = true;
324
+ return;
325
+ }
326
+ const wantEnrich = await prompt.confirm(` Use AI to improve tool names and descriptions?`);
327
+ if (wantEnrich) {
328
+ console.log(` ${c("dim", " Contacting enrichment service...")}`);
329
+ try {
330
+ const { enrichTools } = await import("./enrichment.js");
331
+ const toolSummaries = state.tools.map((t) => ({
332
+ name: t.name,
333
+ description: t.description,
334
+ method: t.method === "QUERY" ? "DB_QUERY" : t.method,
335
+ path: t.path,
336
+ inputSchema: {},
337
+ params: [],
338
+ }));
339
+ const enriched = await enrichTools(toolSummaries);
340
+ let enrichCount = 0;
341
+ for (let i = 0; i < state.tools.length; i++) {
342
+ if (enriched[i]) {
343
+ if (enriched[i].name)
344
+ state.tools[i].name = enriched[i].name;
345
+ if (enriched[i].description)
346
+ state.tools[i].description = enriched[i].description;
347
+ enrichCount++;
348
+ }
349
+ }
350
+ console.log(` ${c("green", "+")} Enhanced ${enrichCount} tool descriptions.`);
351
+ state.enrichmentEnabled = true;
352
+ }
353
+ catch {
354
+ console.log(` ${c("yellow", "!")} Enrichment service unavailable. Using auto-generated names.`);
355
+ state.enrichmentEnabled = false;
356
+ }
357
+ }
358
+ else {
359
+ state.enrichmentEnabled = false;
360
+ }
361
+ console.log(`\n ${c("bold", "Discovered tools:")}`);
362
+ printToolTable(state.tools);
363
+ console.log();
364
+ const reviewMode = await prompt.ask(` Enable all tools, or review individually? ${c("dim", "(all/review)")} `);
365
+ if (reviewMode.toLowerCase() === "review" || reviewMode.toLowerCase() === "r") {
366
+ console.log();
367
+ for (let i = 0; i < state.tools.length; i++) {
368
+ const t = state.tools[i];
369
+ const source = t.source === "route" ? c("yellow", t.method) : c("magenta", "DB");
370
+ const enabled = await prompt.confirm(` ${source} ${c("bold", t.name)} ${c("dim", `- ${t.description}`)}\n Enable this tool?`);
371
+ state.tools[i].enabled = enabled;
372
+ }
373
+ const enabledCount = state.tools.filter((t) => t.enabled).length;
374
+ console.log(`\n ${c("green", "+")} ${enabledCount} of ${state.tools.length} tools enabled.`);
375
+ }
376
+ else {
377
+ console.log(` ${c("green", "+")} All ${state.tools.length} tools enabled.`);
378
+ }
379
+ if (state.databaseEnabled) {
380
+ const wantWrites = await prompt.confirm(`\n Enable write operations (create/insert) for database tables?`, false);
381
+ state.includeWrites = wantWrites;
382
+ }
383
+ }
384
+ async function stepGenerate(state) {
385
+ printStep(5, 5, "Generating MCP server");
386
+ const content = fs.readFileSync(state.entryFile, "utf-8");
387
+ const importLine = state.isESM ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
388
+ const disabledRoutePaths = state.tools
389
+ .filter((t) => t.source === "route" && !t.enabled)
390
+ .map((t) => t.path);
391
+ const enabledRoutePaths = new Set(state.tools
392
+ .filter((t) => t.source === "route" && t.enabled)
393
+ .map((t) => t.path));
394
+ const disabledRoutes = disabledRoutePaths.filter((p) => !enabledRoutePaths.has(p));
395
+ const disabledTables = new Set();
396
+ const dbTools = state.tools.filter((t) => t.source === "database");
397
+ const tableNames = [...new Set(dbTools.map((t) => t.tableName))];
398
+ for (const tableName of tableNames) {
399
+ const tableTools = dbTools.filter((t) => t.tableName === tableName);
400
+ const allDisabled = tableTools.every((t) => !t.enabled);
401
+ if (allDisabled)
402
+ disabledTables.add(tableName);
403
+ }
404
+ const configParts = [];
405
+ configParts.push(`app: ${state.appVarName}`);
406
+ if (disabledRoutes.length > 0) {
407
+ configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
408
+ }
409
+ if (!state.databaseEnabled) {
410
+ configParts.push(`database: false`);
411
+ }
412
+ else {
413
+ if (disabledTables.size > 0) {
414
+ configParts.push(`excludeTables: ${JSON.stringify([...disabledTables])}`);
415
+ }
416
+ if (state.includeWrites) {
417
+ configParts.push(`includeWrites: true`);
418
+ }
419
+ }
420
+ if (!state.enrichmentEnabled) {
421
+ configParts.push(`enrichment: false`);
422
+ }
423
+ let setupCode;
424
+ if (configParts.length <= 2) {
425
+ setupCode = `createMcpServer({ ${configParts.join(", ")} });`;
426
+ }
427
+ else {
428
+ const indent = " ";
429
+ setupCode = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
430
+ }
93
431
  const lines = content.split("\n");
94
432
  let lastImportIndex = -1;
95
433
  for (let i = 0; i < lines.length; i++) {
@@ -97,8 +435,7 @@ function injectMcpSetup(filePath, root) {
97
435
  lastImportIndex = i;
98
436
  }
99
437
  }
100
- const insertImportAt = lastImportIndex + 1;
101
- lines.splice(insertImportAt, 0, importLine);
438
+ lines.splice(lastImportIndex + 1, 0, importLine);
102
439
  let listenIndex = -1;
103
440
  for (let i = lines.length - 1; i >= 0; i--) {
104
441
  if (LISTEN_PATTERN.test(lines[i])) {
@@ -106,72 +443,140 @@ function injectMcpSetup(filePath, root) {
106
443
  break;
107
444
  }
108
445
  }
446
+ const commentLine = `// MCP server - exposes your API to AI assistants (Claude, ChatGPT, Cursor)`;
109
447
  if (listenIndex >= 0) {
110
- lines.splice(listenIndex, 0, "", `// MCP server - auto-discovered from your Express routes and database`, setupCode);
448
+ lines.splice(listenIndex, 0, "", commentLine, setupCode);
111
449
  }
112
450
  else {
113
- lines.push("");
114
- lines.push(`// MCP server - auto-discovered from your Express routes and database`);
115
- lines.push(setupCode);
451
+ lines.push("", commentLine, setupCode);
116
452
  }
117
- fs.writeFileSync(filePath, lines.join("\n"));
118
- return {
119
- success: true,
120
- message: `Added MCP setup to ${path.relative(root, filePath)}`,
453
+ fs.writeFileSync(state.entryFile, lines.join("\n"));
454
+ const relPath = path.relative(state.root, state.entryFile);
455
+ const enabledCount = state.tools.filter((t) => t.enabled).length;
456
+ console.log(` ${c("green", "+")} Updated ${c("bold", relPath)}`);
457
+ console.log();
458
+ console.log(` ${c("dim", " Configuration:")}`);
459
+ console.log(` Tools enabled: ${c("bold", String(enabledCount))}`);
460
+ console.log(` Database: ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
461
+ console.log(` AI enrichment: ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
462
+ console.log(` Write operations: ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
463
+ console.log();
464
+ console.log(` ${c("bold", "Next steps:")}`);
465
+ console.log(` 1. Restart your app`);
466
+ console.log(` 2. Visit ${c("cyan", "http://localhost:3000/mcp")} to see your tools`);
467
+ console.log(` 3. Connect Claude, ChatGPT, or Cursor to your /mcp endpoint`);
468
+ console.log();
469
+ console.log(` ${c("dim", "Full guide:")} ${c("cyan", "https://mcp-skill-scanner.replit.app/guide")}`);
470
+ console.log();
471
+ }
472
+ function generateToolName(method, routePath) {
473
+ const segments = routePath
474
+ .replace(/^\/api\/?/, "")
475
+ .split("/")
476
+ .filter((s) => s && !s.startsWith(":"));
477
+ const resource = segments.join("_") || "root";
478
+ const paramSegments = routePath.split("/").filter((s) => s.startsWith(":"));
479
+ switch (method.toUpperCase()) {
480
+ case "GET":
481
+ return paramSegments.length > 0
482
+ ? `get_${resource}_by_${paramSegments[0].slice(1)}`
483
+ : `list_${resource}`;
484
+ case "POST":
485
+ return `create_${resource}`;
486
+ case "PUT":
487
+ case "PATCH":
488
+ return `update_${resource}`;
489
+ case "DELETE":
490
+ return `delete_${resource}`;
491
+ default:
492
+ return `${method.toLowerCase()}_${resource}`;
493
+ }
494
+ }
495
+ function generateToolDescription(method, routePath) {
496
+ const segments = routePath
497
+ .replace(/^\/api\/?/, "")
498
+ .split("/")
499
+ .filter((s) => s && !s.startsWith(":"));
500
+ const resource = segments.map((s) => s.replace(/[-_]/g, " ")).join(" ") || "resource";
501
+ const paramSegments = routePath.split("/").filter((s) => s.startsWith(":"));
502
+ switch (method.toUpperCase()) {
503
+ case "GET":
504
+ return paramSegments.length > 0
505
+ ? `Get ${resource} by ${paramSegments[0].slice(1)}`
506
+ : `List all ${resource}`;
507
+ case "POST":
508
+ return `Create a new ${resource}`;
509
+ case "PUT":
510
+ case "PATCH":
511
+ return `Update ${resource}`;
512
+ case "DELETE":
513
+ return `Delete ${resource}`;
514
+ default:
515
+ return `${method} ${resource}`;
516
+ }
517
+ }
518
+ function formatTableName(name) {
519
+ return name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
520
+ }
521
+ async function runWizard(specifiedFile) {
522
+ printHeader();
523
+ const prompt = createPrompt();
524
+ const state = {
525
+ root: findProjectRoot(),
526
+ entryFile: "",
527
+ appVarName: "app",
528
+ isESM: false,
529
+ routePrefix: "/api",
530
+ routes: [],
531
+ dbTables: [],
532
+ tools: [],
533
+ databaseEnabled: true,
534
+ enrichmentEnabled: true,
535
+ includeWrites: false,
121
536
  };
537
+ try {
538
+ const found = await stepDetectApp(state, prompt, specifiedFile);
539
+ if (!found) {
540
+ prompt.close();
541
+ return;
542
+ }
543
+ await stepScanRoutes(state);
544
+ await stepScanDatabase(state, prompt);
545
+ await stepEnrichAndReview(state, prompt);
546
+ await stepGenerate(state);
547
+ }
548
+ catch (err) {
549
+ if (err instanceof Error && err.message.includes("readline was closed")) {
550
+ console.log(`\n ${c("dim", "Setup cancelled.")}\n`);
551
+ }
552
+ else {
553
+ throw err;
554
+ }
555
+ }
556
+ finally {
557
+ prompt.close();
558
+ }
122
559
  }
123
560
  function main() {
124
561
  const args = process.argv.slice(2);
125
562
  if (args[0] !== "init") {
126
563
  console.log(`
127
- @hellocrossman/mcp-sdk CLI
564
+ ${c("bold", "@hellocrossman/mcp-sdk")}
128
565
 
129
566
  Usage:
130
- npx @hellocrossman/mcp-sdk init Auto-detect Express app and add MCP server
567
+ npx @hellocrossman/mcp-sdk init Interactive setup wizard
131
568
 
132
569
  Options:
133
570
  --file <path> Specify the Express app file manually
134
571
  `);
135
572
  return;
136
573
  }
137
- console.log("\n @hellocrossman/mcp-sdk\n");
138
- console.log(" Setting up MCP server...\n");
139
- const root = findProjectRoot();
140
574
  const fileArgIndex = args.indexOf("--file");
141
- let entryFile = null;
142
- if (fileArgIndex >= 0 && args[fileArgIndex + 1]) {
143
- const specified = path.resolve(root, args[fileArgIndex + 1]);
144
- if (fs.existsSync(specified)) {
145
- entryFile = specified;
146
- }
147
- else {
148
- console.error(` Error: File not found: ${args[fileArgIndex + 1]}`);
149
- process.exit(1);
150
- }
151
- }
152
- else {
153
- entryFile = findExpressEntryFile(root);
154
- }
155
- if (!entryFile) {
156
- console.error(" Could not find an Express app file.");
157
- console.error(" Try: npx @hellocrossman/mcp-sdk init --file server/index.ts\n");
158
- process.exit(1);
159
- }
160
- console.log(` Found Express app: ${path.relative(root, entryFile)}`);
161
- const result = injectMcpSetup(entryFile, root);
162
- if (result.success) {
163
- console.log(` ${result.message}`);
164
- console.log("");
165
- console.log(" Your MCP server will be available at /mcp when you start your app.");
166
- console.log(" It auto-discovers Express routes and database tables.");
167
- console.log(" Sensitive tables (users, sessions, etc.) are hidden by default.");
168
- console.log("");
169
- console.log(" Restart your app to see it in action.\n");
170
- }
171
- else {
172
- console.error(` Error: ${result.message}\n`);
575
+ const specifiedFile = fileArgIndex >= 0 ? args[fileArgIndex + 1] : undefined;
576
+ runWizard(specifiedFile).catch((err) => {
577
+ console.error(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`);
173
578
  process.exit(1);
174
- }
579
+ });
175
580
  }
176
581
  main();
177
582
  //# sourceMappingURL=cli.js.map