@hellocrossman/mcp-sdk 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +472 -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.map +1 -1
- package/dist/cjs/server.js +41 -1
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/types.d.ts +7 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cli.js +472 -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.map +1 -1
- package/dist/server.js +41 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +7 -0
- 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, 6, "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, 6, "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, 6, "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, 6, "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,239 @@ async function stepEnrichAndReview(state, prompt) {
|
|
|
507
588
|
state.includeWrites = wantWrites;
|
|
508
589
|
}
|
|
509
590
|
}
|
|
591
|
+
async function stepCustomTools(state, prompt) {
|
|
592
|
+
printStep(5, 6, "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
|
+
function generateCustomToolsFile(state) {
|
|
680
|
+
const lines = [];
|
|
681
|
+
lines.push(`// Custom tools for your MCP server`);
|
|
682
|
+
lines.push(`// Hand-crafted tools that persist across re-scans`);
|
|
683
|
+
lines.push(`// Edit this file to customize tool behavior`);
|
|
684
|
+
lines.push(``);
|
|
685
|
+
if (state.isESM) {
|
|
686
|
+
lines.push(`import type { Express } from 'express';`);
|
|
687
|
+
}
|
|
688
|
+
lines.push(``);
|
|
689
|
+
lines.push(`export interface CustomToolDefinition {`);
|
|
690
|
+
lines.push(` name: string;`);
|
|
691
|
+
lines.push(` description: string;`);
|
|
692
|
+
lines.push(` inputSchema: Record<string, unknown>;`);
|
|
693
|
+
lines.push(` handler: (args: Record<string, unknown>) => Promise<unknown>;`);
|
|
694
|
+
lines.push(`}`);
|
|
695
|
+
lines.push(``);
|
|
696
|
+
lines.push(`export function getCustomTools(app: ${state.isESM ? "Express" : "any"}, dbUrl?: string): CustomToolDefinition[] {`);
|
|
697
|
+
lines.push(` const tools: CustomToolDefinition[] = [];`);
|
|
698
|
+
lines.push(``);
|
|
699
|
+
for (const tool of state.customTools) {
|
|
700
|
+
const properties = [];
|
|
701
|
+
const required = [];
|
|
702
|
+
for (const param of tool.params) {
|
|
703
|
+
properties.push(` ${param.name}: { type: "${param.type}", description: "${param.description.replace(/"/g, '\\"')}" }`);
|
|
704
|
+
if (param.required)
|
|
705
|
+
required.push(`"${param.name}"`);
|
|
706
|
+
}
|
|
707
|
+
lines.push(` tools.push({`);
|
|
708
|
+
lines.push(` name: "${tool.name}",`);
|
|
709
|
+
lines.push(` description: "${tool.description.replace(/"/g, '\\"')}",`);
|
|
710
|
+
lines.push(` inputSchema: {`);
|
|
711
|
+
lines.push(` type: "object",`);
|
|
712
|
+
lines.push(` properties: {`);
|
|
713
|
+
lines.push(properties.join(",\n"));
|
|
714
|
+
lines.push(` },`);
|
|
715
|
+
if (required.length > 0) {
|
|
716
|
+
lines.push(` required: [${required.join(", ")}],`);
|
|
717
|
+
}
|
|
718
|
+
lines.push(` },`);
|
|
719
|
+
if (tool.dataSource === "database" && tool.tableName) {
|
|
720
|
+
lines.push(` handler: async (args) => {`);
|
|
721
|
+
lines.push(` if (!dbUrl) return { error: "No database connection configured" };`);
|
|
722
|
+
lines.push(` const pg = await import("pg");`);
|
|
723
|
+
lines.push(` const client = new pg.default.Client({ connectionString: dbUrl });`);
|
|
724
|
+
lines.push(` try {`);
|
|
725
|
+
lines.push(` await client.connect();`);
|
|
726
|
+
if (tool.scopeColumn) {
|
|
727
|
+
lines.push(` const scopeValue = args.${tool.scopeColumn} as string;`);
|
|
728
|
+
lines.push(` if (!scopeValue) return { error: "${tool.scopeColumn} is required for scoped queries" };`);
|
|
729
|
+
lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} WHERE ${tool.scopeColumn} = $1 LIMIT 100\`, [scopeValue]);`);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} LIMIT 100\`);`);
|
|
733
|
+
}
|
|
734
|
+
lines.push(` return result.rows;`);
|
|
735
|
+
lines.push(` } finally {`);
|
|
736
|
+
lines.push(` await client.end();`);
|
|
737
|
+
lines.push(` }`);
|
|
738
|
+
lines.push(` },`);
|
|
739
|
+
}
|
|
740
|
+
else if (tool.dataSource === "route" && tool.routePath) {
|
|
741
|
+
lines.push(` handler: async (args) => {`);
|
|
742
|
+
lines.push(` // TODO: Implement route-based handler`);
|
|
743
|
+
lines.push(` // This tool maps to ${tool.routeMethod} ${tool.routePath}`);
|
|
744
|
+
lines.push(` return { message: "Implement this handler to call your API" };`);
|
|
745
|
+
lines.push(` },`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
lines.push(` handler: async (args) => {`);
|
|
749
|
+
lines.push(` // TODO: Implement custom handler`);
|
|
750
|
+
lines.push(` return { message: "Implement this handler" };`);
|
|
751
|
+
lines.push(` },`);
|
|
752
|
+
}
|
|
753
|
+
lines.push(` });`);
|
|
754
|
+
lines.push(``);
|
|
755
|
+
}
|
|
756
|
+
lines.push(` return tools;`);
|
|
757
|
+
lines.push(`}`);
|
|
758
|
+
lines.push(``);
|
|
759
|
+
return lines.join("\n");
|
|
760
|
+
}
|
|
761
|
+
function validateGeneratedFiles(mcpServerPath, entryFiles) {
|
|
762
|
+
const errors = [];
|
|
763
|
+
const allFiles = [mcpServerPath, ...entryFiles];
|
|
764
|
+
for (const filePath of allFiles) {
|
|
765
|
+
if (!fs_1.default.existsSync(filePath))
|
|
766
|
+
continue;
|
|
767
|
+
const content = fs_1.default.readFileSync(filePath, "utf-8");
|
|
768
|
+
const relPath = path_1.default.basename(filePath);
|
|
769
|
+
let openBraces = 0;
|
|
770
|
+
let openParens = 0;
|
|
771
|
+
let openBrackets = 0;
|
|
772
|
+
let inString = null;
|
|
773
|
+
let inTemplateString = false;
|
|
774
|
+
for (let i = 0; i < content.length; i++) {
|
|
775
|
+
const ch = content[i];
|
|
776
|
+
const prev = i > 0 ? content[i - 1] : "";
|
|
777
|
+
if (inString) {
|
|
778
|
+
if (ch === inString && prev !== "\\")
|
|
779
|
+
inString = null;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (inTemplateString) {
|
|
783
|
+
if (ch === "`" && prev !== "\\")
|
|
784
|
+
inTemplateString = false;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (ch === "'" || ch === '"') {
|
|
788
|
+
inString = ch;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (ch === "`") {
|
|
792
|
+
inTemplateString = true;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
796
|
+
while (i < content.length && content[i] !== "\n")
|
|
797
|
+
i++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (ch === "{")
|
|
801
|
+
openBraces++;
|
|
802
|
+
else if (ch === "}")
|
|
803
|
+
openBraces--;
|
|
804
|
+
else if (ch === "(")
|
|
805
|
+
openParens++;
|
|
806
|
+
else if (ch === ")")
|
|
807
|
+
openParens--;
|
|
808
|
+
else if (ch === "[")
|
|
809
|
+
openBrackets++;
|
|
810
|
+
else if (ch === "]")
|
|
811
|
+
openBrackets--;
|
|
812
|
+
}
|
|
813
|
+
if (openBraces !== 0)
|
|
814
|
+
errors.push(`${relPath}: Unbalanced braces (${openBraces > 0 ? "missing }" : "extra }"})`);
|
|
815
|
+
if (openParens !== 0)
|
|
816
|
+
errors.push(`${relPath}: Unbalanced parentheses (${openParens > 0 ? "missing )" : "extra )"})`);
|
|
817
|
+
if (openBrackets !== 0)
|
|
818
|
+
errors.push(`${relPath}: Unbalanced brackets (${openBrackets > 0 ? "missing ]" : "extra ]"})`);
|
|
819
|
+
}
|
|
820
|
+
return errors;
|
|
821
|
+
}
|
|
510
822
|
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;
|
|
823
|
+
printStep(6, 6, "Generating MCP server");
|
|
514
824
|
const disabledRoutePaths = state.tools
|
|
515
825
|
.filter((t) => t.source === "route" && !t.enabled)
|
|
516
826
|
.map((t) => t.path);
|
|
@@ -528,7 +838,7 @@ async function stepGenerate(state) {
|
|
|
528
838
|
disabledTables.add(tableName);
|
|
529
839
|
}
|
|
530
840
|
const configParts = [];
|
|
531
|
-
configParts.push(`app
|
|
841
|
+
configParts.push(`app`);
|
|
532
842
|
if (disabledRoutes.length > 0) {
|
|
533
843
|
configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
|
|
534
844
|
}
|
|
@@ -546,38 +856,127 @@ async function stepGenerate(state) {
|
|
|
546
856
|
if (!state.enrichmentEnabled) {
|
|
547
857
|
configParts.push(`enrichment: false`);
|
|
548
858
|
}
|
|
549
|
-
|
|
859
|
+
const appFileDir = path_1.default.dirname(state.appFile);
|
|
860
|
+
const appFileBase = path_1.default.basename(state.appFile, path_1.default.extname(state.appFile));
|
|
861
|
+
const ext = state.isESM ? ".ts" : ".js";
|
|
862
|
+
const mcpServerPath = path_1.default.join(appFileDir, `mcp-server${ext}`);
|
|
863
|
+
const mcpServerRel = path_1.default.relative(state.root, mcpServerPath);
|
|
864
|
+
const hasCustomTools = state.customTools.length > 0;
|
|
865
|
+
const customToolsPath = path_1.default.join(appFileDir, `mcp-custom-tools${ext}`);
|
|
866
|
+
const customToolsExists = fs_1.default.existsSync(customToolsPath);
|
|
867
|
+
const importAppPath = `./${appFileBase}`;
|
|
868
|
+
let fileLines = [];
|
|
869
|
+
fileLines.push(`// Auto-generated by @hellocrossman/mcp-sdk`);
|
|
870
|
+
fileLines.push(`// This file connects your Express app to AI assistants (Claude, ChatGPT, Cursor)`);
|
|
871
|
+
fileLines.push(`// Safe to regenerate with: npx @hellocrossman/mcp-sdk init --force`);
|
|
872
|
+
fileLines.push(``);
|
|
873
|
+
if (state.isESM) {
|
|
874
|
+
if (state.appExportName === "default") {
|
|
875
|
+
fileLines.push(`import app from '${importAppPath}';`);
|
|
876
|
+
}
|
|
877
|
+
else if (state.appExportName === "app") {
|
|
878
|
+
fileLines.push(`import { app } from '${importAppPath}';`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
fileLines.push(`import { ${state.appExportName} as app } from '${importAppPath}';`);
|
|
882
|
+
}
|
|
883
|
+
fileLines.push(`import { createMcpServer } from '@hellocrossman/mcp-sdk';`);
|
|
884
|
+
if (hasCustomTools || customToolsExists) {
|
|
885
|
+
fileLines.push(`import { getCustomTools } from './mcp-custom-tools';`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
if (state.appExportName === "default") {
|
|
890
|
+
fileLines.push(`const app = require('${importAppPath}');`);
|
|
891
|
+
}
|
|
892
|
+
else if (state.appExportName === "app") {
|
|
893
|
+
fileLines.push(`const { app } = require('${importAppPath}');`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
fileLines.push(`const { ${state.appExportName}: app } = require('${importAppPath}');`);
|
|
897
|
+
}
|
|
898
|
+
fileLines.push(`const { createMcpServer } = require('@hellocrossman/mcp-sdk');`);
|
|
899
|
+
if (hasCustomTools || customToolsExists) {
|
|
900
|
+
fileLines.push(`const { getCustomTools } = require('./mcp-custom-tools');`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
fileLines.push(``);
|
|
904
|
+
if (hasCustomTools || customToolsExists) {
|
|
905
|
+
configParts.push(`customTools: getCustomTools(app, process.env.DATABASE_URL)`);
|
|
906
|
+
}
|
|
907
|
+
let configStr;
|
|
550
908
|
if (configParts.length <= 2) {
|
|
551
|
-
|
|
909
|
+
configStr = `createMcpServer({ ${configParts.join(", ")} });`;
|
|
552
910
|
}
|
|
553
911
|
else {
|
|
554
912
|
const indent = " ";
|
|
555
|
-
|
|
913
|
+
configStr = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
|
|
556
914
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
915
|
+
fileLines.push(configStr);
|
|
916
|
+
fileLines.push(``);
|
|
917
|
+
if (hasCustomTools) {
|
|
918
|
+
const customToolsContent = generateCustomToolsFile(state);
|
|
919
|
+
fs_1.default.writeFileSync(customToolsPath, customToolsContent);
|
|
563
920
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
921
|
+
const mcpServerContent = fileLines.join("\n");
|
|
922
|
+
const backups = [];
|
|
923
|
+
backups.push({ path: mcpServerPath, content: fs_1.default.existsSync(mcpServerPath) ? fs_1.default.readFileSync(mcpServerPath, "utf-8") : null });
|
|
924
|
+
fs_1.default.writeFileSync(mcpServerPath, mcpServerContent);
|
|
925
|
+
let addedImportTo = [];
|
|
926
|
+
const modifiedEntries = [];
|
|
927
|
+
for (const entryFile of state.entryFiles) {
|
|
928
|
+
const entryContent = fs_1.default.readFileSync(entryFile, "utf-8");
|
|
929
|
+
if (entryContent.includes("mcp-server")) {
|
|
930
|
+
addedImportTo.push(path_1.default.relative(state.root, entryFile));
|
|
931
|
+
continue;
|
|
570
932
|
}
|
|
933
|
+
backups.push({ path: entryFile, content: entryContent });
|
|
934
|
+
const entryDir = path_1.default.dirname(entryFile);
|
|
935
|
+
const importPath = `./${path_1.default.relative(entryDir, mcpServerPath).replace(/\.(ts|js)$/, "")}`;
|
|
936
|
+
const isEntryESM = checkIsESM(entryFile, state.root);
|
|
937
|
+
const importStatement = isEntryESM
|
|
938
|
+
? `import '${importPath}';`
|
|
939
|
+
: `require('${importPath}');`;
|
|
940
|
+
const entryLines = entryContent.split("\n");
|
|
941
|
+
let lastImportIndex = -1;
|
|
942
|
+
for (let i = 0; i < entryLines.length; i++) {
|
|
943
|
+
if (/^import\s/.test(entryLines[i]) || /^const\s.*=\s*require/.test(entryLines[i]) || /^require\s*\(/.test(entryLines[i])) {
|
|
944
|
+
lastImportIndex = i;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const comment = `// MCP server - exposes your API to AI assistants`;
|
|
948
|
+
if (lastImportIndex >= 0) {
|
|
949
|
+
entryLines.splice(lastImportIndex + 1, 0, comment, importStatement);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
entryLines.unshift(comment, importStatement, "");
|
|
953
|
+
}
|
|
954
|
+
const newEntryContent = entryLines.join("\n");
|
|
955
|
+
fs_1.default.writeFileSync(entryFile, newEntryContent);
|
|
956
|
+
modifiedEntries.push({ path: entryFile, newContent: newEntryContent });
|
|
957
|
+
addedImportTo.push(path_1.default.relative(state.root, entryFile));
|
|
571
958
|
}
|
|
572
|
-
const
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
959
|
+
const validationErrors = validateGeneratedFiles(mcpServerPath, modifiedEntries.map((e) => e.path));
|
|
960
|
+
if (validationErrors.length > 0) {
|
|
961
|
+
console.log(`\n ${c("yellow", "!")} Validation failed. Reverting changes...`);
|
|
962
|
+
for (const err of validationErrors) {
|
|
963
|
+
console.log(` ${c("dim", "-")} ${err}`);
|
|
964
|
+
}
|
|
965
|
+
for (const backup of backups) {
|
|
966
|
+
if (backup.content === null) {
|
|
967
|
+
try {
|
|
968
|
+
fs_1.default.unlinkSync(backup.path);
|
|
969
|
+
}
|
|
970
|
+
catch { }
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
fs_1.default.writeFileSync(backup.path, backup.content);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
console.log(` ${c("green", "+")} Changes reverted. Your files are unchanged.`);
|
|
977
|
+
console.log(` ${c("dim", " Please report this issue at:")} ${c("cyan", "https://github.com/hellocrossman/mcp-sdk/issues")}`);
|
|
978
|
+
return;
|
|
578
979
|
}
|
|
579
|
-
fs_1.default.writeFileSync(state.entryFile, lines.join("\n"));
|
|
580
|
-
const relPath = path_1.default.relative(state.root, state.entryFile);
|
|
581
980
|
const enabledCount = state.tools.filter((t) => t.enabled).length;
|
|
582
981
|
console.log();
|
|
583
982
|
console.log(c("green", ` +------------------------------+`));
|
|
@@ -586,10 +985,17 @@ async function stepGenerate(state) {
|
|
|
586
985
|
console.log(c("green", ` | |`));
|
|
587
986
|
console.log(c("green", ` +------------------------------+`));
|
|
588
987
|
console.log();
|
|
589
|
-
console.log(` ${c("green", "\u2713")}
|
|
988
|
+
console.log(` ${c("green", "\u2713")} Created ${c("bold", mcpServerRel)}`);
|
|
989
|
+
if (hasCustomTools) {
|
|
990
|
+
console.log(` ${c("green", "\u2713")} Created ${c("bold", path_1.default.relative(state.root, customToolsPath))}`);
|
|
991
|
+
}
|
|
992
|
+
for (const entry of addedImportTo) {
|
|
993
|
+
console.log(` ${c("green", "\u2713")} Updated ${c("bold", entry)}`);
|
|
994
|
+
}
|
|
590
995
|
console.log();
|
|
591
996
|
console.log(` ${c("dim", "Configuration")}`);
|
|
592
997
|
console.log(` ${c("dim", "\u2502")} Tools enabled ${c("bold", String(enabledCount))}`);
|
|
998
|
+
console.log(` ${c("dim", "\u2502")} Custom tools ${state.customTools.length > 0 ? c("green", `${state.customTools.length} defined`) : c("dim", "none")}`);
|
|
593
999
|
console.log(` ${c("dim", "\u2502")} Database ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
594
1000
|
console.log(` ${c("dim", "\u2502")} AI enrichment ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
595
1001
|
console.log(` ${c("dim", "\u2502")} Write operations ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
@@ -661,13 +1067,16 @@ async function runWizard(specifiedFile, force) {
|
|
|
661
1067
|
const prompt = createPrompt();
|
|
662
1068
|
const state = {
|
|
663
1069
|
root: findProjectRoot(),
|
|
664
|
-
|
|
1070
|
+
appFile: "",
|
|
665
1071
|
appVarName: "app",
|
|
1072
|
+
appExportName: "app",
|
|
1073
|
+
entryFiles: [],
|
|
666
1074
|
isESM: false,
|
|
667
1075
|
routePrefix: "/api",
|
|
668
1076
|
routes: [],
|
|
669
1077
|
dbTables: [],
|
|
670
1078
|
tools: [],
|
|
1079
|
+
customTools: [],
|
|
671
1080
|
databaseEnabled: true,
|
|
672
1081
|
enrichmentEnabled: true,
|
|
673
1082
|
includeWrites: false,
|
|
@@ -681,6 +1090,7 @@ async function runWizard(specifiedFile, force) {
|
|
|
681
1090
|
await stepScanRoutes(state);
|
|
682
1091
|
await stepScanDatabase(state, prompt);
|
|
683
1092
|
await stepEnrichAndReview(state, prompt);
|
|
1093
|
+
await stepCustomTools(state, prompt);
|
|
684
1094
|
await stepGenerate(state);
|
|
685
1095
|
}
|
|
686
1096
|
catch (err) {
|