@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 +720 -62
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/server.d.ts +1 -1
- package/dist/cjs/server.d.ts.map +1 -1
- package/dist/cjs/server.js +120 -7
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/types.d.ts +18 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cli.js +720 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +120 -7
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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(
|
|
179
|
-
|
|
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,
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
+
appFile = findExpressEntryFile(state.root);
|
|
261
350
|
}
|
|
262
|
-
if (
|
|
263
|
-
const relPath = path_1.default.relative(state.root,
|
|
264
|
-
const content = fs_1.default.readFileSync(
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
400
|
+
state.appFile = appFile;
|
|
326
401
|
state.appVarName = appMatch ? appMatch[1] : "app";
|
|
327
|
-
state.
|
|
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,
|
|
333
|
-
state.routes = (0, route_scan_js_1.scanAllRoutes)(state.
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1153
|
+
configStr = `createMcpServer({ ${configParts.join(", ")} });`;
|
|
552
1154
|
}
|
|
553
1155
|
else {
|
|
554
1156
|
const indent = " ";
|
|
555
|
-
|
|
1157
|
+
configStr = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
|
|
556
1158
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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")}
|
|
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
|
-
|
|
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) {
|