webspresso 0.0.91 → 0.0.93
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/README.md +103 -2864
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +8 -2
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +2 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +17 -5
- package/core/compression/compression-stream.js +1 -1
- package/core/compression/middleware.js +9 -0
- package/core/compression/negotiate.js +23 -11
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/index.js +38 -2
- package/core/orm/model.js +11 -2
- package/core/queue/adapters/database.js +46 -13
- package/core/queue/adapters/memory.js +21 -0
- package/core/queue/adapters/redis.js +21 -0
- package/core/queue/manager.js +46 -20
- package/core/validation/index.js +70 -0
- package/index.d.ts +99 -5
- package/index.js +7 -1
- package/package.json +9 -1
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +6 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +20 -2
- package/plugins/admin-panel/index.js +65 -6
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/data-exchange/export-xlsx.js +28 -6
- package/plugins/data-exchange/import.js +45 -20
- package/plugins/data-exchange/index.js +5 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/index.js +4 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/src/file-router.js +184 -123
- package/src/njk-frontmatter.js +24 -14
- package/src/server.js +30 -12
- package/src/services/discovery.js +2 -1
- package/src/services/registry.js +2 -0
- package/src/services/validator.js +2 -53
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso MCP ORM Provider
|
|
3
|
+
* Bridges Webspresso ORM models to MCP Tools and Resources
|
|
4
|
+
* @module plugins/mcp/providers/orm
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getAllModels } = require('../../../core/orm/model');
|
|
8
|
+
const { convertZodToInputSchema } = require('./services');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Describe a model's schema, columns, and relations
|
|
12
|
+
* @param {import('../../../core/orm/types').ModelDefinition} model
|
|
13
|
+
* @returns {Object}
|
|
14
|
+
*/
|
|
15
|
+
function describeModel(model) {
|
|
16
|
+
if (!model) return null;
|
|
17
|
+
|
|
18
|
+
const columns = {};
|
|
19
|
+
if (model.columns instanceof Map) {
|
|
20
|
+
for (const [colName, colMeta] of model.columns.entries()) {
|
|
21
|
+
columns[colName] = {
|
|
22
|
+
type: colMeta.type || 'unknown',
|
|
23
|
+
nullable: Boolean(colMeta.nullable),
|
|
24
|
+
primaryKey: Boolean(colMeta.primaryKey),
|
|
25
|
+
unique: Boolean(colMeta.unique),
|
|
26
|
+
default: colMeta.default,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Also extract fields from schema shape if available
|
|
32
|
+
if (model.schema && model.schema.shape) {
|
|
33
|
+
for (const [key, val] of Object.entries(model.schema.shape)) {
|
|
34
|
+
if (!columns[key]) {
|
|
35
|
+
columns[key] = {
|
|
36
|
+
type: val._def?.typeName?.replace(/^Zod/, '').toLowerCase() || 'unknown',
|
|
37
|
+
nullable: val.isNullable ? val.isNullable() : false,
|
|
38
|
+
optional: val.isOptional ? val.isOptional() : false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const relations = {};
|
|
45
|
+
if (model.relations) {
|
|
46
|
+
for (const [relName, relMeta] of Object.entries(model.relations)) {
|
|
47
|
+
let targetName = relMeta.targetModel;
|
|
48
|
+
if (!targetName && typeof relMeta.model === 'function') {
|
|
49
|
+
try {
|
|
50
|
+
const res = relMeta.model();
|
|
51
|
+
targetName = typeof res === 'string' ? res : res?.name;
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
relations[relName] = {
|
|
55
|
+
type: relMeta.type,
|
|
56
|
+
targetModel: targetName,
|
|
57
|
+
foreignKey: relMeta.foreignKey,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: model.name,
|
|
64
|
+
table: model.table,
|
|
65
|
+
primaryKey: model.primaryKey || 'id',
|
|
66
|
+
columns,
|
|
67
|
+
relations,
|
|
68
|
+
hidden: model.hidden || [],
|
|
69
|
+
softDelete: Boolean(model.softDelete),
|
|
70
|
+
admin: model.admin || null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates ORM-related MCP tools
|
|
76
|
+
* @param {Object} options
|
|
77
|
+
* @param {Object} options.db - Webspresso database instance
|
|
78
|
+
* @param {boolean} [options.readOnly=false]
|
|
79
|
+
* @param {Array<string>} [options.models] - Whitelist of model names
|
|
80
|
+
* @returns {Array<Object>}
|
|
81
|
+
*/
|
|
82
|
+
function createOrmTools(options = {}) {
|
|
83
|
+
const { db, readOnly = false, models: modelWhitelist } = options;
|
|
84
|
+
if (!db) return [];
|
|
85
|
+
|
|
86
|
+
const tools = [
|
|
87
|
+
{
|
|
88
|
+
name: 'orm_describe_model',
|
|
89
|
+
description: 'Get detailed metadata, columns, types, relations, and settings for a given ORM model.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
model: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Name of the model (e.g. "User", "Product")',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ['model'],
|
|
99
|
+
},
|
|
100
|
+
handler: async ({ model }) => {
|
|
101
|
+
const modelDef = db.getModel ? db.getModel(model) : null;
|
|
102
|
+
if (!modelDef) {
|
|
103
|
+
throw new Error(`Model "${model}" not found in registered database models.`);
|
|
104
|
+
}
|
|
105
|
+
return describeModel(modelDef);
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'orm_find',
|
|
110
|
+
description: 'Query records from an ORM model using where filters, pagination, and sorting.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
model: { type: 'string', description: 'Model name' },
|
|
115
|
+
where: { type: 'object', description: 'Filter key-value pairs (e.g. {"status": "active"})' },
|
|
116
|
+
limit: { type: 'number', description: 'Maximum number of records (default: 20, max: 100)' },
|
|
117
|
+
page: { type: 'number', description: 'Page number (default: 1)' },
|
|
118
|
+
sort: { type: 'string', description: 'Column to sort by' },
|
|
119
|
+
order: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order' },
|
|
120
|
+
},
|
|
121
|
+
required: ['model'],
|
|
122
|
+
},
|
|
123
|
+
handler: async ({ model, where = {}, limit = 20, page = 1, sort, order = 'asc' }) => {
|
|
124
|
+
const repo = db.getRepository(model);
|
|
125
|
+
if (!repo) {
|
|
126
|
+
throw new Error(`Repository for model "${model}" not found.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const safeLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
|
|
130
|
+
const safePage = Math.max(1, Number(page) || 1);
|
|
131
|
+
|
|
132
|
+
let query = repo.query();
|
|
133
|
+
if (where && typeof where === 'object') {
|
|
134
|
+
for (const [k, v] of Object.entries(where)) {
|
|
135
|
+
if (v !== undefined) {
|
|
136
|
+
query = query.where(k, v);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (sort) {
|
|
142
|
+
query = query.orderBy(sort, order);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await query.paginate({ page: safePage, limit: safeLimit });
|
|
146
|
+
return result;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'orm_findById',
|
|
151
|
+
description: 'Fetch a single record by primary key ID from an ORM model.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
model: { type: 'string', description: 'Model name' },
|
|
156
|
+
id: { description: 'Record primary key ID' },
|
|
157
|
+
},
|
|
158
|
+
required: ['model', 'id'],
|
|
159
|
+
},
|
|
160
|
+
handler: async ({ model, id }) => {
|
|
161
|
+
const repo = db.getRepository(model);
|
|
162
|
+
if (!repo) {
|
|
163
|
+
throw new Error(`Repository for model "${model}" not found.`);
|
|
164
|
+
}
|
|
165
|
+
const record = await repo.findById(id);
|
|
166
|
+
if (!record) {
|
|
167
|
+
return { found: false, record: null };
|
|
168
|
+
}
|
|
169
|
+
return { found: true, record };
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// If not readOnly, add mutation tools
|
|
175
|
+
if (!readOnly) {
|
|
176
|
+
tools.push(
|
|
177
|
+
{
|
|
178
|
+
name: 'orm_create',
|
|
179
|
+
description: 'Create a new record for a given ORM model with schema validation and lifecycle hooks.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
model: { type: 'string', description: 'Model name' },
|
|
184
|
+
data: { type: 'object', description: 'Record fields to insert' },
|
|
185
|
+
},
|
|
186
|
+
required: ['model', 'data'],
|
|
187
|
+
},
|
|
188
|
+
handler: async ({ model, data }) => {
|
|
189
|
+
const repo = db.getRepository(model);
|
|
190
|
+
if (!repo) {
|
|
191
|
+
throw new Error(`Repository for model "${model}" not found.`);
|
|
192
|
+
}
|
|
193
|
+
const created = await repo.create(data);
|
|
194
|
+
return { success: true, record: created };
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'orm_update',
|
|
199
|
+
description: 'Update an existing record by ID in an ORM model.',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
model: { type: 'string', description: 'Model name' },
|
|
204
|
+
id: { description: 'Record ID to update' },
|
|
205
|
+
data: { type: 'object', description: 'Fields to update' },
|
|
206
|
+
},
|
|
207
|
+
required: ['model', 'id', 'data'],
|
|
208
|
+
},
|
|
209
|
+
handler: async ({ model, id, data }) => {
|
|
210
|
+
const repo = db.getRepository(model);
|
|
211
|
+
if (!repo) {
|
|
212
|
+
throw new Error(`Repository for model "${model}" not found.`);
|
|
213
|
+
}
|
|
214
|
+
const updated = await repo.update(id, data);
|
|
215
|
+
return { success: Boolean(updated), record: updated };
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'orm_delete',
|
|
220
|
+
description: 'Delete a record by ID from an ORM model (respects soft deletes if configured).',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
model: { type: 'string', description: 'Model name' },
|
|
225
|
+
id: { description: 'Record ID to delete' },
|
|
226
|
+
},
|
|
227
|
+
required: ['model', 'id'],
|
|
228
|
+
},
|
|
229
|
+
handler: async ({ model, id }) => {
|
|
230
|
+
const repo = db.getRepository(model);
|
|
231
|
+
if (!repo) {
|
|
232
|
+
throw new Error(`Repository for model "${model}" not found.`);
|
|
233
|
+
}
|
|
234
|
+
const ok = await repo.delete(id);
|
|
235
|
+
return { success: Boolean(ok) };
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Whitelist filtering if specified
|
|
242
|
+
if (modelWhitelist && modelWhitelist.length > 0) {
|
|
243
|
+
const allowed = new Set(modelWhitelist);
|
|
244
|
+
return tools.map((t) => ({
|
|
245
|
+
...t,
|
|
246
|
+
handler: async (args, ctx) => {
|
|
247
|
+
if (args.model && !allowed.has(args.model)) {
|
|
248
|
+
throw new Error(`Access to model "${args.model}" is not permitted.`);
|
|
249
|
+
}
|
|
250
|
+
return t.handler(args, ctx);
|
|
251
|
+
},
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return tools;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Creates ORM-related MCP resources and templates
|
|
260
|
+
* @param {Object} options
|
|
261
|
+
* @param {Object} options.db
|
|
262
|
+
* @returns {{ resources: Array<Object>, templates: Array<Object> }}
|
|
263
|
+
*/
|
|
264
|
+
function createOrmResources(options = {}) {
|
|
265
|
+
const { db } = options;
|
|
266
|
+
if (!db) return { resources: [], templates: [] };
|
|
267
|
+
|
|
268
|
+
const resources = [
|
|
269
|
+
{
|
|
270
|
+
uri: 'webspresso://models',
|
|
271
|
+
name: 'Registered ORM Models',
|
|
272
|
+
description: 'List of all database models registered in Webspresso.',
|
|
273
|
+
mimeType: 'application/json',
|
|
274
|
+
handler: async () => {
|
|
275
|
+
const models = getAllModels ? getAllModels() : [];
|
|
276
|
+
const modelList = models instanceof Map ? Array.from(models.values()) : (Array.isArray(models) ? models : Object.values(models));
|
|
277
|
+
return modelList.map((m) => ({
|
|
278
|
+
name: m.name,
|
|
279
|
+
table: m.table,
|
|
280
|
+
softDelete: Boolean(m.softDelete),
|
|
281
|
+
primaryKey: m.primaryKey || 'id',
|
|
282
|
+
}));
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
uri: 'webspresso://schema',
|
|
287
|
+
name: 'Database Schema Overview',
|
|
288
|
+
description: 'Complete database models schema structure with columns, types, and relations.',
|
|
289
|
+
mimeType: 'application/json',
|
|
290
|
+
handler: async () => {
|
|
291
|
+
const models = getAllModels ? getAllModels() : [];
|
|
292
|
+
const modelList = models instanceof Map ? Array.from(models.values()) : (Array.isArray(models) ? models : Object.values(models));
|
|
293
|
+
const result = {};
|
|
294
|
+
for (const model of modelList) {
|
|
295
|
+
result[model.name] = describeModel(model);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const templates = [
|
|
303
|
+
{
|
|
304
|
+
uriTemplate: 'webspresso://models/{model}',
|
|
305
|
+
name: 'ORM Model Metadata',
|
|
306
|
+
description: 'Get full schema and relationship definition for a specific model.',
|
|
307
|
+
mimeType: 'application/json',
|
|
308
|
+
handler: async (uri, params) => {
|
|
309
|
+
const modelDef = db.getModel ? db.getModel(params.model) : null;
|
|
310
|
+
if (!modelDef) {
|
|
311
|
+
throw new Error(`Model "${params.model}" not found.`);
|
|
312
|
+
}
|
|
313
|
+
return describeModel(modelDef);
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
return { resources, templates };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
createOrmTools,
|
|
323
|
+
createOrmResources,
|
|
324
|
+
describeModel,
|
|
325
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso MCP Services Provider
|
|
3
|
+
* Bridges Webspresso ServiceRegistry to MCP Tools with automatic Zod -> JSON Schema conversion.
|
|
4
|
+
* @module plugins/mcp/providers/services
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes a dot-separated service name to an MCP-safe tool name
|
|
11
|
+
* e.g. "user.create" -> "service__user__create"
|
|
12
|
+
* @param {string} serviceName
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function serviceNameToToolName(serviceName) {
|
|
16
|
+
return `service__${serviceName.replace(/\./g, '__').replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reconstructs original service name from tool name
|
|
21
|
+
* @param {string} toolName
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
function toolNameToServiceName(toolName) {
|
|
25
|
+
if (!toolName.startsWith('service__')) return null;
|
|
26
|
+
return toolName.slice('service__'.length).replace(/__/g, '.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a service name matches any pattern in a list (e.g. ['user.*', 'order.get'])
|
|
31
|
+
* @param {string} name
|
|
32
|
+
* @param {Array<string>} patterns
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
function matchesPattern(name, patterns) {
|
|
36
|
+
if (!patterns || patterns.length === 0) return true;
|
|
37
|
+
return patterns.some((pattern) => {
|
|
38
|
+
if (pattern === '*' || pattern === name) return true;
|
|
39
|
+
if (pattern.endsWith('*')) {
|
|
40
|
+
const prefix = pattern.slice(0, -1);
|
|
41
|
+
return name.startsWith(prefix);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a service is likely a mutation (create, update, delete, remove, post, send, prune, etc.)
|
|
49
|
+
* @param {string} serviceName
|
|
50
|
+
* @param {Object} [serviceDef]
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
function isMutationService(serviceName, serviceDef) {
|
|
54
|
+
if (serviceDef?.mutation !== undefined) {
|
|
55
|
+
return Boolean(serviceDef.mutation);
|
|
56
|
+
}
|
|
57
|
+
const parts = serviceName.toLowerCase().split('.');
|
|
58
|
+
const lastPart = parts[parts.length - 1];
|
|
59
|
+
const mutationKeywords = [
|
|
60
|
+
'create',
|
|
61
|
+
'add',
|
|
62
|
+
'insert',
|
|
63
|
+
'update',
|
|
64
|
+
'patch',
|
|
65
|
+
'edit',
|
|
66
|
+
'delete',
|
|
67
|
+
'remove',
|
|
68
|
+
'destroy',
|
|
69
|
+
'prune',
|
|
70
|
+
'reset',
|
|
71
|
+
'send',
|
|
72
|
+
'dispatch',
|
|
73
|
+
'cancel',
|
|
74
|
+
'pay',
|
|
75
|
+
'checkout',
|
|
76
|
+
'execute',
|
|
77
|
+
'import',
|
|
78
|
+
'clear',
|
|
79
|
+
'flush',
|
|
80
|
+
'purge',
|
|
81
|
+
'drop',
|
|
82
|
+
'truncate',
|
|
83
|
+
];
|
|
84
|
+
return mutationKeywords.some((kw) => lastPart.includes(kw));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Converts a Webspresso Zod schema to a clean MCP inputSchema
|
|
89
|
+
* @param {import('zod').ZodTypeAny} schema
|
|
90
|
+
* @returns {Object}
|
|
91
|
+
*/
|
|
92
|
+
function convertZodToInputSchema(schema) {
|
|
93
|
+
if (!schema) {
|
|
94
|
+
return {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {},
|
|
97
|
+
additionalProperties: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const rawSchema = zodToJsonSchema(schema, {
|
|
103
|
+
$refStrategy: 'none',
|
|
104
|
+
removeAdditionalStrategy: 'strict',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Clean up definition wrapper if present
|
|
108
|
+
if (rawSchema && typeof rawSchema === 'object') {
|
|
109
|
+
const { $schema, ...clean } = rawSchema;
|
|
110
|
+
if (!clean.type) {
|
|
111
|
+
clean.type = 'object';
|
|
112
|
+
}
|
|
113
|
+
return clean;
|
|
114
|
+
}
|
|
115
|
+
return { type: 'object', properties: {} };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {},
|
|
120
|
+
description: `Input schema auto-conversion error: ${err.message}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generates MCP tool definitions from a Webspresso ServiceRegistry
|
|
127
|
+
* @param {Object} options
|
|
128
|
+
* @param {import('../../src/services').ServiceRegistry} options.registry
|
|
129
|
+
* @param {Array<string>} [options.include] - Whitelist of service patterns
|
|
130
|
+
* @param {Array<string>} [options.exclude] - Blacklist of service patterns
|
|
131
|
+
* @param {boolean} [options.readOnly=false] - When true, filters out mutation services
|
|
132
|
+
* @param {string|Array<string>} [options.role] - Simulated user role for auth
|
|
133
|
+
* @returns {Array<Object>}
|
|
134
|
+
*/
|
|
135
|
+
function createServiceTools(options = {}) {
|
|
136
|
+
const { registry, include, exclude, readOnly = false, role = 'admin' } = options;
|
|
137
|
+
if (!registry) return [];
|
|
138
|
+
|
|
139
|
+
const tools = [];
|
|
140
|
+
const serviceNames = registry.list();
|
|
141
|
+
|
|
142
|
+
for (const name of serviceNames) {
|
|
143
|
+
// Check include / exclude patterns
|
|
144
|
+
if (include && !matchesPattern(name, include)) continue;
|
|
145
|
+
if (exclude && matchesPattern(name, exclude)) continue;
|
|
146
|
+
|
|
147
|
+
const def = registry.get(name);
|
|
148
|
+
if (!def) continue;
|
|
149
|
+
|
|
150
|
+
const isMutation = isMutationService(name, def);
|
|
151
|
+
if (readOnly && isMutation) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const toolName = serviceNameToToolName(name);
|
|
156
|
+
const inputSchema = convertZodToInputSchema(def.schema);
|
|
157
|
+
|
|
158
|
+
const description =
|
|
159
|
+
def.description ||
|
|
160
|
+
def.metadata?.description ||
|
|
161
|
+
`Executes Webspresso service '${name}'.${isMutation ? ' (Mutation)' : ' (Query)'}`;
|
|
162
|
+
|
|
163
|
+
tools.push({
|
|
164
|
+
name: toolName,
|
|
165
|
+
description,
|
|
166
|
+
inputSchema,
|
|
167
|
+
handler: async (args, ctx) => {
|
|
168
|
+
// Execute service via registry
|
|
169
|
+
const execCtx = {
|
|
170
|
+
...ctx,
|
|
171
|
+
user: ctx.user || (role ? { id: 'mcp-agent', role } : null),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = await registry.execute(name, args, execCtx);
|
|
175
|
+
return result;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return tools;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
createServiceTools,
|
|
185
|
+
serviceNameToToolName,
|
|
186
|
+
toolNameToServiceName,
|
|
187
|
+
convertZodToInputSchema,
|
|
188
|
+
isMutationService,
|
|
189
|
+
matchesPattern,
|
|
190
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso MCP System Provider
|
|
3
|
+
* Exposes application routes, services index, and health info as MCP Resources
|
|
4
|
+
* @module plugins/mcp/providers/system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates system MCP resources
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {Object} [options.appContext]
|
|
11
|
+
* @param {Object} [options.serviceRegistry]
|
|
12
|
+
* @param {Object} [options.routes]
|
|
13
|
+
* @param {Object} [options.db]
|
|
14
|
+
* @returns {Array<Object>}
|
|
15
|
+
*/
|
|
16
|
+
function createSystemResources(options = {}) {
|
|
17
|
+
const { appContext, serviceRegistry, routes, db } = options;
|
|
18
|
+
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
uri: 'webspresso://routes',
|
|
22
|
+
name: 'Application Routes',
|
|
23
|
+
description: 'List of all file-based SSR and API routes mounted in the Webspresso application.',
|
|
24
|
+
mimeType: 'application/json',
|
|
25
|
+
handler: async () => {
|
|
26
|
+
const rawRoutes = routes || appContext?.routes || [];
|
|
27
|
+
if (Array.isArray(rawRoutes)) {
|
|
28
|
+
return rawRoutes.map((r) => ({
|
|
29
|
+
path: r.path || r.route,
|
|
30
|
+
method: r.method ? r.method.toUpperCase() : 'GET',
|
|
31
|
+
isApi: Boolean(r.isApi || r.path?.startsWith('/api/')),
|
|
32
|
+
middleware: r.middleware || [],
|
|
33
|
+
sourceFile: r.filePath || r.file,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
return rawRoutes;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
uri: 'webspresso://services',
|
|
41
|
+
name: 'Services Directory',
|
|
42
|
+
description: 'List of all business logic services discovered and registered in the application.',
|
|
43
|
+
mimeType: 'application/json',
|
|
44
|
+
handler: async () => {
|
|
45
|
+
if (!serviceRegistry) return [];
|
|
46
|
+
const names = serviceRegistry.list();
|
|
47
|
+
return names.map((name) => {
|
|
48
|
+
const def = serviceRegistry.get(name);
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description: def?.description || '',
|
|
52
|
+
hasSchema: Boolean(def?.schema),
|
|
53
|
+
hasAuth: Boolean(def?.auth),
|
|
54
|
+
timeout: def?.timeout || null,
|
|
55
|
+
transaction: Boolean(def?.transaction),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
uri: 'webspresso://health',
|
|
62
|
+
name: 'System Health & Metrics',
|
|
63
|
+
description: 'Framework runtime information, node version, memory usage, and uptime.',
|
|
64
|
+
mimeType: 'application/json',
|
|
65
|
+
handler: async () => {
|
|
66
|
+
const memory = process.memoryUsage();
|
|
67
|
+
return {
|
|
68
|
+
status: 'ok',
|
|
69
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
70
|
+
nodeVersion: process.version,
|
|
71
|
+
platform: process.platform,
|
|
72
|
+
memory: {
|
|
73
|
+
heapUsedMB: Math.round(memory.heapUsed / 1024 / 1024),
|
|
74
|
+
heapTotalMB: Math.round(memory.heapTotal / 1024 / 1024),
|
|
75
|
+
rssMB: Math.round(memory.rss / 1024 / 1024),
|
|
76
|
+
},
|
|
77
|
+
databaseConnected: Boolean(db),
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
createSystemResources,
|
|
87
|
+
};
|