webspresso 0.0.91 → 0.0.94

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.
Files changed (86) hide show
  1. package/README.md +103 -2864
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/polar-migrate.js +50 -0
  9. package/bin/commands/seed.js +7 -1
  10. package/bin/commands/service.js +8 -2
  11. package/bin/commands/skill.js +7 -1
  12. package/bin/webspresso.js +4 -0
  13. package/core/auth/tokens.js +16 -0
  14. package/core/compileSchema.js +1 -5
  15. package/core/compression/compressible.js +17 -5
  16. package/core/compression/compression-stream.js +1 -1
  17. package/core/compression/middleware.js +9 -0
  18. package/core/compression/negotiate.js +23 -11
  19. package/core/kernel/app.js +2 -1
  20. package/core/kernel/index.js +5 -1
  21. package/core/orm/cache/fingerprint.js +23 -5
  22. package/core/orm/cache/layer.js +1 -1
  23. package/core/orm/eager-loader.js +54 -9
  24. package/core/orm/index.js +42 -3
  25. package/core/orm/model.js +11 -2
  26. package/core/orm/transaction.js +3 -1
  27. package/core/queue/adapters/database.js +46 -13
  28. package/core/queue/adapters/memory.js +21 -0
  29. package/core/queue/adapters/redis.js +21 -0
  30. package/core/queue/manager.js +46 -20
  31. package/core/ssr/stream.js +58 -14
  32. package/core/validation/index.js +70 -0
  33. package/index.d.ts +99 -5
  34. package/index.js +7 -1
  35. package/package.json +9 -1
  36. package/plugins/admin-panel/auth.js +10 -0
  37. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +6 -1
  38. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +18 -8
  39. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  40. package/plugins/admin-panel/client/parts/08-record-list.js +20 -2
  41. package/plugins/admin-panel/index.js +65 -6
  42. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  43. package/plugins/admin-panel/modules/dashboard.js +8 -1
  44. package/plugins/content/index.js +5 -3
  45. package/plugins/data-exchange/export-xlsx.js +28 -6
  46. package/plugins/data-exchange/import.js +120 -58
  47. package/plugins/data-exchange/index.js +5 -0
  48. package/plugins/data-exchange/parse-table.js +14 -5
  49. package/plugins/data-exchange/record-selection.js +2 -0
  50. package/plugins/index.js +6 -0
  51. package/plugins/mcp/auth.js +104 -0
  52. package/plugins/mcp/discovery.js +88 -0
  53. package/plugins/mcp/index.js +208 -0
  54. package/plugins/mcp/prompts.js +149 -0
  55. package/plugins/mcp/providers/orm.js +325 -0
  56. package/plugins/mcp/providers/services.js +190 -0
  57. package/plugins/mcp/providers/system.js +87 -0
  58. package/plugins/mcp/server.js +472 -0
  59. package/plugins/mcp/transports/sse.js +156 -0
  60. package/plugins/mcp/transports/stdio.js +108 -0
  61. package/plugins/polar/.env.example +12 -0
  62. package/plugins/polar/CHANGELOG.md +38 -0
  63. package/plugins/polar/README.md +158 -0
  64. package/plugins/polar/index.js +144 -0
  65. package/plugins/polar/src/api.js +54 -0
  66. package/plugins/polar/src/checkout.js +111 -0
  67. package/plugins/polar/src/config.js +88 -0
  68. package/plugins/polar/src/env.schema.js +36 -0
  69. package/plugins/polar/src/middleware.js +37 -0
  70. package/plugins/polar/src/migration.js +58 -0
  71. package/plugins/polar/src/nunjucks-filters.js +56 -0
  72. package/plugins/polar/src/portal.js +51 -0
  73. package/plugins/polar/src/rate-limit.js +70 -0
  74. package/plugins/polar/src/routes.js +212 -0
  75. package/plugins/polar/src/sync.js +138 -0
  76. package/plugins/polar/src/urls.js +117 -0
  77. package/plugins/polar/src/user-resolver.js +195 -0
  78. package/plugins/polar/src/webhooks.js +248 -0
  79. package/src/file-router.js +203 -125
  80. package/src/helpers.js +60 -13
  81. package/src/njk-frontmatter.js +24 -14
  82. package/src/server.js +30 -12
  83. package/src/services/discovery.js +2 -1
  84. package/src/services/registry.js +2 -0
  85. package/src/services/validator.js +2 -53
  86. package/templates/skills/webspresso-usage/REFERENCE-framework.md +7 -4
@@ -10,13 +10,18 @@ const { buildHeaderMapping, dataRowsToObjects } = require('./parse-table');
10
10
 
11
11
  function allowedImportColumns(model) {
12
12
  const hidden = new Set(model.hidden || []);
13
- if (model.columns && model.columns.size > 0) {
14
- return Array.from(model.columns.keys()).filter((c) => !hidden.has(c));
13
+ const cols = new Set();
14
+ if (model.columns && typeof model.columns.keys === 'function') {
15
+ for (const c of model.columns.keys()) {
16
+ if (!hidden.has(c)) cols.add(c);
17
+ }
15
18
  }
16
19
  if (model.schema && model.schema.shape) {
17
- return Object.keys(model.schema.shape).filter((c) => !hidden.has(c));
20
+ for (const c of Object.keys(model.schema.shape)) {
21
+ if (!hidden.has(c)) cols.add(c);
22
+ }
18
23
  }
19
- return [];
24
+ return Array.from(cols);
20
25
  }
21
26
 
22
27
  /**
@@ -35,12 +40,12 @@ function coerceCell(raw, meta, column) {
35
40
  }
36
41
 
37
42
  if (!meta || !meta.type) {
38
- if (raw === 'true' || raw === '1' || raw === 'yes') return true;
39
- if (raw === 'false' || raw === '0' || raw === 'no') return false;
40
43
  if (typeof raw === 'string' && /^-?\d+(\.\d+)?$/.test(raw)) {
41
44
  const num = Number(raw);
42
45
  if (!Number.isNaN(num)) return num;
43
46
  }
47
+ if (raw === 'true' || raw === 'yes') return true;
48
+ if (raw === 'false' || raw === 'no') return false;
44
49
  return raw;
45
50
  }
46
51
 
@@ -66,6 +71,14 @@ function coerceCell(raw, meta, column) {
66
71
  if (s === 'false' || s === '0' || s === 'no') return false;
67
72
  throw new Error(`Invalid boolean for ${column}`);
68
73
  }
74
+ case 'date':
75
+ case 'datetime':
76
+ case 'timestamp': {
77
+ if (raw instanceof Date) return raw;
78
+ const d = new Date(raw);
79
+ if (Number.isNaN(d.getTime())) throw new Error(`Invalid date for ${column}`);
80
+ return raw;
81
+ }
69
82
  case 'json': {
70
83
  if (typeof raw === 'object') return raw;
71
84
  try {
@@ -84,7 +97,7 @@ function buildPayloadForRow(model, data, mode) {
84
97
  const payload = {};
85
98
  for (const [col, raw] of Object.entries(data)) {
86
99
  if (!allowed.has(col)) continue;
87
- const meta = model.columns.get(col);
100
+ const meta = model.columns?.get ? model.columns.get(col) : undefined;
88
101
  let v;
89
102
  try {
90
103
  v = coerceCell(raw, meta, col);
@@ -95,14 +108,14 @@ function buildPayloadForRow(model, data, mode) {
95
108
  payload[col] = v;
96
109
  }
97
110
 
98
- const pk = model.primaryKey;
99
- const pkMeta = model.columns.get(pk);
100
- if (
101
- mode === 'insert' &&
102
- pkMeta?.autoIncrement &&
103
- (payload[pk] === undefined || payload[pk] === null || payload[pk] === '')
104
- ) {
105
- delete payload[pk];
111
+ const pk = model.primaryKey || 'id';
112
+ const pkMeta = model.columns?.get ? model.columns.get(pk) : undefined;
113
+ if (mode === 'insert') {
114
+ // In insert mode, remove auto-increment / auto-generated primary key
115
+ // so new records are created without unique constraint collision
116
+ if (pkMeta?.autoIncrement || pkMeta?.auto === 'create' || payload[pk] === undefined || payload[pk] === null || payload[pk] === '') {
117
+ delete payload[pk];
118
+ }
106
119
  }
107
120
 
108
121
  return payload;
@@ -139,15 +152,26 @@ async function parseXlsxToRows(buffer) {
139
152
  }
140
153
 
141
154
  function cellValueToPlain(cell) {
155
+ if (!cell) return '';
142
156
  const v = cell.value;
143
157
  if (v === null || v === undefined) return '';
144
158
  if (v instanceof Date) return v.toISOString();
145
- if (typeof v === 'object' && v !== null) {
159
+ if (typeof v === 'object') {
160
+ if (v.error) return '';
146
161
  if ('text' in v && typeof v.text === 'string') return v.text;
147
162
  if ('richText' in v && Array.isArray(v.richText)) {
148
- return v.richText.map((t) => t.text || '').join('');
163
+ return v.richText.map((t) => (t && t.text) ? t.text : '').join('');
164
+ }
165
+ if ('result' in v && v.result !== undefined) {
166
+ return cellValueToPlain({ value: v.result });
167
+ }
168
+ if ('hyperlink' in v) {
169
+ return v.text || v.hyperlink || '';
149
170
  }
150
- if ('result' in v && v.result !== undefined) return v.result;
171
+ if (typeof v.toString === 'function' && v.toString !== Object.prototype.toString) {
172
+ return v.toString();
173
+ }
174
+ return '';
151
175
  }
152
176
  return v;
153
177
  }
@@ -191,7 +215,8 @@ function createImportHandler(opts) {
191
215
  let upsertKey = req.query.upsertKey || req.body?.upsertKey || model.primaryKey;
192
216
  upsertKey = String(upsertKey);
193
217
 
194
- if (!model.columns.has(upsertKey)) {
218
+ const allowedCols = allowedImportColumns(model);
219
+ if (!allowedCols.includes(upsertKey)) {
195
220
  return res.status(400).json({ error: `upsertKey "${upsertKey}" is not a column` });
196
221
  }
197
222
  if (model.hidden?.includes(upsertKey)) {
@@ -226,6 +251,13 @@ function createImportHandler(opts) {
226
251
  return res.status(400).json({ error: `Too many data rows (${objects.length}). Limit is ${maxRows}.` });
227
252
  }
228
253
 
254
+ const isAtomic =
255
+ req.query.atomic === 'true' ||
256
+ req.query.atomic === '1' ||
257
+ req.body?.atomic === 'true' ||
258
+ req.body?.atomic === true ||
259
+ opts?.atomic === true;
260
+
229
261
  const repo = db.getRepository(model.name);
230
262
  const summary = {
231
263
  success: true,
@@ -235,52 +267,82 @@ function createImportHandler(opts) {
235
267
  errors: [],
236
268
  };
237
269
 
238
- for (const { rowNumber, data } of objects) {
239
- try {
240
- const payload = buildPayloadForRow(model, data, mode);
241
- if (Object.keys(payload).length === 0) {
242
- summary.failed++;
243
- summary.errors.push({ row: rowNumber, message: 'No mappable columns' });
244
- continue;
245
- }
270
+ const runRowProcessing = async (targetRepo) => {
271
+ for (const { rowNumber, data } of objects) {
272
+ try {
273
+ const payload = buildPayloadForRow(model, data, mode);
274
+ if (Object.keys(payload).length === 0) {
275
+ summary.failed++;
276
+ const errMsg = 'No mappable columns';
277
+ summary.errors.push({ row: rowNumber, message: errMsg });
278
+ if (isAtomic) {
279
+ throw new Error(`Row ${rowNumber}: ${errMsg}`);
280
+ }
281
+ continue;
282
+ }
246
283
 
247
- if (mode === 'insert') {
248
- await repo.create(payload);
249
- summary.created++;
250
- continue;
251
- }
284
+ if (mode === 'insert') {
285
+ await targetRepo.create(payload);
286
+ summary.created++;
287
+ continue;
288
+ }
252
289
 
253
- const keyMeta = model.columns.get(upsertKey);
254
- let keyVal;
255
- try {
256
- keyVal = coerceCell(data[upsertKey], keyMeta, upsertKey);
290
+ const keyMeta = model.columns?.get ? model.columns.get(upsertKey) : undefined;
291
+ let keyVal;
292
+ try {
293
+ keyVal = coerceCell(data[upsertKey], keyMeta, upsertKey);
294
+ } catch (e) {
295
+ summary.failed++;
296
+ summary.errors.push({ row: rowNumber, message: e.message || 'Bad upsert key' });
297
+ if (isAtomic) throw e;
298
+ continue;
299
+ }
300
+
301
+ if (keyVal === undefined || keyVal === null) {
302
+ const insertPayload = { ...payload };
303
+ delete insertPayload[upsertKey];
304
+ await targetRepo.create(insertPayload);
305
+ summary.created++;
306
+ continue;
307
+ }
308
+
309
+ const existing = await targetRepo.findOne({ [upsertKey]: keyVal });
310
+ if (existing) {
311
+ const id = existing[model.primaryKey];
312
+ await targetRepo.update(id, payload);
313
+ summary.updated++;
314
+ } else {
315
+ await targetRepo.create({ ...payload, [upsertKey]: keyVal });
316
+ summary.created++;
317
+ }
257
318
  } catch (e) {
258
319
  summary.failed++;
259
- summary.errors.push({ row: rowNumber, message: e.message || 'Bad upsert key' });
260
- continue;
261
- }
262
-
263
- if (keyVal === undefined || keyVal === null) {
264
- const insertPayload = { ...payload };
265
- delete insertPayload[upsertKey];
266
- await repo.create(insertPayload);
267
- summary.created++;
268
- continue;
320
+ summary.errors.push({ row: rowNumber, message: e.message || String(e) });
321
+ if (isAtomic) {
322
+ throw e;
323
+ }
269
324
  }
325
+ }
326
+ };
270
327
 
271
- const existing = await repo.findOne({ [upsertKey]: keyVal });
272
- if (existing) {
273
- const id = existing[model.primaryKey];
274
- await repo.update(id, payload);
275
- summary.updated++;
276
- } else {
277
- await repo.create({ ...payload, [upsertKey]: keyVal });
278
- summary.created++;
279
- }
280
- } catch (e) {
281
- summary.failed++;
282
- summary.errors.push({ row: rowNumber, message: e.message || String(e) });
328
+ if (isAtomic && typeof db.transaction === 'function') {
329
+ try {
330
+ await db.transaction(async (trxContext) => {
331
+ const trxRepo = trxContext.getRepository ? trxContext.getRepository(model.name) : repo;
332
+ await runRowProcessing(trxRepo);
333
+ });
334
+ } catch (txErr) {
335
+ return res.status(400).json({
336
+ success: false,
337
+ created: 0,
338
+ updated: 0,
339
+ failed: objects.length,
340
+ error: `Atomic import rolled back: ${txErr.message}`,
341
+ errors: summary.errors,
342
+ });
283
343
  }
344
+ } else {
345
+ await runRowProcessing(repo);
284
346
  }
285
347
 
286
348
  summary.success = summary.failed === 0;
@@ -49,6 +49,7 @@ function dataExchangePlugin(options = {}) {
49
49
 
50
50
  onRoutesReady(ctx) {
51
51
  if (!enabled) return;
52
+ if (ctx.app && ctx.app._dataExchangeRoutesRegistered) return;
52
53
 
53
54
  const db = dbOption ?? ctx.db;
54
55
  if (!db) {
@@ -87,6 +88,10 @@ function dataExchangePlugin(options = {}) {
87
88
  },
88
89
  });
89
90
  }
91
+
92
+ if (ctx.app) {
93
+ ctx.app._dataExchangeRoutesRegistered = true;
94
+ }
90
95
  },
91
96
  };
92
97
  }
@@ -15,18 +15,27 @@ function normalizeHeaderCell(h) {
15
15
  * @returns {(string|null)[]} index → model column or null if unknown
16
16
  */
17
17
  function buildHeaderMapping(headerRow, allowedKeys) {
18
- const lowerToKey = new Map();
18
+ const exactLowerToKey = new Map();
19
+ const strippedToKey = new Map();
19
20
  for (const k of allowedKeys) {
20
- lowerToKey.set(String(k).toLowerCase().replace(/\s+/g, '_'), k);
21
+ const kStr = String(k);
22
+ exactLowerToKey.set(kStr.toLowerCase().replace(/\s+/g, '_'), k);
23
+ strippedToKey.set(kStr.toLowerCase().replace(/[_\s-]+/g, ''), k);
21
24
  }
22
25
  const mapping = [];
23
26
  for (let i = 0; i < headerRow.length; i++) {
24
- const norm = normalizeHeaderCell(headerRow[i]).toLowerCase().replace(/\s+/g, '_');
25
- if (!norm) {
27
+ const raw = normalizeHeaderCell(headerRow[i]);
28
+ if (!raw) {
26
29
  mapping.push(null);
27
30
  continue;
28
31
  }
29
- mapping.push(lowerToKey.get(norm) ?? null);
32
+ const norm = raw.toLowerCase().replace(/\s+/g, '_');
33
+ let matched = exactLowerToKey.get(norm);
34
+ if (!matched) {
35
+ const stripped = raw.toLowerCase().replace(/[_\s-]+/g, '');
36
+ matched = strippedToKey.get(stripped);
37
+ }
38
+ mapping.push(matched ?? null);
30
39
  }
31
40
  return mapping;
32
41
  }
@@ -70,6 +70,8 @@ async function resolveExportRecords(db, modelName, req) {
70
70
  if (record) records.push(record);
71
71
  }
72
72
  }
73
+ } else if (Array.isArray(idList) && idList.length === 0) {
74
+ records = [];
73
75
  } else {
74
76
  records = await repo.findAll();
75
77
  }
package/plugins/index.js CHANGED
@@ -39,6 +39,8 @@ const {
39
39
  redis,
40
40
  createRedisAdapter,
41
41
  } = require('./realtime');
42
+ const { mcpPlugin, createMcpServer, McpServer } = require('./mcp');
43
+ const polarPlugin = require('./polar');
42
44
 
43
45
  module.exports = {
44
46
  sitemapPlugin,
@@ -77,6 +79,10 @@ module.exports = {
77
79
  redis,
78
80
  createRedisAdapter,
79
81
  queuePlugin: require('./queue'),
82
+ mcpPlugin,
83
+ createMcpServer,
84
+ McpServer,
85
+ polarPlugin,
80
86
  };
81
87
 
82
88
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Webspresso MCP Authentication & Security
3
+ * Provides token validation and localhost IP restrictions for HTTP/SSE transports
4
+ * @module plugins/mcp/auth
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+
9
+ /**
10
+ * Constant-time string comparison to avoid timing attacks
11
+ * @param {string} a
12
+ * @param {string} b
13
+ * @returns {boolean}
14
+ */
15
+ function safeEqual(a, b) {
16
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
17
+ const bufA = Buffer.from(a);
18
+ const bufB = Buffer.from(b);
19
+ if (bufA.length !== bufB.length) return false;
20
+ return crypto.timingSafeEqual(bufA, bufB);
21
+ }
22
+
23
+ /**
24
+ * Check if incoming request is from localhost
25
+ * @param {import('express').Request} req
26
+ * @returns {boolean}
27
+ */
28
+ function isLocalhost(req) {
29
+ const ip = req.ip || req.connection?.remoteAddress || '';
30
+ return (
31
+ ip === '127.0.0.1' ||
32
+ ip === '::1' ||
33
+ ip === '::ffff:127.0.0.1' ||
34
+ ip.endsWith('127.0.0.1') ||
35
+ req.hostname === 'localhost'
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Creates authentication middleware for MCP HTTP/SSE endpoints
41
+ * @param {Object} [options={}]
42
+ * @param {string} [options.token] - Pre-shared secret token
43
+ * @param {boolean} [options.localhostOnly=false] - Only allow localhost connections
44
+ * @param {function(import('express').Request): (boolean|Promise<boolean>)} [options.verify] - Custom verify function
45
+ * @returns {import('express').RequestHandler}
46
+ */
47
+ function createMcpAuthMiddleware(options = {}) {
48
+ const { token, localhostOnly = false, verify } = options;
49
+
50
+ return async function mcpAuthMiddleware(req, res, next) {
51
+ // 1. Localhost check
52
+ if (localhostOnly && !isLocalhost(req)) {
53
+ return res.status(403).json({
54
+ jsonrpc: '2.0',
55
+ error: { code: -32000, message: 'Forbidden: MCP server only allows localhost connections' },
56
+ });
57
+ }
58
+
59
+ // 2. Custom verification
60
+ if (typeof verify === 'function') {
61
+ try {
62
+ const ok = await verify(req);
63
+ if (!ok) {
64
+ return res.status(401).json({
65
+ jsonrpc: '2.0',
66
+ error: { code: -32000, message: 'Unauthorized: Custom MCP verification failed' },
67
+ });
68
+ }
69
+ return next();
70
+ } catch (err) {
71
+ return res.status(401).json({
72
+ jsonrpc: '2.0',
73
+ error: { code: -32000, message: `Unauthorized: ${err.message}` },
74
+ });
75
+ }
76
+ }
77
+
78
+ // 3. Pre-shared token check
79
+ if (token) {
80
+ let clientToken = '';
81
+ const authHeader = req.headers.authorization;
82
+ if (authHeader && authHeader.startsWith('Bearer ')) {
83
+ clientToken = authHeader.slice('Bearer '.length).trim();
84
+ } else if (req.query && req.query.token) {
85
+ clientToken = String(req.query.token).trim();
86
+ }
87
+
88
+ if (!clientToken || !safeEqual(clientToken, token)) {
89
+ return res.status(401).json({
90
+ jsonrpc: '2.0',
91
+ error: { code: -32000, message: 'Unauthorized: Invalid or missing MCP bearer token' },
92
+ });
93
+ }
94
+ }
95
+
96
+ next();
97
+ };
98
+ }
99
+
100
+ module.exports = {
101
+ createMcpAuthMiddleware,
102
+ isLocalhost,
103
+ safeEqual,
104
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Webspresso MCP File-Based Auto-Discovery
3
+ * Discovers custom MCP tools, resources, and prompts from project `mcp/` directory
4
+ * @module plugins/mcp/discovery
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Scan a directory for JS files and require them
12
+ * @param {string} dirPath
13
+ * @returns {Array<Object>}
14
+ */
15
+ function scanModuleDir(dirPath) {
16
+ if (!fs.existsSync(dirPath)) return [];
17
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
18
+ const items = [];
19
+
20
+ for (const entry of entries) {
21
+ if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
22
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
23
+ const fullPath = path.join(dirPath, entry.name);
24
+ try {
25
+ const mod = require(fullPath);
26
+ if (mod && typeof mod === 'object') {
27
+ // If mod doesn't have a name, default to filename without extension
28
+ if (!mod.name && !mod.uri) {
29
+ const baseName = path.basename(entry.name, path.extname(entry.name));
30
+ mod.name = baseName;
31
+ }
32
+ items.push(mod);
33
+ }
34
+ } catch (err) {
35
+ console.warn(`[mcp-discovery] Failed to load module at ${fullPath}:`, err.message);
36
+ }
37
+ }
38
+ }
39
+
40
+ return items;
41
+ }
42
+
43
+ /**
44
+ * Discovers custom MCP components from the filesystem and registers them to the server
45
+ * @param {import('./server').McpServer} server
46
+ * @param {string} [rootDir=process.cwd()]
47
+ * @param {string} [mcpDirName='mcp']
48
+ */
49
+ function discoverMcpDirectory(server, rootDir = process.cwd(), mcpDirName = 'mcp') {
50
+ const mcpRoot = path.resolve(rootDir, mcpDirName);
51
+ if (!fs.existsSync(mcpRoot)) return;
52
+
53
+ // 1. Discover custom tools: mcp/tools/
54
+ const toolsDir = path.join(mcpRoot, 'tools');
55
+ const customTools = scanModuleDir(toolsDir);
56
+ for (const tool of customTools) {
57
+ if (typeof tool.handler === 'function') {
58
+ server.registerTool(tool);
59
+ }
60
+ }
61
+
62
+ // 2. Discover custom resources: mcp/resources/
63
+ const resourcesDir = path.join(mcpRoot, 'resources');
64
+ const customResources = scanModuleDir(resourcesDir);
65
+ for (const res of customResources) {
66
+ if (typeof res.handler === 'function') {
67
+ if (res.uriTemplate) {
68
+ server.registerResourceTemplate(res);
69
+ } else if (res.uri) {
70
+ server.registerResource(res);
71
+ }
72
+ }
73
+ }
74
+
75
+ // 3. Discover custom prompts: mcp/prompts/
76
+ const promptsDir = path.join(mcpRoot, 'prompts');
77
+ const customPrompts = scanModuleDir(promptsDir);
78
+ for (const prompt of customPrompts) {
79
+ if (typeof prompt.handler === 'function') {
80
+ server.registerPrompt(prompt);
81
+ }
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ discoverMcpDirectory,
87
+ scanModuleDir,
88
+ };