@hellocrossman/mcp-sdk 0.3.4 → 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 +541 -66
- 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 +541 -66
- 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
|
@@ -104,16 +104,46 @@ function findProjectRoot() {
|
|
|
104
104
|
}
|
|
105
105
|
return process.cwd();
|
|
106
106
|
}
|
|
107
|
+
function scoreExpressFile(filePath) {
|
|
108
|
+
const content = fs_1.default.readFileSync(filePath, "utf-8");
|
|
109
|
+
const hasExpressImport = EXPRESS_PATTERNS.some((p) => p.test(content));
|
|
110
|
+
if (!hasExpressImport)
|
|
111
|
+
return null;
|
|
112
|
+
let score = 0;
|
|
113
|
+
const hasAppCreation = APP_VAR_PATTERN.test(content);
|
|
114
|
+
const hasListen = LISTEN_PATTERN.test(content);
|
|
115
|
+
const hasRoutes = /\.\s*(?:get|post|put|patch|delete)\s*\(/.test(content);
|
|
116
|
+
const hasAppParam = /function\s+\w+\s*\(\s*app\b/.test(content) ||
|
|
117
|
+
/\(\s*app\s*:\s*\w*Express/.test(content) ||
|
|
118
|
+
/\(\s*app\s*:\s*\w*Application/.test(content) ||
|
|
119
|
+
/=>\s*\{[^}]*app\s*\./.test(content);
|
|
120
|
+
const isNamedEntryish = /(?:index|app|server|main)\.(ts|js|mjs)$/.test(filePath);
|
|
121
|
+
if (hasAppCreation)
|
|
122
|
+
score += 10;
|
|
123
|
+
if (hasListen)
|
|
124
|
+
score += 8;
|
|
125
|
+
if (isNamedEntryish)
|
|
126
|
+
score += 3;
|
|
127
|
+
if (hasRoutes)
|
|
128
|
+
score += 2;
|
|
129
|
+
if (!hasAppCreation && hasAppParam)
|
|
130
|
+
score -= 5;
|
|
131
|
+
if (score <= 0)
|
|
132
|
+
return null;
|
|
133
|
+
return { path: filePath, score, hasAppCreation, hasListen, hasRoutes };
|
|
134
|
+
}
|
|
107
135
|
function findExpressEntryFile(root) {
|
|
136
|
+
const candidates = [];
|
|
108
137
|
for (const file of COMMON_ENTRY_FILES) {
|
|
109
138
|
const fullPath = path_1.default.join(root, file);
|
|
110
139
|
if (fs_1.default.existsSync(fullPath)) {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
140
|
+
const candidate = scoreExpressFile(fullPath);
|
|
141
|
+
if (candidate)
|
|
142
|
+
candidates.push(candidate);
|
|
114
143
|
}
|
|
115
144
|
}
|
|
116
145
|
const srcDirs = ["server", "src", "."];
|
|
146
|
+
const seen = new Set(candidates.map((c) => c.path));
|
|
117
147
|
for (const dir of srcDirs) {
|
|
118
148
|
const dirPath = path_1.default.join(root, dir);
|
|
119
149
|
if (!fs_1.default.existsSync(dirPath) || !fs_1.default.statSync(dirPath).isDirectory())
|
|
@@ -121,12 +151,18 @@ function findExpressEntryFile(root) {
|
|
|
121
151
|
const files = fs_1.default.readdirSync(dirPath).filter((f) => /\.(ts|js|mjs)$/.test(f));
|
|
122
152
|
for (const file of files) {
|
|
123
153
|
const fullPath = path_1.default.join(dirPath, file);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
if (seen.has(fullPath))
|
|
155
|
+
continue;
|
|
156
|
+
seen.add(fullPath);
|
|
157
|
+
const candidate = scoreExpressFile(fullPath);
|
|
158
|
+
if (candidate)
|
|
159
|
+
candidates.push(candidate);
|
|
127
160
|
}
|
|
128
161
|
}
|
|
129
|
-
|
|
162
|
+
if (candidates.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
165
|
+
return candidates[0].path;
|
|
130
166
|
}
|
|
131
167
|
function checkIsESM(filePath, root) {
|
|
132
168
|
if (filePath.endsWith(".ts") || filePath.endsWith(".mjs"))
|
|
@@ -139,8 +175,29 @@ function checkIsESM(filePath, root) {
|
|
|
139
175
|
return false;
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
|
-
function alreadySetup(
|
|
143
|
-
|
|
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;
|
|
144
201
|
}
|
|
145
202
|
function checkPgInstalled(root) {
|
|
146
203
|
return fs_1.default.existsSync(path_1.default.join(root, "node_modules", "pg"));
|
|
@@ -207,13 +264,81 @@ function stripExistingMcpConfig(content) {
|
|
|
207
264
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
208
265
|
return result;
|
|
209
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
|
+
}
|
|
210
319
|
async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
211
|
-
printStep(1,
|
|
212
|
-
|
|
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;
|
|
213
338
|
if (specifiedFile) {
|
|
214
339
|
const resolved = path_1.default.resolve(state.root, specifiedFile);
|
|
215
340
|
if (fs_1.default.existsSync(resolved)) {
|
|
216
|
-
|
|
341
|
+
appFile = resolved;
|
|
217
342
|
}
|
|
218
343
|
else {
|
|
219
344
|
console.log(` ${c("yellow", "!")} File not found: ${specifiedFile}`);
|
|
@@ -221,15 +346,45 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
221
346
|
}
|
|
222
347
|
}
|
|
223
348
|
else {
|
|
224
|
-
|
|
349
|
+
appFile = findExpressEntryFile(state.root);
|
|
350
|
+
}
|
|
351
|
+
if (appFile && !specifiedFile) {
|
|
352
|
+
const relPath = path_1.default.relative(state.root, appFile);
|
|
353
|
+
const content = fs_1.default.readFileSync(appFile, "utf-8");
|
|
354
|
+
const hasCreation = APP_VAR_PATTERN.test(content);
|
|
355
|
+
const hasListen = LISTEN_PATTERN.test(content);
|
|
356
|
+
const signals = [];
|
|
357
|
+
if (hasCreation)
|
|
358
|
+
signals.push("creates Express app");
|
|
359
|
+
if (hasListen)
|
|
360
|
+
signals.push("calls .listen()");
|
|
361
|
+
const signalStr = signals.length > 0 ? ` ${c("dim", `(${signals.join(", ")})`)}` : "";
|
|
362
|
+
console.log(` ${c("green", "+")} Found Express app: ${c("bold", relPath)}${signalStr}`);
|
|
363
|
+
const useIt = await prompt.confirm(` Is this the right file?`);
|
|
364
|
+
if (!useIt) {
|
|
365
|
+
const custom = await prompt.ask(` Enter the path to your Express app file: `);
|
|
366
|
+
if (custom) {
|
|
367
|
+
const resolved = path_1.default.resolve(state.root, custom);
|
|
368
|
+
if (fs_1.default.existsSync(resolved)) {
|
|
369
|
+
appFile = resolved;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
console.log(` ${c("yellow", "!")} File not found: ${custom}`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
225
380
|
}
|
|
226
|
-
if (!
|
|
381
|
+
if (!appFile) {
|
|
227
382
|
console.log(` ${c("yellow", "!")} Could not find an Express app file.`);
|
|
228
383
|
const custom = await prompt.ask(` Enter the path to your Express app file: `);
|
|
229
384
|
if (custom) {
|
|
230
385
|
const resolved = path_1.default.resolve(state.root, custom);
|
|
231
386
|
if (fs_1.default.existsSync(resolved)) {
|
|
232
|
-
|
|
387
|
+
appFile = resolved;
|
|
233
388
|
}
|
|
234
389
|
else {
|
|
235
390
|
console.log(` ${c("yellow", "!")} File not found: ${custom}`);
|
|
@@ -240,32 +395,23 @@ async function stepDetectApp(state, prompt, specifiedFile, force) {
|
|
|
240
395
|
return false;
|
|
241
396
|
}
|
|
242
397
|
}
|
|
243
|
-
|
|
244
|
-
if (alreadySetup(content)) {
|
|
245
|
-
if (force) {
|
|
246
|
-
console.log(` ${c("yellow", "~")} Found existing MCP config in ${path_1.default.relative(state.root, entryFile)}`);
|
|
247
|
-
console.log(` ${c("dim", " Removing old configuration for re-scan...")}`);
|
|
248
|
-
content = stripExistingMcpConfig(content);
|
|
249
|
-
fs_1.default.writeFileSync(entryFile, content);
|
|
250
|
-
console.log(` ${c("green", "+")} Old config removed. Starting fresh.`);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log(` ${c("green", "+")} MCP SDK is already set up in ${path_1.default.relative(state.root, entryFile)}`);
|
|
254
|
-
console.log(` ${c("dim", " Run with")} ${c("cyan", "--force")} ${c("dim", "to re-scan and reconfigure.")}`);
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
398
|
+
const content = fs_1.default.readFileSync(appFile, "utf-8");
|
|
258
399
|
const appMatch = content.match(APP_VAR_PATTERN);
|
|
259
|
-
state.
|
|
400
|
+
state.appFile = appFile;
|
|
260
401
|
state.appVarName = appMatch ? appMatch[1] : "app";
|
|
261
|
-
state.
|
|
262
|
-
|
|
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;
|
|
263
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
|
+
}
|
|
264
410
|
return true;
|
|
265
411
|
}
|
|
266
412
|
async function stepScanRoutes(state) {
|
|
267
|
-
printStep(2,
|
|
268
|
-
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);
|
|
269
415
|
if (state.routes.length === 0) {
|
|
270
416
|
console.log(` ${c("dim", " No routes found under")} ${state.routePrefix}`);
|
|
271
417
|
console.log(` ${c("dim", " Routes will be discovered at runtime when your app starts.")}`);
|
|
@@ -290,7 +436,7 @@ async function stepScanRoutes(state) {
|
|
|
290
436
|
}
|
|
291
437
|
}
|
|
292
438
|
async function stepScanDatabase(state, prompt) {
|
|
293
|
-
printStep(3,
|
|
439
|
+
printStep(3, 6, "Database discovery");
|
|
294
440
|
const wantDb = await prompt.confirm(` Scan your database for tables to expose as tools?`);
|
|
295
441
|
if (!wantDb) {
|
|
296
442
|
state.databaseEnabled = false;
|
|
@@ -378,7 +524,7 @@ async function stepScanDatabase(state, prompt) {
|
|
|
378
524
|
}
|
|
379
525
|
}
|
|
380
526
|
async function stepEnrichAndReview(state, prompt) {
|
|
381
|
-
printStep(4,
|
|
527
|
+
printStep(4, 6, "AI enrichment & tool review");
|
|
382
528
|
if (state.tools.length === 0) {
|
|
383
529
|
console.log(` ${c("dim", " No tools discovered. Routes will be discovered at runtime.")}`);
|
|
384
530
|
state.enrichmentEnabled = true;
|
|
@@ -442,10 +588,239 @@ async function stepEnrichAndReview(state, prompt) {
|
|
|
442
588
|
state.includeWrites = wantWrites;
|
|
443
589
|
}
|
|
444
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
|
+
}
|
|
445
822
|
async function stepGenerate(state) {
|
|
446
|
-
printStep(
|
|
447
|
-
const content = fs_1.default.readFileSync(state.entryFile, "utf-8");
|
|
448
|
-
const importLine = state.isESM ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
|
|
823
|
+
printStep(6, 6, "Generating MCP server");
|
|
449
824
|
const disabledRoutePaths = state.tools
|
|
450
825
|
.filter((t) => t.source === "route" && !t.enabled)
|
|
451
826
|
.map((t) => t.path);
|
|
@@ -463,7 +838,7 @@ async function stepGenerate(state) {
|
|
|
463
838
|
disabledTables.add(tableName);
|
|
464
839
|
}
|
|
465
840
|
const configParts = [];
|
|
466
|
-
configParts.push(`app
|
|
841
|
+
configParts.push(`app`);
|
|
467
842
|
if (disabledRoutes.length > 0) {
|
|
468
843
|
configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
|
|
469
844
|
}
|
|
@@ -481,38 +856,127 @@ async function stepGenerate(state) {
|
|
|
481
856
|
if (!state.enrichmentEnabled) {
|
|
482
857
|
configParts.push(`enrichment: false`);
|
|
483
858
|
}
|
|
484
|
-
|
|
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;
|
|
485
908
|
if (configParts.length <= 2) {
|
|
486
|
-
|
|
909
|
+
configStr = `createMcpServer({ ${configParts.join(", ")} });`;
|
|
487
910
|
}
|
|
488
911
|
else {
|
|
489
912
|
const indent = " ";
|
|
490
|
-
|
|
913
|
+
configStr = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
|
|
491
914
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
915
|
+
fileLines.push(configStr);
|
|
916
|
+
fileLines.push(``);
|
|
917
|
+
if (hasCustomTools) {
|
|
918
|
+
const customToolsContent = generateCustomToolsFile(state);
|
|
919
|
+
fs_1.default.writeFileSync(customToolsPath, customToolsContent);
|
|
498
920
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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;
|
|
505
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));
|
|
506
958
|
}
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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;
|
|
513
979
|
}
|
|
514
|
-
fs_1.default.writeFileSync(state.entryFile, lines.join("\n"));
|
|
515
|
-
const relPath = path_1.default.relative(state.root, state.entryFile);
|
|
516
980
|
const enabledCount = state.tools.filter((t) => t.enabled).length;
|
|
517
981
|
console.log();
|
|
518
982
|
console.log(c("green", ` +------------------------------+`));
|
|
@@ -521,10 +985,17 @@ async function stepGenerate(state) {
|
|
|
521
985
|
console.log(c("green", ` | |`));
|
|
522
986
|
console.log(c("green", ` +------------------------------+`));
|
|
523
987
|
console.log();
|
|
524
|
-
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
|
+
}
|
|
525
995
|
console.log();
|
|
526
996
|
console.log(` ${c("dim", "Configuration")}`);
|
|
527
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")}`);
|
|
528
999
|
console.log(` ${c("dim", "\u2502")} Database ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
529
1000
|
console.log(` ${c("dim", "\u2502")} AI enrichment ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
530
1001
|
console.log(` ${c("dim", "\u2502")} Write operations ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
@@ -596,13 +1067,16 @@ async function runWizard(specifiedFile, force) {
|
|
|
596
1067
|
const prompt = createPrompt();
|
|
597
1068
|
const state = {
|
|
598
1069
|
root: findProjectRoot(),
|
|
599
|
-
|
|
1070
|
+
appFile: "",
|
|
600
1071
|
appVarName: "app",
|
|
1072
|
+
appExportName: "app",
|
|
1073
|
+
entryFiles: [],
|
|
601
1074
|
isESM: false,
|
|
602
1075
|
routePrefix: "/api",
|
|
603
1076
|
routes: [],
|
|
604
1077
|
dbTables: [],
|
|
605
1078
|
tools: [],
|
|
1079
|
+
customTools: [],
|
|
606
1080
|
databaseEnabled: true,
|
|
607
1081
|
enrichmentEnabled: true,
|
|
608
1082
|
includeWrites: false,
|
|
@@ -616,6 +1090,7 @@ async function runWizard(specifiedFile, force) {
|
|
|
616
1090
|
await stepScanRoutes(state);
|
|
617
1091
|
await stepScanDatabase(state, prompt);
|
|
618
1092
|
await stepEnrichAndReview(state, prompt);
|
|
1093
|
+
await stepCustomTools(state, prompt);
|
|
619
1094
|
await stepGenerate(state);
|
|
620
1095
|
}
|
|
621
1096
|
catch (err) {
|