@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/cli.js
CHANGED
|
@@ -147,8 +147,29 @@ function checkIsESM(filePath, root) {
|
|
|
147
147
|
return false;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
function alreadySetup(
|
|
151
|
-
|
|
150
|
+
function alreadySetup(root) {
|
|
151
|
+
const mcpServerFile = path.join(root, "mcp-server.ts");
|
|
152
|
+
const mcpServerFileJs = path.join(root, "mcp-server.js");
|
|
153
|
+
if (fs.existsSync(mcpServerFile) || fs.existsSync(mcpServerFileJs))
|
|
154
|
+
return true;
|
|
155
|
+
const serverMcpFile = path.join(root, "server", "mcp-server.ts");
|
|
156
|
+
const serverMcpFileJs = path.join(root, "server", "mcp-server.js");
|
|
157
|
+
if (fs.existsSync(serverMcpFile) || fs.existsSync(serverMcpFileJs))
|
|
158
|
+
return true;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function findExistingMcpServerFile(root) {
|
|
162
|
+
const candidates = [
|
|
163
|
+
path.join(root, "mcp-server.ts"),
|
|
164
|
+
path.join(root, "mcp-server.js"),
|
|
165
|
+
path.join(root, "server", "mcp-server.ts"),
|
|
166
|
+
path.join(root, "server", "mcp-server.js"),
|
|
167
|
+
];
|
|
168
|
+
for (const f of candidates) {
|
|
169
|
+
if (fs.existsSync(f))
|
|
170
|
+
return f;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
152
173
|
}
|
|
153
174
|
function checkPgInstalled(root) {
|
|
154
175
|
return fs.existsSync(path.join(root, "node_modules", "pg"));
|
|
@@ -215,13 +236,81 @@ function stripExistingMcpConfig(content) {
|
|
|
215
236
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
216
237
|
return result;
|
|
217
238
|
}
|
|
239
|
+
function findAppExport(content, varName) {
|
|
240
|
+
const namedExportRe = new RegExp(`export\\s+(?:const|let|var)\\s+${varName}\\s*=`);
|
|
241
|
+
if (namedExportRe.test(content))
|
|
242
|
+
return varName;
|
|
243
|
+
if (/export\s+default\s+app\b/.test(content))
|
|
244
|
+
return "default";
|
|
245
|
+
const moduleExportsRe = new RegExp(`module\\.exports\\s*=\\s*${varName}`);
|
|
246
|
+
if (moduleExportsRe.test(content))
|
|
247
|
+
return "default";
|
|
248
|
+
const exportsRe = new RegExp(`module\\.exports\\.${varName}\\s*=`);
|
|
249
|
+
if (exportsRe.test(content))
|
|
250
|
+
return varName;
|
|
251
|
+
return varName;
|
|
252
|
+
}
|
|
253
|
+
function findEntryFiles(root, appFile) {
|
|
254
|
+
const entries = [];
|
|
255
|
+
const appFileRel = path.relative(root, appFile);
|
|
256
|
+
const appBaseName = path.basename(appFile, path.extname(appFile));
|
|
257
|
+
const appDir = path.dirname(appFile);
|
|
258
|
+
const searchDirs = [appDir, root, path.join(root, "server"), path.join(root, "src")];
|
|
259
|
+
const seen = new Set();
|
|
260
|
+
for (const dir of searchDirs) {
|
|
261
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
262
|
+
continue;
|
|
263
|
+
const files = fs.readdirSync(dir).filter((f) => /\.(ts|js|mjs)$/.test(f));
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
const fullPath = path.join(dir, file);
|
|
266
|
+
if (seen.has(fullPath) || fullPath === appFile)
|
|
267
|
+
continue;
|
|
268
|
+
seen.add(fullPath);
|
|
269
|
+
try {
|
|
270
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
271
|
+
const hasListen = LISTEN_PATTERN.test(content);
|
|
272
|
+
const importsApp = content.includes(`./${appBaseName}`) ||
|
|
273
|
+
content.includes(`'./${appFileRel}'`) ||
|
|
274
|
+
content.includes(`"./${appFileRel}"`) ||
|
|
275
|
+
content.includes(`from './${appBaseName}'`) ||
|
|
276
|
+
content.includes(`from "./${appBaseName}"`) ||
|
|
277
|
+
content.includes(`require('./${appBaseName}')`) ||
|
|
278
|
+
content.includes(`require("./${appBaseName}")`);
|
|
279
|
+
if (hasListen || importsApp) {
|
|
280
|
+
entries.push(fullPath);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (entries.length === 0) {
|
|
287
|
+
entries.push(appFile);
|
|
288
|
+
}
|
|
289
|
+
return entries;
|
|
290
|
+
}
|
|
218
291
|
async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
219
|
-
printStep(1,
|
|
220
|
-
|
|
292
|
+
printStep(1, 6, "Detecting Express app");
|
|
293
|
+
if (alreadySetup(state.root)) {
|
|
294
|
+
if (force) {
|
|
295
|
+
const existingFile = findExistingMcpServerFile(state.root);
|
|
296
|
+
if (existingFile) {
|
|
297
|
+
console.log(` ${c("yellow", "~")} Found existing ${c("bold", path.relative(state.root, existingFile))}`);
|
|
298
|
+
console.log(` ${c("dim", " Will regenerate with fresh scan...")}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const existingFile = findExistingMcpServerFile(state.root);
|
|
303
|
+
const relPath = existingFile ? path.relative(state.root, existingFile) : "mcp-server.ts";
|
|
304
|
+
console.log(` ${c("green", "+")} MCP SDK is already set up (${c("bold", relPath)})`);
|
|
305
|
+
console.log(` ${c("dim", " Run with")} ${c("cyan", "--force")} ${c("dim", "to re-scan and reconfigure.")}`);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let appFile = null;
|
|
221
310
|
if (specifiedFile) {
|
|
222
311
|
const resolved = path.resolve(state.root, specifiedFile);
|
|
223
312
|
if (fs.existsSync(resolved)) {
|
|
224
|
-
|
|
313
|
+
appFile = resolved;
|
|
225
314
|
}
|
|
226
315
|
else {
|
|
227
316
|
console.log(` ${c("yellow", "!")} File not found: ${specifiedFile}`);
|
|
@@ -229,11 +318,11 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
229
318
|
}
|
|
230
319
|
}
|
|
231
320
|
else {
|
|
232
|
-
|
|
321
|
+
appFile = findExpressEntryFile(state.root);
|
|
233
322
|
}
|
|
234
|
-
if (
|
|
235
|
-
const relPath = path.relative(state.root,
|
|
236
|
-
const content = fs.readFileSync(
|
|
323
|
+
if (appFile && !specifiedFile) {
|
|
324
|
+
const relPath = path.relative(state.root, appFile);
|
|
325
|
+
const content = fs.readFileSync(appFile, "utf-8");
|
|
237
326
|
const hasCreation = APP_VAR_PATTERN.test(content);
|
|
238
327
|
const hasListen = LISTEN_PATTERN.test(content);
|
|
239
328
|
const signals = [];
|
|
@@ -249,7 +338,7 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
249
338
|
if (custom) {
|
|
250
339
|
const resolved = path.resolve(state.root, custom);
|
|
251
340
|
if (fs.existsSync(resolved)) {
|
|
252
|
-
|
|
341
|
+
appFile = resolved;
|
|
253
342
|
}
|
|
254
343
|
else {
|
|
255
344
|
console.log(` ${c("yellow", "!")} File not found: ${custom}`);
|
|
@@ -261,13 +350,13 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
261
350
|
}
|
|
262
351
|
}
|
|
263
352
|
}
|
|
264
|
-
if (!
|
|
353
|
+
if (!appFile) {
|
|
265
354
|
console.log(` ${c("yellow", "!")} Could not find an Express app file.`);
|
|
266
355
|
const custom = await prompt.ask(` Enter the path to your Express app file: `);
|
|
267
356
|
if (custom) {
|
|
268
357
|
const resolved = path.resolve(state.root, custom);
|
|
269
358
|
if (fs.existsSync(resolved)) {
|
|
270
|
-
|
|
359
|
+
appFile = resolved;
|
|
271
360
|
}
|
|
272
361
|
else {
|
|
273
362
|
console.log(` ${c("yellow", "!")} File not found: ${custom}`);
|
|
@@ -278,31 +367,23 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
278
367
|
return false;
|
|
279
368
|
}
|
|
280
369
|
}
|
|
281
|
-
|
|
282
|
-
if (alreadySetup(content)) {
|
|
283
|
-
if (force) {
|
|
284
|
-
console.log(` ${c("yellow", "~")} Found existing MCP config in ${path.relative(state.root, entryFile)}`);
|
|
285
|
-
console.log(` ${c("dim", " Removing old configuration for re-scan...")}`);
|
|
286
|
-
content = stripExistingMcpConfig(content);
|
|
287
|
-
fs.writeFileSync(entryFile, content);
|
|
288
|
-
console.log(` ${c("green", "+")} Old config removed. Starting fresh.`);
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
console.log(` ${c("green", "+")} MCP SDK is already set up in ${path.relative(state.root, entryFile)}`);
|
|
292
|
-
console.log(` ${c("dim", " Run with")} ${c("cyan", "--force")} ${c("dim", "to re-scan and reconfigure.")}`);
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
370
|
+
const content = fs.readFileSync(appFile, "utf-8");
|
|
296
371
|
const appMatch = content.match(APP_VAR_PATTERN);
|
|
297
|
-
state.
|
|
372
|
+
state.appFile = appFile;
|
|
298
373
|
state.appVarName = appMatch ? appMatch[1] : "app";
|
|
299
|
-
state.
|
|
374
|
+
state.appExportName = findAppExport(content, state.appVarName);
|
|
375
|
+
state.isESM = checkIsESM(appFile, state.root);
|
|
376
|
+
const entryFiles = findEntryFiles(state.root, appFile);
|
|
377
|
+
state.entryFiles = entryFiles;
|
|
300
378
|
console.log(` ${c("dim", ` Variable: ${state.appVarName}, Module: ${state.isESM ? "ESM" : "CommonJS"}`)}`);
|
|
379
|
+
if (entryFiles.length > 0 && entryFiles[0] !== appFile) {
|
|
380
|
+
console.log(` ${c("dim", ` Entry files: ${entryFiles.map((f) => path.relative(state.root, f)).join(", ")}`)}`);
|
|
381
|
+
}
|
|
301
382
|
return true;
|
|
302
383
|
}
|
|
303
384
|
async function stepScanRoutes(state) {
|
|
304
|
-
printStep(2,
|
|
305
|
-
state.routes = scanAllRoutes(state.
|
|
385
|
+
printStep(2, 6, "Scanning API routes");
|
|
386
|
+
state.routes = scanAllRoutes(state.appFile, state.routePrefix);
|
|
306
387
|
if (state.routes.length === 0) {
|
|
307
388
|
console.log(` ${c("dim", " No routes found under")} ${state.routePrefix}`);
|
|
308
389
|
console.log(` ${c("dim", " Routes will be discovered at runtime when your app starts.")}`);
|
|
@@ -327,7 +408,7 @@ async function stepScanRoutes(state) {
|
|
|
327
408
|
}
|
|
328
409
|
}
|
|
329
410
|
async function stepScanDatabase(state, prompt) {
|
|
330
|
-
printStep(3,
|
|
411
|
+
printStep(3, 6, "Database discovery");
|
|
331
412
|
const wantDb = await prompt.confirm(` Scan your database for tables to expose as tools?`);
|
|
332
413
|
if (!wantDb) {
|
|
333
414
|
state.databaseEnabled = false;
|
|
@@ -415,7 +496,7 @@ async function stepScanDatabase(state, prompt) {
|
|
|
415
496
|
}
|
|
416
497
|
}
|
|
417
498
|
async function stepEnrichAndReview(state, prompt) {
|
|
418
|
-
printStep(4,
|
|
499
|
+
printStep(4, 6, "AI enrichment & tool review");
|
|
419
500
|
if (state.tools.length === 0) {
|
|
420
501
|
console.log(` ${c("dim", " No tools discovered. Routes will be discovered at runtime.")}`);
|
|
421
502
|
state.enrichmentEnabled = true;
|
|
@@ -479,10 +560,239 @@ async function stepEnrichAndReview(state, prompt) {
|
|
|
479
560
|
state.includeWrites = wantWrites;
|
|
480
561
|
}
|
|
481
562
|
}
|
|
563
|
+
async function stepCustomTools(state, prompt) {
|
|
564
|
+
printStep(5, 6, "Custom tools (optional)");
|
|
565
|
+
console.log(` ${c("dim", "Auto-discovered tools give broad coverage.")}`);
|
|
566
|
+
console.log(` ${c("dim", "Custom tools let you define purpose-built queries")}`);
|
|
567
|
+
console.log(` ${c("dim", "for specific business questions.\n")}`);
|
|
568
|
+
const wantCustom = await prompt.confirm(` Define custom tools?`, false);
|
|
569
|
+
if (!wantCustom) {
|
|
570
|
+
console.log(` ${c("dim", " Skipping custom tools.")}`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const availableRoutes = state.routes.map((r) => `${r.method} ${r.path}`);
|
|
574
|
+
const availableTables = state.dbTables.map((t) => t.name);
|
|
575
|
+
while (true) {
|
|
576
|
+
console.log();
|
|
577
|
+
const name = await prompt.ask(` ${c("cyan", "Tool name")} ${c("dim", "(e.g. get_traffic_overview):")} `);
|
|
578
|
+
if (!name)
|
|
579
|
+
break;
|
|
580
|
+
const toolName = name.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
581
|
+
const description = await prompt.ask(` ${c("cyan", "Description")} ${c("dim", "(what does this tool do?):")} `);
|
|
582
|
+
if (!description)
|
|
583
|
+
break;
|
|
584
|
+
const params = [];
|
|
585
|
+
console.log(`\n ${c("dim", "Define input parameters (press Enter with no name to finish):")}`);
|
|
586
|
+
while (true) {
|
|
587
|
+
const paramName = await prompt.ask(` ${c("cyan", "Parameter name:")} `);
|
|
588
|
+
if (!paramName)
|
|
589
|
+
break;
|
|
590
|
+
const paramDesc = await prompt.ask(` ${c("cyan", "Description:")} `);
|
|
591
|
+
console.log(` ${c("dim", "Type: 1) string 2) number 3) boolean")}`);
|
|
592
|
+
const typeChoice = await prompt.ask(` ${c("cyan", "Type")} ${c("dim", "(1/2/3, default: 1):")} `);
|
|
593
|
+
const paramType = typeChoice === "2" ? "number" : typeChoice === "3" ? "boolean" : "string";
|
|
594
|
+
const isRequired = await prompt.confirm(` Required?`);
|
|
595
|
+
params.push({ name: paramName, type: paramType, description: paramDesc || paramName, required: isRequired });
|
|
596
|
+
console.log(` ${c("green", "+")} Added parameter: ${c("bold", paramName)} (${paramType})`);
|
|
597
|
+
}
|
|
598
|
+
console.log(`\n ${c("dim", "How should this tool get its data?")}`);
|
|
599
|
+
console.log(` ${c("cyan", "1.")} Call an existing API route`);
|
|
600
|
+
console.log(` ${c("cyan", "2.")} Run a database query`);
|
|
601
|
+
const sourceChoice = await prompt.ask(` ${c("dim", ">")} `);
|
|
602
|
+
const customTool = {
|
|
603
|
+
name: toolName,
|
|
604
|
+
description,
|
|
605
|
+
params,
|
|
606
|
+
dataSource: sourceChoice === "2" ? "database" : "route",
|
|
607
|
+
};
|
|
608
|
+
if (sourceChoice === "2" && availableTables.length > 0) {
|
|
609
|
+
console.log(`\n ${c("dim", "Available tables:")}`);
|
|
610
|
+
availableTables.forEach((t, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${t}`));
|
|
611
|
+
const tableChoice = await prompt.ask(` ${c("dim", "Table number or name:")} `);
|
|
612
|
+
const tableIndex = parseInt(tableChoice) - 1;
|
|
613
|
+
customTool.tableName = availableTables[tableIndex] || tableChoice;
|
|
614
|
+
const tableInfo = state.dbTables.find((t) => t.name === customTool.tableName);
|
|
615
|
+
if (tableInfo) {
|
|
616
|
+
const scopeColumns = tableInfo.columns
|
|
617
|
+
.filter((col) => /user_id|project_id|account_id|org_id|team_id|owner_id/.test(col.name))
|
|
618
|
+
.map((col) => col.name);
|
|
619
|
+
if (scopeColumns.length > 0) {
|
|
620
|
+
console.log(`\n ${c("dim", "User scoping - filter results by:")}`);
|
|
621
|
+
scopeColumns.forEach((col, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${col}`));
|
|
622
|
+
console.log(` ${c("cyan", `${scopeColumns.length + 1}.`)} No scoping (public data)`);
|
|
623
|
+
const scopeChoice = await prompt.ask(` ${c("dim", ">")} `);
|
|
624
|
+
const scopeIndex = parseInt(scopeChoice) - 1;
|
|
625
|
+
if (scopeIndex >= 0 && scopeIndex < scopeColumns.length) {
|
|
626
|
+
customTool.scopeColumn = scopeColumns[scopeIndex];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
else if (sourceChoice !== "2" && availableRoutes.length > 0) {
|
|
632
|
+
console.log(`\n ${c("dim", "Available routes:")}`);
|
|
633
|
+
availableRoutes.forEach((r, i) => console.log(` ${c("cyan", `${i + 1}.`)} ${r}`));
|
|
634
|
+
const routeChoice = await prompt.ask(` ${c("dim", "Route number:")} `);
|
|
635
|
+
const routeIndex = parseInt(routeChoice) - 1;
|
|
636
|
+
if (routeIndex >= 0 && routeIndex < state.routes.length) {
|
|
637
|
+
customTool.routeMethod = state.routes[routeIndex].method;
|
|
638
|
+
customTool.routePath = state.routes[routeIndex].path;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
state.customTools.push(customTool);
|
|
642
|
+
console.log(`\n ${c("green", "+")} Added custom tool: ${c("bold", toolName)}`);
|
|
643
|
+
const addMore = await prompt.confirm(`\n Add another custom tool?`, false);
|
|
644
|
+
if (!addMore)
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
if (state.customTools.length > 0) {
|
|
648
|
+
console.log(`\n ${c("green", "+")} ${state.customTools.length} custom tool${state.customTools.length === 1 ? "" : "s"} defined.`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function generateCustomToolsFile(state) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push(`// Custom tools for your MCP server`);
|
|
654
|
+
lines.push(`// Hand-crafted tools that persist across re-scans`);
|
|
655
|
+
lines.push(`// Edit this file to customize tool behavior`);
|
|
656
|
+
lines.push(``);
|
|
657
|
+
if (state.isESM) {
|
|
658
|
+
lines.push(`import type { Express } from 'express';`);
|
|
659
|
+
}
|
|
660
|
+
lines.push(``);
|
|
661
|
+
lines.push(`export interface CustomToolDefinition {`);
|
|
662
|
+
lines.push(` name: string;`);
|
|
663
|
+
lines.push(` description: string;`);
|
|
664
|
+
lines.push(` inputSchema: Record<string, unknown>;`);
|
|
665
|
+
lines.push(` handler: (args: Record<string, unknown>) => Promise<unknown>;`);
|
|
666
|
+
lines.push(`}`);
|
|
667
|
+
lines.push(``);
|
|
668
|
+
lines.push(`export function getCustomTools(app: ${state.isESM ? "Express" : "any"}, dbUrl?: string): CustomToolDefinition[] {`);
|
|
669
|
+
lines.push(` const tools: CustomToolDefinition[] = [];`);
|
|
670
|
+
lines.push(``);
|
|
671
|
+
for (const tool of state.customTools) {
|
|
672
|
+
const properties = [];
|
|
673
|
+
const required = [];
|
|
674
|
+
for (const param of tool.params) {
|
|
675
|
+
properties.push(` ${param.name}: { type: "${param.type}", description: "${param.description.replace(/"/g, '\\"')}" }`);
|
|
676
|
+
if (param.required)
|
|
677
|
+
required.push(`"${param.name}"`);
|
|
678
|
+
}
|
|
679
|
+
lines.push(` tools.push({`);
|
|
680
|
+
lines.push(` name: "${tool.name}",`);
|
|
681
|
+
lines.push(` description: "${tool.description.replace(/"/g, '\\"')}",`);
|
|
682
|
+
lines.push(` inputSchema: {`);
|
|
683
|
+
lines.push(` type: "object",`);
|
|
684
|
+
lines.push(` properties: {`);
|
|
685
|
+
lines.push(properties.join(",\n"));
|
|
686
|
+
lines.push(` },`);
|
|
687
|
+
if (required.length > 0) {
|
|
688
|
+
lines.push(` required: [${required.join(", ")}],`);
|
|
689
|
+
}
|
|
690
|
+
lines.push(` },`);
|
|
691
|
+
if (tool.dataSource === "database" && tool.tableName) {
|
|
692
|
+
lines.push(` handler: async (args) => {`);
|
|
693
|
+
lines.push(` if (!dbUrl) return { error: "No database connection configured" };`);
|
|
694
|
+
lines.push(` const pg = await import("pg");`);
|
|
695
|
+
lines.push(` const client = new pg.default.Client({ connectionString: dbUrl });`);
|
|
696
|
+
lines.push(` try {`);
|
|
697
|
+
lines.push(` await client.connect();`);
|
|
698
|
+
if (tool.scopeColumn) {
|
|
699
|
+
lines.push(` const scopeValue = args.${tool.scopeColumn} as string;`);
|
|
700
|
+
lines.push(` if (!scopeValue) return { error: "${tool.scopeColumn} is required for scoped queries" };`);
|
|
701
|
+
lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} WHERE ${tool.scopeColumn} = $1 LIMIT 100\`, [scopeValue]);`);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
lines.push(` const result = await client.query(\`SELECT * FROM ${tool.tableName} LIMIT 100\`);`);
|
|
705
|
+
}
|
|
706
|
+
lines.push(` return result.rows;`);
|
|
707
|
+
lines.push(` } finally {`);
|
|
708
|
+
lines.push(` await client.end();`);
|
|
709
|
+
lines.push(` }`);
|
|
710
|
+
lines.push(` },`);
|
|
711
|
+
}
|
|
712
|
+
else if (tool.dataSource === "route" && tool.routePath) {
|
|
713
|
+
lines.push(` handler: async (args) => {`);
|
|
714
|
+
lines.push(` // TODO: Implement route-based handler`);
|
|
715
|
+
lines.push(` // This tool maps to ${tool.routeMethod} ${tool.routePath}`);
|
|
716
|
+
lines.push(` return { message: "Implement this handler to call your API" };`);
|
|
717
|
+
lines.push(` },`);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
lines.push(` handler: async (args) => {`);
|
|
721
|
+
lines.push(` // TODO: Implement custom handler`);
|
|
722
|
+
lines.push(` return { message: "Implement this handler" };`);
|
|
723
|
+
lines.push(` },`);
|
|
724
|
+
}
|
|
725
|
+
lines.push(` });`);
|
|
726
|
+
lines.push(``);
|
|
727
|
+
}
|
|
728
|
+
lines.push(` return tools;`);
|
|
729
|
+
lines.push(`}`);
|
|
730
|
+
lines.push(``);
|
|
731
|
+
return lines.join("\n");
|
|
732
|
+
}
|
|
733
|
+
function validateGeneratedFiles(mcpServerPath, entryFiles) {
|
|
734
|
+
const errors = [];
|
|
735
|
+
const allFiles = [mcpServerPath, ...entryFiles];
|
|
736
|
+
for (const filePath of allFiles) {
|
|
737
|
+
if (!fs.existsSync(filePath))
|
|
738
|
+
continue;
|
|
739
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
740
|
+
const relPath = path.basename(filePath);
|
|
741
|
+
let openBraces = 0;
|
|
742
|
+
let openParens = 0;
|
|
743
|
+
let openBrackets = 0;
|
|
744
|
+
let inString = null;
|
|
745
|
+
let inTemplateString = false;
|
|
746
|
+
for (let i = 0; i < content.length; i++) {
|
|
747
|
+
const ch = content[i];
|
|
748
|
+
const prev = i > 0 ? content[i - 1] : "";
|
|
749
|
+
if (inString) {
|
|
750
|
+
if (ch === inString && prev !== "\\")
|
|
751
|
+
inString = null;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (inTemplateString) {
|
|
755
|
+
if (ch === "`" && prev !== "\\")
|
|
756
|
+
inTemplateString = false;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (ch === "'" || ch === '"') {
|
|
760
|
+
inString = ch;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (ch === "`") {
|
|
764
|
+
inTemplateString = true;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
768
|
+
while (i < content.length && content[i] !== "\n")
|
|
769
|
+
i++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (ch === "{")
|
|
773
|
+
openBraces++;
|
|
774
|
+
else if (ch === "}")
|
|
775
|
+
openBraces--;
|
|
776
|
+
else if (ch === "(")
|
|
777
|
+
openParens++;
|
|
778
|
+
else if (ch === ")")
|
|
779
|
+
openParens--;
|
|
780
|
+
else if (ch === "[")
|
|
781
|
+
openBrackets++;
|
|
782
|
+
else if (ch === "]")
|
|
783
|
+
openBrackets--;
|
|
784
|
+
}
|
|
785
|
+
if (openBraces !== 0)
|
|
786
|
+
errors.push(`${relPath}: Unbalanced braces (${openBraces > 0 ? "missing }" : "extra }"})`);
|
|
787
|
+
if (openParens !== 0)
|
|
788
|
+
errors.push(`${relPath}: Unbalanced parentheses (${openParens > 0 ? "missing )" : "extra )"})`);
|
|
789
|
+
if (openBrackets !== 0)
|
|
790
|
+
errors.push(`${relPath}: Unbalanced brackets (${openBrackets > 0 ? "missing ]" : "extra ]"})`);
|
|
791
|
+
}
|
|
792
|
+
return errors;
|
|
793
|
+
}
|
|
482
794
|
async function stepGenerate(state) {
|
|
483
|
-
printStep(
|
|
484
|
-
const content = fs.readFileSync(state.entryFile, "utf-8");
|
|
485
|
-
const importLine = state.isESM ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
|
|
795
|
+
printStep(6, 6, "Generating MCP server");
|
|
486
796
|
const disabledRoutePaths = state.tools
|
|
487
797
|
.filter((t) => t.source === "route" && !t.enabled)
|
|
488
798
|
.map((t) => t.path);
|
|
@@ -500,7 +810,7 @@ async function stepGenerate(state) {
|
|
|
500
810
|
disabledTables.add(tableName);
|
|
501
811
|
}
|
|
502
812
|
const configParts = [];
|
|
503
|
-
configParts.push(`app
|
|
813
|
+
configParts.push(`app`);
|
|
504
814
|
if (disabledRoutes.length > 0) {
|
|
505
815
|
configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
|
|
506
816
|
}
|
|
@@ -518,38 +828,127 @@ async function stepGenerate(state) {
|
|
|
518
828
|
if (!state.enrichmentEnabled) {
|
|
519
829
|
configParts.push(`enrichment: false`);
|
|
520
830
|
}
|
|
521
|
-
|
|
831
|
+
const appFileDir = path.dirname(state.appFile);
|
|
832
|
+
const appFileBase = path.basename(state.appFile, path.extname(state.appFile));
|
|
833
|
+
const ext = state.isESM ? ".ts" : ".js";
|
|
834
|
+
const mcpServerPath = path.join(appFileDir, `mcp-server${ext}`);
|
|
835
|
+
const mcpServerRel = path.relative(state.root, mcpServerPath);
|
|
836
|
+
const hasCustomTools = state.customTools.length > 0;
|
|
837
|
+
const customToolsPath = path.join(appFileDir, `mcp-custom-tools${ext}`);
|
|
838
|
+
const customToolsExists = fs.existsSync(customToolsPath);
|
|
839
|
+
const importAppPath = `./${appFileBase}`;
|
|
840
|
+
let fileLines = [];
|
|
841
|
+
fileLines.push(`// Auto-generated by @hellocrossman/mcp-sdk`);
|
|
842
|
+
fileLines.push(`// This file connects your Express app to AI assistants (Claude, ChatGPT, Cursor)`);
|
|
843
|
+
fileLines.push(`// Safe to regenerate with: npx @hellocrossman/mcp-sdk init --force`);
|
|
844
|
+
fileLines.push(``);
|
|
845
|
+
if (state.isESM) {
|
|
846
|
+
if (state.appExportName === "default") {
|
|
847
|
+
fileLines.push(`import app from '${importAppPath}';`);
|
|
848
|
+
}
|
|
849
|
+
else if (state.appExportName === "app") {
|
|
850
|
+
fileLines.push(`import { app } from '${importAppPath}';`);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
fileLines.push(`import { ${state.appExportName} as app } from '${importAppPath}';`);
|
|
854
|
+
}
|
|
855
|
+
fileLines.push(`import { createMcpServer } from '@hellocrossman/mcp-sdk';`);
|
|
856
|
+
if (hasCustomTools || customToolsExists) {
|
|
857
|
+
fileLines.push(`import { getCustomTools } from './mcp-custom-tools';`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
if (state.appExportName === "default") {
|
|
862
|
+
fileLines.push(`const app = require('${importAppPath}');`);
|
|
863
|
+
}
|
|
864
|
+
else if (state.appExportName === "app") {
|
|
865
|
+
fileLines.push(`const { app } = require('${importAppPath}');`);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
fileLines.push(`const { ${state.appExportName}: app } = require('${importAppPath}');`);
|
|
869
|
+
}
|
|
870
|
+
fileLines.push(`const { createMcpServer } = require('@hellocrossman/mcp-sdk');`);
|
|
871
|
+
if (hasCustomTools || customToolsExists) {
|
|
872
|
+
fileLines.push(`const { getCustomTools } = require('./mcp-custom-tools');`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
fileLines.push(``);
|
|
876
|
+
if (hasCustomTools || customToolsExists) {
|
|
877
|
+
configParts.push(`customTools: getCustomTools(app, process.env.DATABASE_URL)`);
|
|
878
|
+
}
|
|
879
|
+
let configStr;
|
|
522
880
|
if (configParts.length <= 2) {
|
|
523
|
-
|
|
881
|
+
configStr = `createMcpServer({ ${configParts.join(", ")} });`;
|
|
524
882
|
}
|
|
525
883
|
else {
|
|
526
884
|
const indent = " ";
|
|
527
|
-
|
|
885
|
+
configStr = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
|
|
528
886
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
887
|
+
fileLines.push(configStr);
|
|
888
|
+
fileLines.push(``);
|
|
889
|
+
if (hasCustomTools) {
|
|
890
|
+
const customToolsContent = generateCustomToolsFile(state);
|
|
891
|
+
fs.writeFileSync(customToolsPath, customToolsContent);
|
|
535
892
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
893
|
+
const mcpServerContent = fileLines.join("\n");
|
|
894
|
+
const backups = [];
|
|
895
|
+
backups.push({ path: mcpServerPath, content: fs.existsSync(mcpServerPath) ? fs.readFileSync(mcpServerPath, "utf-8") : null });
|
|
896
|
+
fs.writeFileSync(mcpServerPath, mcpServerContent);
|
|
897
|
+
let addedImportTo = [];
|
|
898
|
+
const modifiedEntries = [];
|
|
899
|
+
for (const entryFile of state.entryFiles) {
|
|
900
|
+
const entryContent = fs.readFileSync(entryFile, "utf-8");
|
|
901
|
+
if (entryContent.includes("mcp-server")) {
|
|
902
|
+
addedImportTo.push(path.relative(state.root, entryFile));
|
|
903
|
+
continue;
|
|
542
904
|
}
|
|
905
|
+
backups.push({ path: entryFile, content: entryContent });
|
|
906
|
+
const entryDir = path.dirname(entryFile);
|
|
907
|
+
const importPath = `./${path.relative(entryDir, mcpServerPath).replace(/\.(ts|js)$/, "")}`;
|
|
908
|
+
const isEntryESM = checkIsESM(entryFile, state.root);
|
|
909
|
+
const importStatement = isEntryESM
|
|
910
|
+
? `import '${importPath}';`
|
|
911
|
+
: `require('${importPath}');`;
|
|
912
|
+
const entryLines = entryContent.split("\n");
|
|
913
|
+
let lastImportIndex = -1;
|
|
914
|
+
for (let i = 0; i < entryLines.length; i++) {
|
|
915
|
+
if (/^import\s/.test(entryLines[i]) || /^const\s.*=\s*require/.test(entryLines[i]) || /^require\s*\(/.test(entryLines[i])) {
|
|
916
|
+
lastImportIndex = i;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const comment = `// MCP server - exposes your API to AI assistants`;
|
|
920
|
+
if (lastImportIndex >= 0) {
|
|
921
|
+
entryLines.splice(lastImportIndex + 1, 0, comment, importStatement);
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
entryLines.unshift(comment, importStatement, "");
|
|
925
|
+
}
|
|
926
|
+
const newEntryContent = entryLines.join("\n");
|
|
927
|
+
fs.writeFileSync(entryFile, newEntryContent);
|
|
928
|
+
modifiedEntries.push({ path: entryFile, newContent: newEntryContent });
|
|
929
|
+
addedImportTo.push(path.relative(state.root, entryFile));
|
|
543
930
|
}
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
931
|
+
const validationErrors = validateGeneratedFiles(mcpServerPath, modifiedEntries.map((e) => e.path));
|
|
932
|
+
if (validationErrors.length > 0) {
|
|
933
|
+
console.log(`\n ${c("yellow", "!")} Validation failed. Reverting changes...`);
|
|
934
|
+
for (const err of validationErrors) {
|
|
935
|
+
console.log(` ${c("dim", "-")} ${err}`);
|
|
936
|
+
}
|
|
937
|
+
for (const backup of backups) {
|
|
938
|
+
if (backup.content === null) {
|
|
939
|
+
try {
|
|
940
|
+
fs.unlinkSync(backup.path);
|
|
941
|
+
}
|
|
942
|
+
catch { }
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
fs.writeFileSync(backup.path, backup.content);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
console.log(` ${c("green", "+")} Changes reverted. Your files are unchanged.`);
|
|
949
|
+
console.log(` ${c("dim", " Please report this issue at:")} ${c("cyan", "https://github.com/hellocrossman/mcp-sdk/issues")}`);
|
|
950
|
+
return;
|
|
550
951
|
}
|
|
551
|
-
fs.writeFileSync(state.entryFile, lines.join("\n"));
|
|
552
|
-
const relPath = path.relative(state.root, state.entryFile);
|
|
553
952
|
const enabledCount = state.tools.filter((t) => t.enabled).length;
|
|
554
953
|
console.log();
|
|
555
954
|
console.log(c("green", ` +------------------------------+`));
|
|
@@ -558,10 +957,17 @@ async function stepGenerate(state) {
|
|
|
558
957
|
console.log(c("green", ` | |`));
|
|
559
958
|
console.log(c("green", ` +------------------------------+`));
|
|
560
959
|
console.log();
|
|
561
|
-
console.log(` ${c("green", "\u2713")}
|
|
960
|
+
console.log(` ${c("green", "\u2713")} Created ${c("bold", mcpServerRel)}`);
|
|
961
|
+
if (hasCustomTools) {
|
|
962
|
+
console.log(` ${c("green", "\u2713")} Created ${c("bold", path.relative(state.root, customToolsPath))}`);
|
|
963
|
+
}
|
|
964
|
+
for (const entry of addedImportTo) {
|
|
965
|
+
console.log(` ${c("green", "\u2713")} Updated ${c("bold", entry)}`);
|
|
966
|
+
}
|
|
562
967
|
console.log();
|
|
563
968
|
console.log(` ${c("dim", "Configuration")}`);
|
|
564
969
|
console.log(` ${c("dim", "\u2502")} Tools enabled ${c("bold", String(enabledCount))}`);
|
|
970
|
+
console.log(` ${c("dim", "\u2502")} Custom tools ${state.customTools.length > 0 ? c("green", `${state.customTools.length} defined`) : c("dim", "none")}`);
|
|
565
971
|
console.log(` ${c("dim", "\u2502")} Database ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
566
972
|
console.log(` ${c("dim", "\u2502")} AI enrichment ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
567
973
|
console.log(` ${c("dim", "\u2502")} Write operations ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
@@ -633,13 +1039,16 @@ async function runWizard(specifiedFile, force) {
|
|
|
633
1039
|
const prompt = createPrompt();
|
|
634
1040
|
const state = {
|
|
635
1041
|
root: findProjectRoot(),
|
|
636
|
-
|
|
1042
|
+
appFile: "",
|
|
637
1043
|
appVarName: "app",
|
|
1044
|
+
appExportName: "app",
|
|
1045
|
+
entryFiles: [],
|
|
638
1046
|
isESM: false,
|
|
639
1047
|
routePrefix: "/api",
|
|
640
1048
|
routes: [],
|
|
641
1049
|
dbTables: [],
|
|
642
1050
|
tools: [],
|
|
1051
|
+
customTools: [],
|
|
643
1052
|
databaseEnabled: true,
|
|
644
1053
|
enrichmentEnabled: true,
|
|
645
1054
|
includeWrites: false,
|
|
@@ -653,6 +1062,7 @@ async function runWizard(specifiedFile, force) {
|
|
|
653
1062
|
await stepScanRoutes(state);
|
|
654
1063
|
await stepScanDatabase(state, prompt);
|
|
655
1064
|
await stepEnrichAndReview(state, prompt);
|
|
1065
|
+
await stepCustomTools(state, prompt);
|
|
656
1066
|
await stepGenerate(state);
|
|
657
1067
|
}
|
|
658
1068
|
catch (err) {
|