@hellocrossman/mcp-sdk 0.2.0 → 0.3.2
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 +528 -73
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/route-scan.d.ts +9 -0
- package/dist/cjs/route-scan.d.ts.map +1 -0
- package/dist/cjs/route-scan.js +140 -0
- package/dist/cjs/route-scan.js.map +1 -0
- package/dist/cli.js +505 -73
- package/dist/cli.js.map +1 -1
- package/dist/route-scan.d.ts +9 -0
- package/dist/route-scan.d.ts.map +1 -0
- package/dist/route-scan.js +133 -0
- package/dist/route-scan.js.map +1 -0
- package/package.json +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
3
26
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
27
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
28
|
};
|
|
6
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
30
|
const fs_1 = __importDefault(require("fs"));
|
|
8
31
|
const path_1 = __importDefault(require("path"));
|
|
32
|
+
const readline_1 = __importDefault(require("readline"));
|
|
33
|
+
const route_scan_js_1 = require("./route-scan.js");
|
|
9
34
|
const IMPORT_LINE_ESM = `import { createMcpServer } from '@hellocrossman/mcp-sdk';`;
|
|
10
35
|
const IMPORT_LINE_CJS = `const { createMcpServer } = require('@hellocrossman/mcp-sdk');`;
|
|
11
|
-
const SETUP_LINE = `createMcpServer({ app });`;
|
|
12
36
|
const COMMON_ENTRY_FILES = [
|
|
13
37
|
"server/index.ts",
|
|
14
38
|
"server/index.js",
|
|
@@ -33,12 +57,49 @@ const EXPRESS_PATTERNS = [
|
|
|
33
57
|
];
|
|
34
58
|
const APP_VAR_PATTERN = /(?:const|let|var)\s+(\w+)\s*=\s*express\(\)/;
|
|
35
59
|
const LISTEN_PATTERN = /(?:app|server|httpServer)\s*\.listen\s*\(/;
|
|
60
|
+
const COLORS = {
|
|
61
|
+
reset: "\x1b[0m",
|
|
62
|
+
bold: "\x1b[1m",
|
|
63
|
+
dim: "\x1b[2m",
|
|
64
|
+
green: "\x1b[32m",
|
|
65
|
+
yellow: "\x1b[33m",
|
|
66
|
+
blue: "\x1b[34m",
|
|
67
|
+
magenta: "\x1b[35m",
|
|
68
|
+
cyan: "\x1b[36m",
|
|
69
|
+
white: "\x1b[37m",
|
|
70
|
+
gray: "\x1b[90m",
|
|
71
|
+
bgGreen: "\x1b[42m",
|
|
72
|
+
bgYellow: "\x1b[43m",
|
|
73
|
+
};
|
|
74
|
+
function c(color, text) {
|
|
75
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
76
|
+
}
|
|
77
|
+
function createPrompt() {
|
|
78
|
+
const rl = readline_1.default.createInterface({
|
|
79
|
+
input: process.stdin,
|
|
80
|
+
output: process.stdout,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
ask: (question) => new Promise((resolve) => {
|
|
84
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
85
|
+
}),
|
|
86
|
+
confirm: async (question, defaultYes = true) => {
|
|
87
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
88
|
+
const answer = await new Promise((resolve) => {
|
|
89
|
+
rl.question(`${question} ${c("dim", `(${hint})`)} `, (a) => resolve(a.trim().toLowerCase()));
|
|
90
|
+
});
|
|
91
|
+
if (answer === "")
|
|
92
|
+
return defaultYes;
|
|
93
|
+
return answer === "y" || answer === "yes";
|
|
94
|
+
},
|
|
95
|
+
close: () => rl.close(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
36
98
|
function findProjectRoot() {
|
|
37
99
|
let dir = process.cwd();
|
|
38
100
|
while (dir !== path_1.default.dirname(dir)) {
|
|
39
|
-
if (fs_1.default.existsSync(path_1.default.join(dir, "package.json")))
|
|
101
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, "package.json")))
|
|
40
102
|
return dir;
|
|
41
|
-
}
|
|
42
103
|
dir = path_1.default.dirname(dir);
|
|
43
104
|
}
|
|
44
105
|
return process.cwd();
|
|
@@ -48,9 +109,8 @@ function findExpressEntryFile(root) {
|
|
|
48
109
|
const fullPath = path_1.default.join(root, file);
|
|
49
110
|
if (fs_1.default.existsSync(fullPath)) {
|
|
50
111
|
const content = fs_1.default.readFileSync(fullPath, "utf-8");
|
|
51
|
-
if (EXPRESS_PATTERNS.some((p) => p.test(content)))
|
|
112
|
+
if (EXPRESS_PATTERNS.some((p) => p.test(content)))
|
|
52
113
|
return fullPath;
|
|
53
|
-
}
|
|
54
114
|
}
|
|
55
115
|
}
|
|
56
116
|
const srcDirs = ["server", "src", "."];
|
|
@@ -62,14 +122,13 @@ function findExpressEntryFile(root) {
|
|
|
62
122
|
for (const file of files) {
|
|
63
123
|
const fullPath = path_1.default.join(dirPath, file);
|
|
64
124
|
const content = fs_1.default.readFileSync(fullPath, "utf-8");
|
|
65
|
-
if (EXPRESS_PATTERNS.some((p) => p.test(content)))
|
|
125
|
+
if (EXPRESS_PATTERNS.some((p) => p.test(content)))
|
|
66
126
|
return fullPath;
|
|
67
|
-
}
|
|
68
127
|
}
|
|
69
128
|
}
|
|
70
129
|
return null;
|
|
71
130
|
}
|
|
72
|
-
function
|
|
131
|
+
function checkIsESM(filePath, root) {
|
|
73
132
|
if (filePath.endsWith(".ts") || filePath.endsWith(".mjs"))
|
|
74
133
|
return true;
|
|
75
134
|
try {
|
|
@@ -81,20 +140,336 @@ function isESM(filePath, root) {
|
|
|
81
140
|
}
|
|
82
141
|
}
|
|
83
142
|
function alreadySetup(content) {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
return content.includes("createMcpServer") || content.includes("@hellocrossman/mcp-sdk");
|
|
144
|
+
}
|
|
145
|
+
function checkPgInstalled(root) {
|
|
146
|
+
return fs_1.default.existsSync(path_1.default.join(root, "node_modules", "pg"));
|
|
147
|
+
}
|
|
148
|
+
function detectPackageManager(root) {
|
|
149
|
+
if (fs_1.default.existsSync(path_1.default.join(root, "pnpm-lock.yaml")))
|
|
150
|
+
return "pnpm";
|
|
151
|
+
if (fs_1.default.existsSync(path_1.default.join(root, "yarn.lock")))
|
|
152
|
+
return "yarn";
|
|
153
|
+
return "npm";
|
|
154
|
+
}
|
|
155
|
+
function printHeader() {
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(c("cyan", ` __ __ _____ ____`));
|
|
158
|
+
console.log(c("cyan", ` | \\/ |/ ____| _ \\`));
|
|
159
|
+
console.log(c("cyan", ` | \\ / | | | |_) |`));
|
|
160
|
+
console.log(c("cyan", ` | |\\/| | | | __/`));
|
|
161
|
+
console.log(c("cyan", ` | | | | |____| |`));
|
|
162
|
+
console.log(c("cyan", ` |_| |_|\\_____|_|`) + c("dim", ` SDK`));
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(` ${c("bold", "@hellocrossman/mcp-sdk")}`);
|
|
165
|
+
console.log(` ${c("dim", "Turn your Express API into an MCP server")}`);
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(` ${c("dim", "This wizard will walk you through setup.")}`);
|
|
168
|
+
console.log(` ${c("dim", "Press Ctrl+C at any time to cancel.")}`);
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(c("dim", ` ${"~".repeat(46)}`));
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
function printStep(step, total, title) {
|
|
174
|
+
console.log();
|
|
175
|
+
const bar = Array.from({ length: total }, (_, i) => i < step ? c("cyan", "\u2501") : c("dim", "\u2501")).join("");
|
|
176
|
+
console.log(` ${bar} ${c("dim", `${step}/${total}`)}`);
|
|
177
|
+
console.log(` ${c("bold", title)}`);
|
|
178
|
+
console.log();
|
|
87
179
|
}
|
|
88
|
-
function
|
|
89
|
-
const
|
|
180
|
+
function printToolTable(tools) {
|
|
181
|
+
const routeTools = tools.filter((t) => t.source === "route");
|
|
182
|
+
const dbTools = tools.filter((t) => t.source === "database");
|
|
183
|
+
if (routeTools.length > 0) {
|
|
184
|
+
console.log(`\n ${c("dim", "API Routes:")}`);
|
|
185
|
+
for (const t of routeTools) {
|
|
186
|
+
const status = t.enabled ? c("green", "ON ") : c("dim", "OFF");
|
|
187
|
+
const method = t.method.padEnd(6);
|
|
188
|
+
console.log(` ${status} ${c("yellow", method)} ${t.name} ${c("dim", `- ${t.description}`)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (dbTools.length > 0) {
|
|
192
|
+
console.log(`\n ${c("dim", "Database Tables:")}`);
|
|
193
|
+
for (const t of dbTools) {
|
|
194
|
+
const status = t.enabled ? c("green", "ON ") : c("dim", "OFF");
|
|
195
|
+
const method = t.method.padEnd(6);
|
|
196
|
+
console.log(` ${status} ${c("magenta", method)} ${t.name} ${c("dim", `- ${t.description}`)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function stepDetectApp(state, prompt, specifiedFile) {
|
|
201
|
+
printStep(1, 5, "Detecting Express app");
|
|
202
|
+
let entryFile = null;
|
|
203
|
+
if (specifiedFile) {
|
|
204
|
+
const resolved = path_1.default.resolve(state.root, specifiedFile);
|
|
205
|
+
if (fs_1.default.existsSync(resolved)) {
|
|
206
|
+
entryFile = resolved;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log(` ${c("yellow", "!")} File not found: ${specifiedFile}`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
entryFile = findExpressEntryFile(state.root);
|
|
215
|
+
}
|
|
216
|
+
if (!entryFile) {
|
|
217
|
+
console.log(` ${c("yellow", "!")} Could not find an Express app file.`);
|
|
218
|
+
const custom = await prompt.ask(` Enter the path to your Express app file: `);
|
|
219
|
+
if (custom) {
|
|
220
|
+
const resolved = path_1.default.resolve(state.root, custom);
|
|
221
|
+
if (fs_1.default.existsSync(resolved)) {
|
|
222
|
+
entryFile = resolved;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(` ${c("yellow", "!")} File not found: ${custom}`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const content = fs_1.default.readFileSync(entryFile, "utf-8");
|
|
90
234
|
if (alreadySetup(content)) {
|
|
91
|
-
|
|
235
|
+
console.log(` ${c("green", "+")} MCP SDK is already set up in ${path_1.default.relative(state.root, entryFile)}`);
|
|
236
|
+
console.log(` ${c("dim", " Remove the existing createMcpServer() call to reconfigure.")}`);
|
|
237
|
+
return false;
|
|
92
238
|
}
|
|
93
|
-
const esm = isESM(filePath, root);
|
|
94
|
-
const importLine = esm ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
|
|
95
239
|
const appMatch = content.match(APP_VAR_PATTERN);
|
|
96
|
-
|
|
97
|
-
|
|
240
|
+
state.entryFile = entryFile;
|
|
241
|
+
state.appVarName = appMatch ? appMatch[1] : "app";
|
|
242
|
+
state.isESM = checkIsESM(entryFile, state.root);
|
|
243
|
+
console.log(` ${c("green", "+")} Found Express app: ${c("bold", path_1.default.relative(state.root, entryFile))}`);
|
|
244
|
+
console.log(` ${c("dim", ` Variable: ${state.appVarName}, Module: ${state.isESM ? "ESM" : "CommonJS"}`)}`);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
async function stepScanRoutes(state) {
|
|
248
|
+
printStep(2, 5, "Scanning API routes");
|
|
249
|
+
state.routes = (0, route_scan_js_1.scanAllRoutes)(state.entryFile, state.routePrefix);
|
|
250
|
+
if (state.routes.length === 0) {
|
|
251
|
+
console.log(` ${c("dim", " No routes found under")} ${state.routePrefix}`);
|
|
252
|
+
console.log(` ${c("dim", " Routes will be discovered at runtime when your app starts.")}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
console.log(` ${c("green", "+")} Found ${c("bold", String(state.routes.length))} API routes:\n`);
|
|
256
|
+
for (const route of state.routes) {
|
|
257
|
+
const method = route.method.padEnd(6);
|
|
258
|
+
console.log(` ${c("yellow", method)} ${route.path}`);
|
|
259
|
+
}
|
|
260
|
+
for (const route of state.routes) {
|
|
261
|
+
const name = generateToolName(route.method, route.path);
|
|
262
|
+
const description = generateToolDescription(route.method, route.path);
|
|
263
|
+
state.tools.push({
|
|
264
|
+
name,
|
|
265
|
+
description,
|
|
266
|
+
method: route.method,
|
|
267
|
+
path: route.path,
|
|
268
|
+
source: "route",
|
|
269
|
+
enabled: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function stepScanDatabase(state, prompt) {
|
|
274
|
+
printStep(3, 5, "Database discovery");
|
|
275
|
+
const wantDb = await prompt.confirm(` Scan your database for tables to expose as tools?`);
|
|
276
|
+
if (!wantDb) {
|
|
277
|
+
state.databaseEnabled = false;
|
|
278
|
+
console.log(` ${c("dim", " Skipping database discovery.")}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
let dbUrl = process.env.DATABASE_URL || process.env.POSTGRES_URL || process.env.PG_CONNECTION_STRING;
|
|
282
|
+
if (!dbUrl) {
|
|
283
|
+
console.log(` ${c("yellow", "!")} No DATABASE_URL found in environment.`);
|
|
284
|
+
dbUrl = (await prompt.ask(` Enter your PostgreSQL connection string (or press Enter to skip): `)) || undefined;
|
|
285
|
+
if (!dbUrl) {
|
|
286
|
+
state.databaseEnabled = false;
|
|
287
|
+
console.log(` ${c("dim", " Skipping database discovery.")}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!checkPgInstalled(state.root)) {
|
|
292
|
+
console.log(` ${c("yellow", "!")} The 'pg' package is required for database features.`);
|
|
293
|
+
const pm = detectPackageManager(state.root);
|
|
294
|
+
const installCmd = pm === "yarn" ? "yarn add pg" : pm === "pnpm" ? "pnpm add pg" : "npm install pg";
|
|
295
|
+
const installPg = await prompt.confirm(` Install pg now? (${c("dim", installCmd)})`);
|
|
296
|
+
if (installPg) {
|
|
297
|
+
console.log(` ${c("dim", ` Running ${installCmd}...`)}`);
|
|
298
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
299
|
+
try {
|
|
300
|
+
execSync(installCmd, { cwd: state.root, stdio: "pipe" });
|
|
301
|
+
console.log(` ${c("green", "+")} pg installed successfully.`);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
console.log(` ${c("yellow", "!")} Failed to install pg. Run '${installCmd}' manually.`);
|
|
305
|
+
state.databaseEnabled = false;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.log(` ${c("dim", ` Run '${installCmd}' to enable database features later.`)}`);
|
|
311
|
+
state.databaseEnabled = false;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const { introspectDatabase } = await Promise.resolve().then(() => __importStar(require("./db-introspect.js")));
|
|
317
|
+
const { isSensitiveTable } = await Promise.resolve().then(() => __importStar(require("./db-tool-generator.js")));
|
|
318
|
+
console.log(` ${c("dim", " Connecting to database...")}`);
|
|
319
|
+
const result = await introspectDatabase(dbUrl);
|
|
320
|
+
const safeTables = result.tables.filter((t) => !isSensitiveTable(t.name));
|
|
321
|
+
const hiddenTables = result.tables.filter((t) => isSensitiveTable(t.name));
|
|
322
|
+
state.dbTables = safeTables;
|
|
323
|
+
state.databaseEnabled = true;
|
|
324
|
+
console.log(` ${c("green", "+")} Found ${c("bold", String(result.tables.length))} tables (${safeTables.length} safe, ${hiddenTables.length} hidden)\n`);
|
|
325
|
+
if (hiddenTables.length > 0) {
|
|
326
|
+
console.log(` ${c("dim", " Auto-hidden (sensitive):")} ${hiddenTables.map((t) => t.name).join(", ")}`);
|
|
327
|
+
}
|
|
328
|
+
console.log(`\n ${c("dim", " Available tables:")}`);
|
|
329
|
+
for (const table of safeTables) {
|
|
330
|
+
const colCount = table.columns.length;
|
|
331
|
+
const pk = table.columns.find((col) => col.isPrimary);
|
|
332
|
+
console.log(` ${c("magenta", table.name)} ${c("dim", `(${colCount} columns${pk ? `, pk: ${pk.name}` : ""})`)}`);
|
|
333
|
+
state.tools.push({
|
|
334
|
+
name: `list_${table.name}`,
|
|
335
|
+
description: `List all ${formatTableName(table.name)} records with filtering and pagination`,
|
|
336
|
+
method: "QUERY",
|
|
337
|
+
path: `db://${table.name}`,
|
|
338
|
+
source: "database",
|
|
339
|
+
enabled: true,
|
|
340
|
+
tableName: table.name,
|
|
341
|
+
});
|
|
342
|
+
if (pk) {
|
|
343
|
+
state.tools.push({
|
|
344
|
+
name: `get_${table.name}_by_${pk.name}`,
|
|
345
|
+
description: `Get a specific ${formatTableName(table.name)} record by ${pk.name}`,
|
|
346
|
+
method: "QUERY",
|
|
347
|
+
path: `db://${table.name}/${pk.name}`,
|
|
348
|
+
source: "database",
|
|
349
|
+
enabled: true,
|
|
350
|
+
tableName: table.name,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
357
|
+
console.log(` ${c("yellow", "!")} Database scan failed: ${msg}`);
|
|
358
|
+
state.databaseEnabled = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function stepEnrichAndReview(state, prompt) {
|
|
362
|
+
printStep(4, 5, "AI enrichment & tool review");
|
|
363
|
+
if (state.tools.length === 0) {
|
|
364
|
+
console.log(` ${c("dim", " No tools discovered. Routes will be discovered at runtime.")}`);
|
|
365
|
+
state.enrichmentEnabled = true;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const wantEnrich = await prompt.confirm(` Use AI to improve tool names and descriptions?`);
|
|
369
|
+
if (wantEnrich) {
|
|
370
|
+
console.log(` ${c("dim", " Contacting enrichment service...")}`);
|
|
371
|
+
try {
|
|
372
|
+
const { enrichTools } = await Promise.resolve().then(() => __importStar(require("./enrichment.js")));
|
|
373
|
+
const toolSummaries = state.tools.map((t) => ({
|
|
374
|
+
name: t.name,
|
|
375
|
+
description: t.description,
|
|
376
|
+
method: t.method === "QUERY" ? "DB_QUERY" : t.method,
|
|
377
|
+
path: t.path,
|
|
378
|
+
inputSchema: {},
|
|
379
|
+
params: [],
|
|
380
|
+
}));
|
|
381
|
+
const enriched = await enrichTools(toolSummaries);
|
|
382
|
+
let enrichCount = 0;
|
|
383
|
+
for (let i = 0; i < state.tools.length; i++) {
|
|
384
|
+
if (enriched[i]) {
|
|
385
|
+
if (enriched[i].name)
|
|
386
|
+
state.tools[i].name = enriched[i].name;
|
|
387
|
+
if (enriched[i].description)
|
|
388
|
+
state.tools[i].description = enriched[i].description;
|
|
389
|
+
enrichCount++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
console.log(` ${c("green", "+")} Enhanced ${enrichCount} tool descriptions.`);
|
|
393
|
+
state.enrichmentEnabled = true;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
console.log(` ${c("yellow", "!")} Enrichment service unavailable. Using auto-generated names.`);
|
|
397
|
+
state.enrichmentEnabled = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
state.enrichmentEnabled = false;
|
|
402
|
+
}
|
|
403
|
+
console.log(`\n ${c("bold", "Discovered tools:")}`);
|
|
404
|
+
printToolTable(state.tools);
|
|
405
|
+
console.log();
|
|
406
|
+
const reviewMode = await prompt.ask(` Enable all tools, or review individually? ${c("dim", "(all/review)")} `);
|
|
407
|
+
if (reviewMode.toLowerCase() === "review" || reviewMode.toLowerCase() === "r") {
|
|
408
|
+
console.log();
|
|
409
|
+
for (let i = 0; i < state.tools.length; i++) {
|
|
410
|
+
const t = state.tools[i];
|
|
411
|
+
const source = t.source === "route" ? c("yellow", t.method) : c("magenta", "DB");
|
|
412
|
+
const enabled = await prompt.confirm(` ${source} ${c("bold", t.name)} ${c("dim", `- ${t.description}`)}\n Enable this tool?`);
|
|
413
|
+
state.tools[i].enabled = enabled;
|
|
414
|
+
}
|
|
415
|
+
const enabledCount = state.tools.filter((t) => t.enabled).length;
|
|
416
|
+
console.log(`\n ${c("green", "+")} ${enabledCount} of ${state.tools.length} tools enabled.`);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
console.log(` ${c("green", "+")} All ${state.tools.length} tools enabled.`);
|
|
420
|
+
}
|
|
421
|
+
if (state.databaseEnabled) {
|
|
422
|
+
const wantWrites = await prompt.confirm(`\n Enable write operations (create/insert) for database tables?`, false);
|
|
423
|
+
state.includeWrites = wantWrites;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function stepGenerate(state) {
|
|
427
|
+
printStep(5, 5, "Generating MCP server");
|
|
428
|
+
const content = fs_1.default.readFileSync(state.entryFile, "utf-8");
|
|
429
|
+
const importLine = state.isESM ? IMPORT_LINE_ESM : IMPORT_LINE_CJS;
|
|
430
|
+
const disabledRoutePaths = state.tools
|
|
431
|
+
.filter((t) => t.source === "route" && !t.enabled)
|
|
432
|
+
.map((t) => t.path);
|
|
433
|
+
const enabledRoutePaths = new Set(state.tools
|
|
434
|
+
.filter((t) => t.source === "route" && t.enabled)
|
|
435
|
+
.map((t) => t.path));
|
|
436
|
+
const disabledRoutes = disabledRoutePaths.filter((p) => !enabledRoutePaths.has(p));
|
|
437
|
+
const disabledTables = new Set();
|
|
438
|
+
const dbTools = state.tools.filter((t) => t.source === "database");
|
|
439
|
+
const tableNames = [...new Set(dbTools.map((t) => t.tableName))];
|
|
440
|
+
for (const tableName of tableNames) {
|
|
441
|
+
const tableTools = dbTools.filter((t) => t.tableName === tableName);
|
|
442
|
+
const allDisabled = tableTools.every((t) => !t.enabled);
|
|
443
|
+
if (allDisabled)
|
|
444
|
+
disabledTables.add(tableName);
|
|
445
|
+
}
|
|
446
|
+
const configParts = [];
|
|
447
|
+
configParts.push(`app: ${state.appVarName}`);
|
|
448
|
+
if (disabledRoutes.length > 0) {
|
|
449
|
+
configParts.push(`excludeRoutes: ${JSON.stringify(disabledRoutes)}`);
|
|
450
|
+
}
|
|
451
|
+
if (!state.databaseEnabled) {
|
|
452
|
+
configParts.push(`database: false`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (disabledTables.size > 0) {
|
|
456
|
+
configParts.push(`excludeTables: ${JSON.stringify([...disabledTables])}`);
|
|
457
|
+
}
|
|
458
|
+
if (state.includeWrites) {
|
|
459
|
+
configParts.push(`includeWrites: true`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!state.enrichmentEnabled) {
|
|
463
|
+
configParts.push(`enrichment: false`);
|
|
464
|
+
}
|
|
465
|
+
let setupCode;
|
|
466
|
+
if (configParts.length <= 2) {
|
|
467
|
+
setupCode = `createMcpServer({ ${configParts.join(", ")} });`;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const indent = " ";
|
|
471
|
+
setupCode = `createMcpServer({\n${configParts.map((p) => `${indent}${p},`).join("\n")}\n});`;
|
|
472
|
+
}
|
|
98
473
|
const lines = content.split("\n");
|
|
99
474
|
let lastImportIndex = -1;
|
|
100
475
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -102,8 +477,7 @@ function injectMcpSetup(filePath, root) {
|
|
|
102
477
|
lastImportIndex = i;
|
|
103
478
|
}
|
|
104
479
|
}
|
|
105
|
-
|
|
106
|
-
lines.splice(insertImportAt, 0, importLine);
|
|
480
|
+
lines.splice(lastImportIndex + 1, 0, importLine);
|
|
107
481
|
let listenIndex = -1;
|
|
108
482
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
109
483
|
if (LISTEN_PATTERN.test(lines[i])) {
|
|
@@ -111,72 +485,153 @@ function injectMcpSetup(filePath, root) {
|
|
|
111
485
|
break;
|
|
112
486
|
}
|
|
113
487
|
}
|
|
488
|
+
const commentLine = `// MCP server - exposes your API to AI assistants (Claude, ChatGPT, Cursor)`;
|
|
114
489
|
if (listenIndex >= 0) {
|
|
115
|
-
lines.splice(listenIndex, 0, "",
|
|
490
|
+
lines.splice(listenIndex, 0, "", commentLine, setupCode);
|
|
116
491
|
}
|
|
117
492
|
else {
|
|
118
|
-
lines.push("");
|
|
119
|
-
lines.push(`// MCP server - auto-discovered from your Express routes and database`);
|
|
120
|
-
lines.push(setupCode);
|
|
493
|
+
lines.push("", commentLine, setupCode);
|
|
121
494
|
}
|
|
122
|
-
fs_1.default.writeFileSync(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
495
|
+
fs_1.default.writeFileSync(state.entryFile, lines.join("\n"));
|
|
496
|
+
const relPath = path_1.default.relative(state.root, state.entryFile);
|
|
497
|
+
const enabledCount = state.tools.filter((t) => t.enabled).length;
|
|
498
|
+
console.log();
|
|
499
|
+
console.log(c("green", ` +------------------------------+`));
|
|
500
|
+
console.log(c("green", ` | |`));
|
|
501
|
+
console.log(c("green", ` | `) + c("bold", `MCP server configured!`) + c("green", ` |`));
|
|
502
|
+
console.log(c("green", ` | |`));
|
|
503
|
+
console.log(c("green", ` +------------------------------+`));
|
|
504
|
+
console.log();
|
|
505
|
+
console.log(` ${c("green", "\u2713")} Updated ${c("bold", relPath)}`);
|
|
506
|
+
console.log();
|
|
507
|
+
console.log(` ${c("dim", "Configuration")}`);
|
|
508
|
+
console.log(` ${c("dim", "\u2502")} Tools enabled ${c("bold", String(enabledCount))}`);
|
|
509
|
+
console.log(` ${c("dim", "\u2502")} Database ${state.databaseEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
510
|
+
console.log(` ${c("dim", "\u2502")} AI enrichment ${state.enrichmentEnabled ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
511
|
+
console.log(` ${c("dim", "\u2502")} Write operations ${state.includeWrites ? c("green", "enabled") : c("dim", "disabled")}`);
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(` ${c("bold", "What's next?")}`);
|
|
514
|
+
console.log();
|
|
515
|
+
console.log(` ${c("cyan", "1.")} Restart your app`);
|
|
516
|
+
console.log(` ${c("cyan", "2.")} Visit ${c("cyan", "http://localhost:3000/mcp")} to see your tools`);
|
|
517
|
+
console.log(` ${c("cyan", "3.")} Connect Claude, ChatGPT, or Cursor to your /mcp endpoint`);
|
|
518
|
+
console.log();
|
|
519
|
+
console.log(` ${c("dim", "Full guide:")} ${c("cyan", "https://mcp-skill-scanner.replit.app/guide")}`);
|
|
520
|
+
console.log();
|
|
127
521
|
}
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`
|
|
140
|
-
|
|
522
|
+
function generateToolName(method, routePath) {
|
|
523
|
+
const segments = routePath
|
|
524
|
+
.replace(/^\/api\/?/, "")
|
|
525
|
+
.split("/")
|
|
526
|
+
.filter((s) => s && !s.startsWith(":"));
|
|
527
|
+
const resource = segments.join("_") || "root";
|
|
528
|
+
const paramSegments = routePath.split("/").filter((s) => s.startsWith(":"));
|
|
529
|
+
switch (method.toUpperCase()) {
|
|
530
|
+
case "GET":
|
|
531
|
+
return paramSegments.length > 0
|
|
532
|
+
? `get_${resource}_by_${paramSegments[0].slice(1)}`
|
|
533
|
+
: `list_${resource}`;
|
|
534
|
+
case "POST":
|
|
535
|
+
return `create_${resource}`;
|
|
536
|
+
case "PUT":
|
|
537
|
+
case "PATCH":
|
|
538
|
+
return `update_${resource}`;
|
|
539
|
+
case "DELETE":
|
|
540
|
+
return `delete_${resource}`;
|
|
541
|
+
default:
|
|
542
|
+
return `${method.toLowerCase()}_${resource}`;
|
|
141
543
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
544
|
+
}
|
|
545
|
+
function generateToolDescription(method, routePath) {
|
|
546
|
+
const segments = routePath
|
|
547
|
+
.replace(/^\/api\/?/, "")
|
|
548
|
+
.split("/")
|
|
549
|
+
.filter((s) => s && !s.startsWith(":"));
|
|
550
|
+
const resource = segments.map((s) => s.replace(/[-_]/g, " ")).join(" ") || "resource";
|
|
551
|
+
const paramSegments = routePath.split("/").filter((s) => s.startsWith(":"));
|
|
552
|
+
switch (method.toUpperCase()) {
|
|
553
|
+
case "GET":
|
|
554
|
+
return paramSegments.length > 0
|
|
555
|
+
? `Get ${resource} by ${paramSegments[0].slice(1)}`
|
|
556
|
+
: `List all ${resource}`;
|
|
557
|
+
case "POST":
|
|
558
|
+
return `Create a new ${resource}`;
|
|
559
|
+
case "PUT":
|
|
560
|
+
case "PATCH":
|
|
561
|
+
return `Update ${resource}`;
|
|
562
|
+
case "DELETE":
|
|
563
|
+
return `Delete ${resource}`;
|
|
564
|
+
default:
|
|
565
|
+
return `${method} ${resource}`;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function formatTableName(name) {
|
|
569
|
+
return name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
570
|
+
}
|
|
571
|
+
async function runWizard(specifiedFile) {
|
|
572
|
+
printHeader();
|
|
573
|
+
const prompt = createPrompt();
|
|
574
|
+
const state = {
|
|
575
|
+
root: findProjectRoot(),
|
|
576
|
+
entryFile: "",
|
|
577
|
+
appVarName: "app",
|
|
578
|
+
isESM: false,
|
|
579
|
+
routePrefix: "/api",
|
|
580
|
+
routes: [],
|
|
581
|
+
dbTables: [],
|
|
582
|
+
tools: [],
|
|
583
|
+
databaseEnabled: true,
|
|
584
|
+
enrichmentEnabled: true,
|
|
585
|
+
includeWrites: false,
|
|
586
|
+
};
|
|
587
|
+
try {
|
|
588
|
+
const found = await stepDetectApp(state, prompt, specifiedFile);
|
|
589
|
+
if (!found) {
|
|
590
|
+
prompt.close();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
await stepScanRoutes(state);
|
|
594
|
+
await stepScanDatabase(state, prompt);
|
|
595
|
+
await stepEnrichAndReview(state, prompt);
|
|
596
|
+
await stepGenerate(state);
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
if (err instanceof Error && err.message.includes("readline was closed")) {
|
|
600
|
+
console.log(`\n ${c("dim", "Setup cancelled.")}\n`);
|
|
151
601
|
}
|
|
152
602
|
else {
|
|
153
|
-
|
|
154
|
-
process.exit(1);
|
|
603
|
+
throw err;
|
|
155
604
|
}
|
|
156
605
|
}
|
|
157
|
-
|
|
158
|
-
|
|
606
|
+
finally {
|
|
607
|
+
prompt.close();
|
|
159
608
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
console.log(`
|
|
169
|
-
console.log("");
|
|
170
|
-
console.log("
|
|
171
|
-
console.log(
|
|
172
|
-
console.log("
|
|
173
|
-
console.log("");
|
|
174
|
-
console.log(
|
|
609
|
+
}
|
|
610
|
+
function main() {
|
|
611
|
+
const args = process.argv.slice(2);
|
|
612
|
+
if (args[0] !== "init") {
|
|
613
|
+
console.log();
|
|
614
|
+
console.log(c("cyan", ` __ __ _____ ____`));
|
|
615
|
+
console.log(c("cyan", ` | \\/ |/ ____| _ \\`));
|
|
616
|
+
console.log(c("cyan", ` | \\ / | | | |_) |`));
|
|
617
|
+
console.log(c("cyan", ` | |\\/| | | | __/`));
|
|
618
|
+
console.log(c("cyan", ` | | | | |____| |`));
|
|
619
|
+
console.log(c("cyan", ` |_| |_|\\_____|_|`) + c("dim", ` SDK`));
|
|
620
|
+
console.log();
|
|
621
|
+
console.log(` ${c("bold", "Usage")}`);
|
|
622
|
+
console.log(` ${c("dim", "$")} npx @hellocrossman/mcp-sdk init`);
|
|
623
|
+
console.log();
|
|
624
|
+
console.log(` ${c("bold", "Options")}`);
|
|
625
|
+
console.log(` --file <path> Specify the Express app file`);
|
|
626
|
+
console.log();
|
|
627
|
+
return;
|
|
175
628
|
}
|
|
176
|
-
|
|
177
|
-
|
|
629
|
+
const fileArgIndex = args.indexOf("--file");
|
|
630
|
+
const specifiedFile = fileArgIndex >= 0 ? args[fileArgIndex + 1] : undefined;
|
|
631
|
+
runWizard(specifiedFile).catch((err) => {
|
|
632
|
+
console.error(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
178
633
|
process.exit(1);
|
|
179
|
-
}
|
|
634
|
+
});
|
|
180
635
|
}
|
|
181
636
|
main();
|
|
182
637
|
//# sourceMappingURL=cli.js.map
|