@hellocrossman/mcp-sdk 0.3.5 → 0.4.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/cjs/cli.js CHANGED
@@ -175,8 +175,29 @@ function checkIsESM(filePath, root) {
175
175
  return false;
176
176
  }
177
177
  }
178
- function alreadySetup(content) {
179
- return content.includes("createMcpServer") || content.includes("@hellocrossman/mcp-sdk");
178
+ function alreadySetup(root) {
179
+ const mcpServerFile = path_1.default.join(root, "mcp-server.ts");
180
+ const mcpServerFileJs = path_1.default.join(root, "mcp-server.js");
181
+ if (fs_1.default.existsSync(mcpServerFile) || fs_1.default.existsSync(mcpServerFileJs))
182
+ return true;
183
+ const serverMcpFile = path_1.default.join(root, "server", "mcp-server.ts");
184
+ const serverMcpFileJs = path_1.default.join(root, "server", "mcp-server.js");
185
+ if (fs_1.default.existsSync(serverMcpFile) || fs_1.default.existsSync(serverMcpFileJs))
186
+ return true;
187
+ return false;
188
+ }
189
+ function findExistingMcpServerFile(root) {
190
+ const candidates = [
191
+ path_1.default.join(root, "mcp-server.ts"),
192
+ path_1.default.join(root, "mcp-server.js"),
193
+ path_1.default.join(root, "server", "mcp-server.ts"),
194
+ path_1.default.join(root, "server", "mcp-server.js"),
195
+ ];
196
+ for (const f of candidates) {
197
+ if (fs_1.default.existsSync(f))
198
+ return f;
199
+ }
200
+ return null;
180
201
  }
181
202
  function checkPgInstalled(root) {
182
203
  return fs_1.default.existsSync(path_1.default.join(root, "node_modules", "pg"));
@@ -243,13 +264,81 @@ function stripExistingMcpConfig(content) {
243
264
  result = result.replace(/\n{3,}/g, "\n\n");
244
265
  return result;
245
266
  }
267
+ function findAppExport(content, varName) {
268
+ const namedExportRe = new RegExp(`export\\s+(?:const|let|var)\\s+${varName}\\s*=`);
269
+ if (namedExportRe.test(content))
270
+ return varName;
271
+ if (/export\s+default\s+app\b/.test(content))
272
+ return "default";
273
+ const moduleExportsRe = new RegExp(`module\\.exports\\s*=\\s*${varName}`);
274
+ if (moduleExportsRe.test(content))
275
+ return "default";
276
+ const exportsRe = new RegExp(`module\\.exports\\.${varName}\\s*=`);
277
+ if (exportsRe.test(content))
278
+ return varName;
279
+ return varName;
280
+ }
281
+ function findEntryFiles(root, appFile) {
282
+ const entries = [];
283
+ const appFileRel = path_1.default.relative(root, appFile);
284
+ const appBaseName = path_1.default.basename(appFile, path_1.default.extname(appFile));
285
+ const appDir = path_1.default.dirname(appFile);
286
+ const searchDirs = [appDir, root, path_1.default.join(root, "server"), path_1.default.join(root, "src")];
287
+ const seen = new Set();
288
+ for (const dir of searchDirs) {
289
+ if (!fs_1.default.existsSync(dir) || !fs_1.default.statSync(dir).isDirectory())
290
+ continue;
291
+ const files = fs_1.default.readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f));
292
+ for (const file of files) {
293
+ const fullPath = path_1.default.join(dir, file);
294
+ if (seen.has(fullPath) || fullPath === appFile)
295
+ continue;
296
+ seen.add(fullPath);
297
+ try {
298
+ const content = fs_1.default.readFileSync(fullPath, "utf-8");
299
+ const hasListen = LISTEN_PATTERN.test(content);
300
+ const importsApp = content.includes(`./${appBaseName}`) ||
301
+ content.includes(`'./${appFileRel}'`) ||
302
+ content.includes(`"./${appFileRel}"`) ||
303
+ content.includes(`from './${appBaseName}'`) ||
304
+ content.includes(`from "./${appBaseName}"`) ||
305
+ content.includes(`require('./${appBaseName}')`) ||
306
+ content.includes(`require("./${appBaseName}")`);
307
+ if (hasListen || importsApp) {
308
+ entries.push(fullPath);
309
+ }
310
+ }
311
+ catch { }
312
+ }
313
+ }
314
+ if (entries.length === 0) {
315
+ entries.push(appFile);
316
+ }
317
+ return entries;
318
+ }
246
319
  async function stepDetectApp(state, prompt, specifiedFile, force) {
247
- printStep(1, 5, "Detecting Express app");
248
- let entryFile = null;
320
+ printStep(1, 7, "Detecting Express app");
321
+ if (alreadySetup(state.root)) {
322
+ if (force) {
323
+ const existingFile = findExistingMcpServerFile(state.root);
324
+ if (existingFile) {
325
+ console.log(` ${c("yellow", "~")} Found existing ${c("bold", path_1.default.relative(state.root, existingFile))}`);
326
+ console.log(` ${c("dim", " Will regenerate with fresh scan...")}`);
327
+ }
328
+ }
329
+ else {
330
+ const existingFile = findExistingMcpServerFile(state.root);
331
+ const relPath = existingFile ? path_1.default.relative(state.root, existingFile) : "mcp-server.ts";
332
+ console.log(` ${c("green", "+")} MCP SDK is already set up (${c("bold", relPath)})`);
333
+ console.log(` ${c("dim", " Run with")} ${c("cyan", "--force")} ${c("dim", "to re-scan and reconfigure.")}`);
334
+ return false;
335
+ }
336
+ }
337
+ let appFile = null;
249
338
  if (specifiedFile) {
250
339
  const resolved = path_1.default.resolve(state.root, specifiedFile);
251
340
  if (fs_1.default.existsSync(resolved)) {
252
- entryFile = resolved;
341
+ appFile = resolved;
253
342
  }
254
343
  else {
255
344
  console.log(` ${c("yellow", "!")} File not found: ${specifiedFile}`);
@@ -257,11 +346,11 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
257
346
  }
258
347
  }
259
348
  else {
260
- entryFile = findExpressEntryFile(state.root);
349
+ appFile = findExpressEntryFile(state.root);
261
350
  }
262
- if (entryFile && !specifiedFile) {
263
- const relPath = path_1.default.relative(state.root, entryFile);
264
- const content = fs_1.default.readFileSync(entryFile, "utf-8");
351
+ if (appFile && !specifiedFile) {
352
+ const relPath = path_1.default.relative(state.root, appFile);
353
+ const content = fs_1.default.readFileSync(appFile, "utf-8");
265
354
  const hasCreation = APP_VAR_PATTERN.test(content);
266
355
  const hasListen = LISTEN_PATTERN.test(content);
267
356
  const signals = [];
@@ -277,7 +366,7 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
277
366
  if (custom) {
278
367
  const resolved = path_1.default.resolve(state.root, custom);
279
368
  if (fs_1.default.existsSync(resolved)) {
280
- entryFile = resolved;
369
+ appFile = resolved;
281
370
  }
282
371
  else {
283
372
  console.log(` ${c("yellow", "!")} File not found: ${custom}`);
@@ -289,13 +378,13 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
289
378
  }
290
379
  }
291
380
  }
292
- if (!entryFile) {
381
+ if (!appFile) {
293
382
  console.log(` ${c("yellow", "!")} Could not find an Express app file.`);
294
383
  const custom = await prompt.ask(` Enter the path to your Express app file: `);
295
384
  if (custom) {
296
385
  const resolved = path_1.default.resolve(state.root, custom);
297
386
  if (fs_1.default.existsSync(resolved)) {
298
- entryFile = resolved;
387
+ appFile = resolved;
299
388
  }
300
389
  else {
301
390
  console.log(` ${c("yellow", "!")} File not found: ${custom}`);
@@ -306,31 +395,23 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
306
395
  return false;
307
396
  }
308
397
  }
309
- let content = fs_1.default.readFileSync(entryFile, "utf-8");
310
- if (alreadySetup(content)) {
311
- if (force) {
312
- console.log(` ${c("yellow", "~")} Found existing MCP config in ${path_1.default.relative(state.root, entryFile)}`);
313
- console.log(` ${c("dim", " Removing old configuration for re-scan...")}`);
314
- content = stripExistingMcpConfig(content);
315
- fs_1.default.writeFileSync(entryFile, content);
316
- console.log(` ${c("green", "+")} Old config removed. Starting fresh.`);
317
- }
318
- else {
319
- console.log(` ${c("green", "+")} MCP SDK is already set up in ${path_1.default.relative(state.root, entryFile)}`);
320
- console.log(` ${c("dim", " Run with")} ${c("cyan", "--force")} ${c("dim", "to re-scan and reconfigure.")}`);
321
- return false;
322
- }
323
- }
398
+ const content = fs_1.default.readFileSync(appFile, "utf-8");
324
399
  const appMatch = content.match(APP_VAR_PATTERN);
325
- state.entryFile = entryFile;
400
+ state.appFile = appFile;
326
401
  state.appVarName = appMatch ? appMatch[1] : "app";
327
- state.isESM = checkIsESM(entryFile, state.root);
402
+ state.appExportName = findAppExport(content, state.appVarName);
403
+ state.isESM = checkIsESM(appFile, state.root);
404
+ const entryFiles = findEntryFiles(state.root, appFile);
405
+ state.entryFiles = entryFiles;
328
406
  console.log(` ${c("dim", ` Variable: ${state.appVarName}, Module: ${state.isESM ? "ESM" : "CommonJS"}`)}`);
407
+ if (entryFiles.length > 0 && entryFiles[0] !== appFile) {
408
+ console.log(` ${c("dim", ` Entry files: ${entryFiles.map((f) => path_1.default.relative(state.root, f)).join(", ")}`)}`);
409
+ }
329
410
  return true;
330
411
  }
331
412
  async function stepScanRoutes(state) {
332
- printStep(2, 5, "Scanning API routes");
333
- state.routes = (0, route_scan_js_1.scanAllRoutes)(state.entryFile, state.routePrefix);
413
+ printStep(2, 7, "Scanning API routes");
414
+ state.routes = (0, route_scan_js_1.scanAllRoutes)(state.appFile, state.routePrefix);
334
415
  if (state.routes.length === 0) {
335
416
  console.log(` ${c("dim", " No routes found under")} ${state.routePrefix}`);
336
417
  console.log(` ${c("dim", " Routes will be discovered at runtime when your app starts.")}`);
@@ -355,7 +436,7 @@ async function stepScanRoutes(state) {
355
436
  }
356
437
  }
357
438
  async function stepScanDatabase(state, prompt) {
358
- printStep(3, 5, "Database discovery");
439
+ printStep(3, 7, "Database discovery");
359
440
  const wantDb = await prompt.confirm(` Scan your database for tables to expose as tools?`);
360
441
  if (!wantDb) {
361
442
  state.databaseEnabled = false;
@@ -443,7 +524,7 @@ async function stepScanDatabase(state, prompt) {
443
524
  }
444
525
  }
445
526
  async function stepEnrichAndReview(state, prompt) {
446
- printStep(4, 5, "AI enrichment & tool review");
527
+ printStep(4, 7, "AI enrichment & tool review");
447
528
  if (state.tools.length === 0) {
448
529
  console.log(` ${c("dim", " No tools discovered. Routes will be discovered at runtime.")}`);
449
530
  state.enrichmentEnabled = true;
@@ -507,10 +588,477 @@ async function stepEnrichAndReview(state, prompt) {
507
588
  state.includeWrites = wantWrites;
508
589
  }
509
590
  }
591
+ async function stepCustomTools(state, prompt) {
592
+ printStep(5, 7, "Custom tools (optional)");
593
+ console.log(` ${c("dim", "Auto-discovered tools give broad coverage.")}`);
594
+ console.log(` ${c("dim", "Custom tools let you define purpose-built queries")}`);
595
+ console.log(` ${c("dim", "for specific business questions.\n")}`);
596
+ const wantCustom = await prompt.confirm(` Define custom tools?`, false);
597
+ if (!wantCustom) {
598
+ console.log(` ${c("dim", " Skipping custom tools.")}`);
599
+ return;
600
+ }
601
+ const availableRoutes = state.routes.map((r) => `${r.method} ${r.path}`);
602
+ const availableTables = state.dbTables.map((t) => t.name);
603
+ while (true) {
604
+ console.log();
605
+ const name = await prompt.ask(` ${c("cyan", "Tool name")} ${c("dim", "(e.g. get_traffic_overview):")} `);
606
+ if (!name)
607
+ break;
608
+ const toolName = name.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
609
+ const description = await prompt.ask(` ${c("cyan", "Description")} ${c("dim", "(what does this tool do?):")} `);
610
+ if (!description)
611
+ break;
612
+ const params = [];
613
+ console.log(`\n ${c("dim", "Define input parameters (press Enter with no name to finish):")}`);
614
+ while (true) {
615
+ const paramName = await prompt.ask(` ${c("cyan", "Parameter name:")} `);
616
+ if (!paramName)
617
+ break;
618
+ const paramDesc = await prompt.ask(` ${c("cyan", "Description:")} `);
619
+ console.log(` ${c("dim", "Type: 1) string 2) number 3) boolean")}`);
620
+ const typeChoice = await prompt.ask(` ${c("cyan", "Type")} ${c("dim", "(1/2/3, default: 1):")} `);
621
+ const paramType = typeChoice === "2" ? "number" : typeChoice === "3" ? "boolean" : "string";
622
+ const isRequired = await prompt.confirm(` Required?`);
623
+ params.push({ name: paramName, type: paramType, description: paramDesc || paramName, required: isRequired });
624
+ console.log(` ${c("green", "+")} Added parameter: ${c("bold", paramName)} (${paramType})`);
625
+ }
626
+ console.log(`\n ${c("dim", "How should this tool get its data?")}`);
627
+ console.log(` ${c("cyan", "1.")} Call an existing API route`);
628
+ console.log(` ${c("cyan", "2.")} Run a database query`);
629
+ const sourceChoice = await prompt.ask(` ${c("dim", ">")} `);
630
+ const customTool = {
631
+ name: toolName,
632
+ description,
633
+ params,
634
+ dataSource: sourceChoice === "2" ? "database" : "route",
635
+ };
636
+ if (sourceChoice === "2" && availableTables.length > 0) {
637
+ console.log(`\n ${c("dim", "Available tables:")}`);
638
+ availableTables.forEach((t, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${t}`));
639
+ const tableChoice = await prompt.ask(` ${c("dim", "Table number or name:")} `);
640
+ const tableIndex = parseInt(tableChoice) - 1;
641
+ customTool.tableName = availableTables[tableIndex] || tableChoice;
642
+ const tableInfo = state.dbTables.find((t) => t.name === customTool.tableName);
643
+ if (tableInfo) {
644
+ const scopeColumns = tableInfo.columns
645
+ .filter((col) => /user_id|project_id|account_id|org_id|team_id|owner_id/.test(col.name))
646
+ .map((col) => col.name);
647
+ if (scopeColumns.length > 0) {
648
+ console.log(`\n ${c("dim", "User scoping - filter results by:")}`);
649
+ scopeColumns.forEach((col, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${col}`));
650
+ console.log(` ${c("cyan", `${scopeColumns.length + 1}.`)} No scoping (public data)`);
651
+ const scopeChoice = await prompt.ask(` ${c("dim", ">")} `);
652
+ const scopeIndex = parseInt(scopeChoice) - 1;
653
+ if (scopeIndex >= 0 && scopeIndex < scopeColumns.length) {
654
+ customTool.scopeColumn = scopeColumns[scopeIndex];
655
+ }
656
+ }
657
+ }
658
+ }
659
+ else if (sourceChoice !== "2" && availableRoutes.length > 0) {
660
+ console.log(`\n ${c("dim", "Available routes:")}`);
661
+ availableRoutes.forEach((r, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${r}`));
662
+ const routeChoice = await prompt.ask(` ${c("dim", "Route number:")} `);
663
+ const routeIndex = parseInt(routeChoice) - 1;
664
+ if (routeIndex >= 0 && routeIndex < state.routes.length) {
665
+ customTool.routeMethod = state.routes[routeIndex].method;
666
+ customTool.routePath = state.routes[routeIndex].path;
667
+ }
668
+ }
669
+ state.customTools.push(customTool);
670
+ console.log(`\n ${c("green", "+")} Added custom tool: ${c("bold", toolName)}`);
671
+ const addMore = await prompt.confirm(`\n Add another custom tool?`, false);
672
+ if (!addMore)
673
+ break;
674
+ }
675
+ if (state.customTools.length > 0) {
676
+ console.log(`\n ${c("green", "+")} ${state.customTools.length} custom tool${state.customTools.length === 1 ? "" : "s"} defined.`);
677
+ }
678
+ }
679
+ const AUTH_PROVIDERS = [
680
+ {
681
+ id: "supabase",
682
+ label: "Supabase",
683
+ packages: ["@supabase/supabase-js", "@supabase/ssr", "@supabase/auth-helpers-express"],
684
+ filePatterns: [/supabase\.auth/, /createClient.*supabase/],
685
+ },
686
+ {
687
+ id: "clerk",
688
+ label: "Clerk",
689
+ packages: ["@clerk/clerk-sdk-node", "@clerk/express", "@clerk/nextjs", "@clerk/backend"],
690
+ filePatterns: [/clerkMiddleware/, /requireAuth/, /clerkClient/],
691
+ },
692
+ {
693
+ id: "auth0",
694
+ label: "Auth0",
695
+ packages: ["express-oauth2-jwt-bearer", "auth0", "@auth0/nextjs-auth0", "express-openid-connect"],
696
+ filePatterns: [/auth0/, /express-oauth2-jwt-bearer/],
697
+ },
698
+ {
699
+ id: "jwt",
700
+ label: "Custom JWT",
701
+ packages: ["jsonwebtoken", "jose"],
702
+ filePatterns: [/jwt\.verify/, /jwt\.sign/, /jose.*jwtVerify/],
703
+ },
704
+ ];
705
+ function detectAuthProvider(root) {
706
+ let pkgDeps = {};
707
+ try {
708
+ const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, "package.json"), "utf-8"));
709
+ pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
710
+ }
711
+ catch {
712
+ return null;
713
+ }
714
+ for (const provider of AUTH_PROVIDERS) {
715
+ const hasPackage = provider.packages.some((p) => p in pkgDeps);
716
+ if (hasPackage) {
717
+ return { provider: provider.id, confidence: "high" };
718
+ }
719
+ }
720
+ const srcDirs = ["server", "src", "lib", "."];
721
+ for (const dir of srcDirs) {
722
+ const dirPath = path_1.default.join(root, dir);
723
+ if (!fs_1.default.existsSync(dirPath) || !fs_1.default.statSync(dirPath).isDirectory())
724
+ continue;
725
+ try {
726
+ const files = fs_1.default.readdirSync(dirPath).filter((f) => /\.(ts|js|mjs)$/.test(f));
727
+ for (const file of files) {
728
+ const content = fs_1.default.readFileSync(path_1.default.join(dirPath, file), "utf-8");
729
+ for (const provider of AUTH_PROVIDERS) {
730
+ if (provider.filePatterns.some((p) => p.test(content))) {
731
+ return { provider: provider.id, confidence: "medium" };
732
+ }
733
+ }
734
+ }
735
+ }
736
+ catch { }
737
+ }
738
+ return null;
739
+ }
740
+ async function stepAuth(state, prompt) {
741
+ printStep(6, 7, "Authentication");
742
+ console.log(` ${c("dim", "MCP clients like Claude Desktop require OAuth for remote servers.")}`);
743
+ console.log(` ${c("dim", "The SDK can generate the OAuth bridge using your existing auth.\n")}`);
744
+ const detected = detectAuthProvider(state.root);
745
+ let selectedProvider;
746
+ if (detected) {
747
+ const providerLabel = AUTH_PROVIDERS.find((p) => p.id === detected.provider)?.label || detected.provider;
748
+ const confidence = detected.confidence === "high" ? "found in dependencies" : "detected in code";
749
+ console.log(` ${c("green", "+")} Detected: ${c("bold", providerLabel)} ${c("dim", `(${confidence})`)}`);
750
+ console.log();
751
+ const useDetected = await prompt.confirm(` Use ${providerLabel} for MCP authentication?`);
752
+ if (useDetected) {
753
+ selectedProvider = detected.provider;
754
+ }
755
+ else {
756
+ selectedProvider = await promptAuthChoice(prompt);
757
+ }
758
+ }
759
+ else {
760
+ console.log(` ${c("dim", " No auth provider auto-detected.")}`);
761
+ console.log();
762
+ selectedProvider = await promptAuthChoice(prompt);
763
+ }
764
+ state.authProvider = selectedProvider;
765
+ if (selectedProvider === "none") {
766
+ console.log(`\n ${c("dim", " Skipping auth. MCP endpoint will be open (good for local dev).")}`);
767
+ }
768
+ else {
769
+ const label = AUTH_PROVIDERS.find((p) => p.id === selectedProvider)?.label || selectedProvider;
770
+ console.log(`\n ${c("green", "+")} Auth configured: ${c("bold", label)}`);
771
+ if (selectedProvider === "supabase") {
772
+ console.log(` ${c("dim", " Requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY env vars.")}`);
773
+ }
774
+ else if (selectedProvider === "clerk") {
775
+ console.log(` ${c("dim", " Requires CLERK_SECRET_KEY env var.")}`);
776
+ }
777
+ else if (selectedProvider === "auth0") {
778
+ console.log(` ${c("dim", " Requires AUTH0_ISSUER_BASE_URL and AUTH0_AUDIENCE env vars.")}`);
779
+ }
780
+ else if (selectedProvider === "jwt") {
781
+ console.log(` ${c("dim", " Requires JWT_SECRET env var.")}`);
782
+ }
783
+ else if (selectedProvider === "apikey") {
784
+ console.log(` ${c("dim", " Requires MCP_API_KEY env var.")}`);
785
+ }
786
+ }
787
+ }
788
+ async function promptAuthChoice(prompt) {
789
+ console.log(` Which auth provider do you use?`);
790
+ console.log();
791
+ console.log(` ${c("cyan", "1.")} Supabase`);
792
+ console.log(` ${c("cyan", "2.")} Clerk`);
793
+ console.log(` ${c("cyan", "3.")} Auth0`);
794
+ console.log(` ${c("cyan", "4.")} Custom JWT (jsonwebtoken, jose)`);
795
+ console.log(` ${c("cyan", "5.")} API keys (simple Bearer token)`);
796
+ console.log(` ${c("cyan", "6.")} None / skip auth`);
797
+ console.log();
798
+ const choice = await prompt.ask(` ${c("dim", "Enter 1-6:")} `);
799
+ switch (choice) {
800
+ case "1": return "supabase";
801
+ case "2": return "clerk";
802
+ case "3": return "auth0";
803
+ case "4": return "jwt";
804
+ case "5": return "apikey";
805
+ case "6":
806
+ default: return "none";
807
+ }
808
+ }
809
+ function generateAuthCode(state) {
810
+ const lines = [];
811
+ switch (state.authProvider) {
812
+ case "supabase":
813
+ if (state.isESM) {
814
+ lines.push(`import { createClient } from '@supabase/supabase-js';`);
815
+ }
816
+ else {
817
+ lines.push(`const { createClient } = require('@supabase/supabase-js');`);
818
+ }
819
+ lines.push(``);
820
+ lines.push(`const supabase = createClient(`);
821
+ lines.push(` process.env.SUPABASE_URL!,`);
822
+ lines.push(` process.env.SUPABASE_SERVICE_ROLE_KEY!`);
823
+ lines.push(`);`);
824
+ lines.push(``);
825
+ lines.push(`const mcpAuth = {`);
826
+ lines.push(` async verifyToken(token: string) {`);
827
+ lines.push(` const { data: { user }, error } = await supabase.auth.getUser(token);`);
828
+ lines.push(` if (error || !user) return null;`);
829
+ lines.push(` return { userId: user.id, email: user.email };`);
830
+ lines.push(` },`);
831
+ lines.push(` issuer: process.env.SUPABASE_URL,`);
832
+ lines.push(`};`);
833
+ break;
834
+ case "clerk":
835
+ if (state.isESM) {
836
+ lines.push(`import { createClerkClient } from '@clerk/backend';`);
837
+ }
838
+ else {
839
+ lines.push(`const { createClerkClient } = require('@clerk/backend');`);
840
+ }
841
+ lines.push(``);
842
+ lines.push(`const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! });`);
843
+ lines.push(``);
844
+ lines.push(`const mcpAuth = {`);
845
+ lines.push(` async verifyToken(token: string) {`);
846
+ lines.push(` try {`);
847
+ lines.push(` const session = await clerk.sessions.verifySession(token, token);`);
848
+ lines.push(` return { userId: session.userId };`);
849
+ lines.push(` } catch {`);
850
+ lines.push(` return null;`);
851
+ lines.push(` }`);
852
+ lines.push(` },`);
853
+ lines.push(`};`);
854
+ break;
855
+ case "auth0":
856
+ if (state.isESM) {
857
+ lines.push(`import { createRemoteJWKSet, jwtVerify } from 'jose';`);
858
+ }
859
+ else {
860
+ lines.push(`const { createRemoteJWKSet, jwtVerify } = require('jose');`);
861
+ }
862
+ lines.push(``);
863
+ lines.push(`const AUTH0_DOMAIN = process.env.AUTH0_ISSUER_BASE_URL!;`);
864
+ lines.push(`const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE || '';`);
865
+ lines.push(`const JWKS = createRemoteJWKSet(new URL(\`\${AUTH0_DOMAIN}.well-known/jwks.json\`));`);
866
+ lines.push(``);
867
+ lines.push(`const mcpAuth = {`);
868
+ lines.push(` async verifyToken(token: string) {`);
869
+ lines.push(` try {`);
870
+ lines.push(` const { payload } = await jwtVerify(token, JWKS, {`);
871
+ lines.push(` issuer: AUTH0_DOMAIN,`);
872
+ lines.push(` audience: AUTH0_AUDIENCE || undefined,`);
873
+ lines.push(` });`);
874
+ lines.push(` if (!payload.sub) return null;`);
875
+ lines.push(` return { userId: payload.sub };`);
876
+ lines.push(` } catch {`);
877
+ lines.push(` return null;`);
878
+ lines.push(` }`);
879
+ lines.push(` },`);
880
+ lines.push(` issuer: process.env.AUTH0_ISSUER_BASE_URL,`);
881
+ lines.push(`};`);
882
+ break;
883
+ case "jwt":
884
+ if (state.isESM) {
885
+ lines.push(`import jwt from 'jsonwebtoken';`);
886
+ }
887
+ else {
888
+ lines.push(`const jwt = require('jsonwebtoken');`);
889
+ }
890
+ lines.push(``);
891
+ lines.push(`const mcpAuth = {`);
892
+ lines.push(` async verifyToken(token: string) {`);
893
+ lines.push(` try {`);
894
+ lines.push(` const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;`);
895
+ lines.push(` return { userId: decoded.sub || decoded.userId || decoded.id };`);
896
+ lines.push(` } catch {`);
897
+ lines.push(` return null;`);
898
+ lines.push(` }`);
899
+ lines.push(` },`);
900
+ lines.push(`};`);
901
+ break;
902
+ case "apikey":
903
+ lines.push(`const mcpAuth = {`);
904
+ lines.push(` async verifyToken(token: string) {`);
905
+ lines.push(` if (token === process.env.MCP_API_KEY) {`);
906
+ lines.push(` return { userId: 'api-key-user' };`);
907
+ lines.push(` }`);
908
+ lines.push(` return null;`);
909
+ lines.push(` },`);
910
+ lines.push(`};`);
911
+ break;
912
+ default:
913
+ break;
914
+ }
915
+ return lines;
916
+ }
917
+ function generateCustomToolsFile(state) {
918
+ const lines = [];
919
+ lines.push(`// Custom tools for your MCP server`);
920
+ lines.push(`// Hand-crafted tools that persist across re-scans`);
921
+ lines.push(`// Edit this file to customize tool behavior`);
922
+ lines.push(``);
923
+ if (state.isESM) {
924
+ lines.push(`import type { Express } from 'express';`);
925
+ }
926
+ lines.push(``);
927
+ lines.push(`export interface CustomToolDefinition {`);
928
+ lines.push(` name: string;`);
929
+ lines.push(` description: string;`);
930
+ lines.push(` inputSchema: Record<string, unknown>;`);
931
+ lines.push(` handler: (args: Record<string, unknown>) => Promise<unknown>;`);
932
+ lines.push(`}`);
933
+ lines.push(``);
934
+ lines.push(`export function getCustomTools(app: ${state.isESM ? "Express" : "any"}, dbUrl?: string): CustomToolDefinition[] {`);
935
+ lines.push(` const tools: CustomToolDefinition[] = [];`);
936
+ lines.push(``);
937
+ for (const tool of state.customTools) {
938
+ const properties = [];
939
+ const required = [];
940
+ for (const param of tool.params) {
941
+ properties.push(` ${param.name}: { type: "${param.type}", description: "${param.description.replace(/"/g, '\\"')}" }`);
942
+ if (param.required)
943
+ required.push(`"${param.name}"`);
944
+ }
945
+ lines.push(` tools.push({`);
946
+ lines.push(` name: "${tool.name}",`);
947
+ lines.push(` description: "${tool.description.replace(/"/g, '\\"')}",`);
948
+ lines.push(` inputSchema: {`);
949
+ lines.push(` type: "object",`);
950
+ lines.push(` properties: {`);
951
+ lines.push(properties.join(",\n"));
952
+ lines.push(` },`);
953
+ if (required.length > 0) {
954
+ lines.push(` required: [${required.join(", ")}],`);
955
+ }
956
+ lines.push(` },`);
957
+ if (tool.dataSource === "database" && tool.tableName) {
958
+ lines.push(` handler: async (args) => {`);
959
+ lines.push(` if (!dbUrl) return { error: "No database connection configured" };`);
960
+ lines.push(` const pg = await import("pg");`);
961
+ lines.push(` const client = new pg.default.Client({ connectionString: dbUrl });`);
962
+ lines.push(` try {`);
963
+ lines.push(` await client.connect();`);
964
+ if (tool.scopeColumn) {
965
+ lines.push(` const scopeValue = args.${tool.scopeColumn} as string;`);
966
+ lines.push(` if (!scopeValue) return { error: "${tool.scopeColumn} is required for scoped queries" };`);
967
+ lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} WHERE ${tool.scopeColumn} = $1 LIMIT 100\`, [scopeValue]);`);
968
+ }
969
+ else {
970
+ lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} LIMIT 100\`);`);
971
+ }
972
+ lines.push(` return result.rows;`);
973
+ lines.push(` } finally {`);
974
+ lines.push(` await client.end();`);
975
+ lines.push(` }`);
976
+ lines.push(` },`);
977
+ }
978
+ else if (tool.dataSource === "route" && tool.routePath) {
979
+ lines.push(` handler: async (args) => {`);
980
+ lines.push(` // TODO: Implement route-based handler`);
981
+ lines.push(` // This tool maps to ${tool.routeMethod} ${tool.routePath}`);
982
+ lines.push(` return { message: "Implement this handler to call your API" };`);
983
+ lines.push(` },`);
984
+ }
985
+ else {
986
+ lines.push(` handler: async (args) => {`);
987
+ lines.push(` // TODO: Implement custom handler`);
988
+ lines.push(` return { message: "Implement this handler" };`);
989
+ lines.push(` },`);
990
+ }
991
+ lines.push(` });`);
992
+ lines.push(``);
993
+ }
994
+ lines.push(` return tools;`);
995
+ lines.push(`}`);
996
+ lines.push(``);
997
+ return lines.join("\n");
998
+ }
999
+ function validateGeneratedFiles(mcpServerPath, entryFiles) {
1000
+ const errors = [];
1001
+ const allFiles = [mcpServerPath, ...entryFiles];
1002
+ for (const filePath of allFiles) {
1003
+ if (!fs_1.default.existsSync(filePath))
1004
+ continue;
1005
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
1006
+ const relPath = path_1.default.basename(filePath);
1007
+ let openBraces = 0;
1008
+ let openParens = 0;
1009
+ let openBrackets = 0;
1010
+ let inString = null;
1011
+ let inTemplateString = false;
1012
+ for (let i = 0; i < content.length; i++) {
1013
+ const ch = content[i];
1014
+ const prev = i > 0 ? content[i - 1] : "";
1015
+ if (inString) {
1016
+ if (ch === inString && prev !== "\\")
1017
+ inString = null;
1018
+ continue;
1019
+ }
1020
+ if (inTemplateString) {
1021
+ if (ch === "`" && prev !== "\\")
1022
+ inTemplateString = false;
1023
+ continue;
1024
+ }
1025
+ if (ch === "'" || ch === '"') {
1026
+ inString = ch;
1027
+ continue;
1028
+ }
1029
+ if (ch === "`") {
1030
+ inTemplateString = true;
1031
+ continue;
1032
+ }
1033
+ if (ch === "/" && content[i + 1] === "/") {
1034
+ while (i < content.length && content[i] !== "\n")
1035
+ i++;
1036
+ continue;
1037
+ }
1038
+ if (ch === "{")
1039
+ openBraces++;
1040
+ else if (ch === "}")
1041
+ openBraces--;
1042
+ else if (ch === "(")
1043
+ openParens++;
1044
+ else if (ch === ")")
1045
+ openParens--;
1046
+ else if (ch === "[")
1047
+ openBrackets++;
1048
+ else if (ch === "]")
1049
+ openBrackets--;
1050
+ }
1051
+ if (openBraces !== 0)
1052
+ errors.push(`${relPath}: Unbalanced braces (${openBraces > 0 ? "missing }" : "extra }"})`);
1053
+ if (openParens !== 0)
1054
+ errors.push(`${relPath}: Unbalanced parentheses (${openParens > 0 ? "missing )" : "extra )"})`);
1055
+ if (openBrackets !== 0)
1056
+ errors.push(`${relPath}: Unbalanced brackets (${openBrackets > 0 ? "missing ]" : "extra ]"})`);
1057
+ }
1058
+ return errors;
1059
+ }
510
1060
  async function stepGenerate(state) {
511
- printStep(5, 5, "Generating MCP server");
512
- const content = fs_1.default.readFileSync(state.entryFile, "utf-8");
513
- const importLine = state.isESM ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
1061
+ printStep(7, 7, "Generating MCP server");
514
1062
  const disabledRoutePaths = state.tools
515
1063
  .filter((t) => t.source === "route" && !t.enabled)
516
1064
  .map((t) => t.path);
@@ -528,7 +1076,7 @@ async function stepGenerate(state) {
528
1076
  disabledTables.add(tableName);
529
1077
  }
530
1078
  const configParts = [];
531
- configParts.push(`app: ${state.appVarName}`);
1079
+ configParts.push(`app`);
532
1080
  if (disabledRoutes.length > 0) {
533
1081
  configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
534
1082
  }
@@ -546,38 +1094,133 @@ async function stepGenerate(state) {
546
1094
  if (!state.enrichmentEnabled) {
547
1095
  configParts.push(`enrichment: false`);
548
1096
  }
549
- let setupCode;
1097
+ const appFileDir = path_1.default.dirname(state.appFile);
1098
+ const appFileBase = path_1.default.basename(state.appFile, path_1.default.extname(state.appFile));
1099
+ const ext = state.isESM ? ".ts" : ".js";
1100
+ const mcpServerPath = path_1.default.join(appFileDir, `mcp-server${ext}`);
1101
+ const mcpServerRel = path_1.default.relative(state.root, mcpServerPath);
1102
+ const hasCustomTools = state.customTools.length > 0;
1103
+ const customToolsPath = path_1.default.join(appFileDir, `mcp-custom-tools${ext}`);
1104
+ const customToolsExists = fs_1.default.existsSync(customToolsPath);
1105
+ const importAppPath = `./${appFileBase}`;
1106
+ let fileLines = [];
1107
+ fileLines.push(`// Auto-generated by @hellocrossman/mcp-sdk`);
1108
+ fileLines.push(`// This file connects your Express app to AI assistants (Claude, ChatGPT, Cursor)`);
1109
+ fileLines.push(`// Safe to regenerate with: npx @hellocrossman/mcp-sdk init --force`);
1110
+ fileLines.push(``);
1111
+ if (state.isESM) {
1112
+ if (state.appExportName === "default") {
1113
+ fileLines.push(`import app from '${importAppPath}';`);
1114
+ }
1115
+ else if (state.appExportName === "app") {
1116
+ fileLines.push(`import { app } from '${importAppPath}';`);
1117
+ }
1118
+ else {
1119
+ fileLines.push(`import { ${state.appExportName} as app } from '${importAppPath}';`);
1120
+ }
1121
+ fileLines.push(`import { createMcpServer } from '@hellocrossman/mcp-sdk';`);
1122
+ if (hasCustomTools || customToolsExists) {
1123
+ fileLines.push(`import { getCustomTools } from './mcp-custom-tools';`);
1124
+ }
1125
+ }
1126
+ else {
1127
+ if (state.appExportName === "default") {
1128
+ fileLines.push(`const app = require('${importAppPath}');`);
1129
+ }
1130
+ else if (state.appExportName === "app") {
1131
+ fileLines.push(`const { app } = require('${importAppPath}');`);
1132
+ }
1133
+ else {
1134
+ fileLines.push(`const { ${state.appExportName}: app } = require('${importAppPath}');`);
1135
+ }
1136
+ fileLines.push(`const { createMcpServer } = require('@hellocrossman/mcp-sdk');`);
1137
+ if (hasCustomTools || customToolsExists) {
1138
+ fileLines.push(`const { getCustomTools } = require('./mcp-custom-tools');`);
1139
+ }
1140
+ }
1141
+ fileLines.push(``);
1142
+ if (hasCustomTools || customToolsExists) {
1143
+ configParts.push(`customTools: getCustomTools(app, process.env.DATABASE_URL)`);
1144
+ }
1145
+ if (state.authProvider !== "none") {
1146
+ const authLines = generateAuthCode(state);
1147
+ fileLines.push(...authLines);
1148
+ fileLines.push(``);
1149
+ configParts.push(`auth: mcpAuth`);
1150
+ }
1151
+ let configStr;
550
1152
  if (configParts.length <= 2) {
551
- setupCode = `createMcpServer({ ${configParts.join(", ")} });`;
1153
+ configStr = `createMcpServer({ ${configParts.join(", ")} });`;
552
1154
  }
553
1155
  else {
554
1156
  const indent = " ";
555
- setupCode = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
1157
+ configStr = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
556
1158
  }
557
- const lines = content.split("\n");
558
- let lastImportIndex = -1;
559
- for (let i = 0; i < lines.length; i++) {
560
- if (/^import\s/.test(lines[i]) || /^const\s.*=\s*require/.test(lines[i])) {
561
- lastImportIndex = i;
562
- }
1159
+ fileLines.push(configStr);
1160
+ fileLines.push(``);
1161
+ if (hasCustomTools) {
1162
+ const customToolsContent = generateCustomToolsFile(state);
1163
+ fs_1.default.writeFileSync(customToolsPath, customToolsContent);
563
1164
  }
564
- lines.splice(lastImportIndex + 1, 0, importLine);
565
- let listenIndex = -1;
566
- for (let i = lines.length - 1; i >= 0; i--) {
567
- if (LISTEN_PATTERN.test(lines[i])) {
568
- listenIndex = i;
569
- break;
1165
+ const mcpServerContent = fileLines.join("\n");
1166
+ const backups = [];
1167
+ backups.push({ path: mcpServerPath, content: fs_1.default.existsSync(mcpServerPath) ? fs_1.default.readFileSync(mcpServerPath, "utf-8") : null });
1168
+ fs_1.default.writeFileSync(mcpServerPath, mcpServerContent);
1169
+ let addedImportTo = [];
1170
+ const modifiedEntries = [];
1171
+ for (const entryFile of state.entryFiles) {
1172
+ const entryContent = fs_1.default.readFileSync(entryFile, "utf-8");
1173
+ if (entryContent.includes("mcp-server")) {
1174
+ addedImportTo.push(path_1.default.relative(state.root, entryFile));
1175
+ continue;
570
1176
  }
1177
+ backups.push({ path: entryFile, content: entryContent });
1178
+ const entryDir = path_1.default.dirname(entryFile);
1179
+ const importPath = `./${path_1.default.relative(entryDir, mcpServerPath).replace(/\.(ts|js)$/, "")}`;
1180
+ const isEntryESM = checkIsESM(entryFile, state.root);
1181
+ const importStatement = isEntryESM
1182
+ ? `import '${importPath}';`
1183
+ : `require('${importPath}');`;
1184
+ const entryLines = entryContent.split("\n");
1185
+ let lastImportIndex = -1;
1186
+ for (let i = 0; i < entryLines.length; i++) {
1187
+ if (/^import\s/.test(entryLines[i]) || /^const\s.*=\s*require/.test(entryLines[i]) || /^require\s*\(/.test(entryLines[i])) {
1188
+ lastImportIndex = i;
1189
+ }
1190
+ }
1191
+ const comment = `// MCP server - exposes your API to AI assistants`;
1192
+ if (lastImportIndex >= 0) {
1193
+ entryLines.splice(lastImportIndex + 1, 0, comment, importStatement);
1194
+ }
1195
+ else {
1196
+ entryLines.unshift(comment, importStatement, "");
1197
+ }
1198
+ const newEntryContent = entryLines.join("\n");
1199
+ fs_1.default.writeFileSync(entryFile, newEntryContent);
1200
+ modifiedEntries.push({ path: entryFile, newContent: newEntryContent });
1201
+ addedImportTo.push(path_1.default.relative(state.root, entryFile));
571
1202
  }
572
- const commentLine = `// MCP server - exposes your API to AI assistants (Claude, ChatGPT, Cursor)`;
573
- if (listenIndex >= 0) {
574
- lines.splice(listenIndex, 0, "", commentLine, setupCode);
575
- }
576
- else {
577
- lines.push("", commentLine, setupCode);
1203
+ const validationErrors = validateGeneratedFiles(mcpServerPath, modifiedEntries.map((e) => e.path));
1204
+ if (validationErrors.length > 0) {
1205
+ console.log(`\n ${c("yellow", "!")} Validation failed. Reverting changes...`);
1206
+ for (const err of validationErrors) {
1207
+ console.log(` ${c("dim", "-")} ${err}`);
1208
+ }
1209
+ for (const backup of backups) {
1210
+ if (backup.content === null) {
1211
+ try {
1212
+ fs_1.default.unlinkSync(backup.path);
1213
+ }
1214
+ catch { }
1215
+ }
1216
+ else {
1217
+ fs_1.default.writeFileSync(backup.path, backup.content);
1218
+ }
1219
+ }
1220
+ console.log(` ${c("green", "+")} Changes reverted. Your files are unchanged.`);
1221
+ console.log(` ${c("dim", " Please report this issue at:")} ${c("cyan", "https://github.com/hellocrossman/mcp-sdk/issues")}`);
1222
+ return;
578
1223
  }
579
- fs_1.default.writeFileSync(state.entryFile, lines.join("\n"));
580
- const relPath = path_1.default.relative(state.root, state.entryFile);
581
1224
  const enabledCount = state.tools.filter((t) => t.enabled).length;
582
1225
  console.log();
583
1226
  console.log(c("green", ` +------------------------------+`));
@@ -586,13 +1229,22 @@ async function stepGenerate(state) {
586
1229
  console.log(c("green", ` | |`));
587
1230
  console.log(c("green", ` +------------------------------+`));
588
1231
  console.log();
589
- console.log(` ${c("green", "\u2713")} Updated ${c("bold", relPath)}`);
1232
+ console.log(` ${c("green", "\u2713")} Created ${c("bold", mcpServerRel)}`);
1233
+ if (hasCustomTools) {
1234
+ console.log(` ${c("green", "\u2713")} Created ${c("bold", path_1.default.relative(state.root, customToolsPath))}`);
1235
+ }
1236
+ for (const entry of addedImportTo) {
1237
+ console.log(` ${c("green", "\u2713")} Updated ${c("bold", entry)}`);
1238
+ }
590
1239
  console.log();
591
1240
  console.log(` ${c("dim", "Configuration")}`);
592
1241
  console.log(` ${c("dim", "\u2502")} Tools enabled ${c("bold", String(enabledCount))}`);
1242
+ console.log(` ${c("dim", "\u2502")} Custom tools ${state.customTools.length > 0 ? c("green", `${state.customTools.length} defined`) : c("dim", "none")}`);
593
1243
  console.log(` ${c("dim", "\u2502")} Database ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
594
1244
  console.log(` ${c("dim", "\u2502")} AI enrichment ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
595
1245
  console.log(` ${c("dim", "\u2502")} Write operations ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
1246
+ const authLabel = state.authProvider === "none" ? c("dim", "disabled") : c("green", AUTH_PROVIDERS.find((p) => p.id === state.authProvider)?.label || state.authProvider);
1247
+ console.log(` ${c("dim", "\u2502")} Authentication ${authLabel}`);
596
1248
  console.log();
597
1249
  console.log(` ${c("bold", "What's next?")}`);
598
1250
  console.log();
@@ -661,16 +1313,20 @@ async function runWizard(specifiedFile, force) {
661
1313
  const prompt = createPrompt();
662
1314
  const state = {
663
1315
  root: findProjectRoot(),
664
- entryFile: "",
1316
+ appFile: "",
665
1317
  appVarName: "app",
1318
+ appExportName: "app",
1319
+ entryFiles: [],
666
1320
  isESM: false,
667
1321
  routePrefix: "/api",
668
1322
  routes: [],
669
1323
  dbTables: [],
670
1324
  tools: [],
1325
+ customTools: [],
671
1326
  databaseEnabled: true,
672
1327
  enrichmentEnabled: true,
673
1328
  includeWrites: false,
1329
+ authProvider: "none",
674
1330
  };
675
1331
  try {
676
1332
  const found = await stepDetectApp(state, prompt, specifiedFile, force);
@@ -681,6 +1337,8 @@ async function runWizard(specifiedFile, force) {
681
1337
  await stepScanRoutes(state);
682
1338
  await stepScanDatabase(state, prompt);
683
1339
  await stepEnrichAndReview(state, prompt);
1340
+ await stepCustomTools(state, prompt);
1341
+ await stepAuth(state, prompt);
684
1342
  await stepGenerate(state);
685
1343
  }
686
1344
  catch (err) {