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.
- 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/polar-migrate.js +50 -0
- 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 +4 -0
- package/core/auth/tokens.js +16 -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/cache/layer.js +1 -1
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/index.js +42 -3
- package/core/orm/model.js +11 -2
- package/core/orm/transaction.js +3 -1
- 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/ssr/stream.js +58 -14
- 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/auth.js +10 -0
- 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/content/index.js +5 -3
- package/plugins/data-exchange/export-xlsx.js +28 -6
- package/plugins/data-exchange/import.js +120 -58
- 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 +6 -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 +156 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/polar/.env.example +12 -0
- package/plugins/polar/CHANGELOG.md +38 -0
- package/plugins/polar/README.md +158 -0
- package/plugins/polar/index.js +144 -0
- package/plugins/polar/src/api.js +54 -0
- package/plugins/polar/src/checkout.js +111 -0
- package/plugins/polar/src/config.js +88 -0
- package/plugins/polar/src/env.schema.js +36 -0
- package/plugins/polar/src/middleware.js +37 -0
- package/plugins/polar/src/migration.js +58 -0
- package/plugins/polar/src/nunjucks-filters.js +56 -0
- package/plugins/polar/src/portal.js +51 -0
- package/plugins/polar/src/rate-limit.js +70 -0
- package/plugins/polar/src/routes.js +212 -0
- package/plugins/polar/src/sync.js +138 -0
- package/plugins/polar/src/urls.js +117 -0
- package/plugins/polar/src/user-resolver.js +195 -0
- package/plugins/polar/src/webhooks.js +248 -0
- package/src/file-router.js +203 -125
- package/src/helpers.js +60 -13
- 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 +7 -4
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Model Context Protocol (MCP) Plugin
|
|
3
|
+
* Connects Webspresso applications to AI agents, IDEs, and LLM tools.
|
|
4
|
+
* @module plugins/mcp
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { McpServer } = require('./server');
|
|
8
|
+
const { createServiceTools } = require('./providers/services');
|
|
9
|
+
const { createOrmTools, createOrmResources } = require('./providers/orm');
|
|
10
|
+
const { createSystemResources } = require('./providers/system');
|
|
11
|
+
const { createBuiltinPrompts } = require('./prompts');
|
|
12
|
+
const { createMcpAuthMiddleware } = require('./auth');
|
|
13
|
+
const { mountSseTransport } = require('./transports/sse');
|
|
14
|
+
const { startStdioTransport, redirectConsoleToStderr, restoreConsole } = require('./transports/stdio');
|
|
15
|
+
const { discoverMcpDirectory } = require('./discovery');
|
|
16
|
+
|
|
17
|
+
const { getServiceRegistry, hasServiceRegistry } = require('../../src/app-context');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* MCP Plugin Factory
|
|
21
|
+
* @param {Object} [options={}]
|
|
22
|
+
* @param {string} [options.path='/_mcp'] - Route path for HTTP & SSE endpoints
|
|
23
|
+
* @param {boolean} [options.enabled=true] - Whether the plugin is active
|
|
24
|
+
* @param {Object} [options.auth] - Authentication options { token, localhostOnly, verify }
|
|
25
|
+
* @param {Object|boolean} [options.services] - Services auto-bridge config
|
|
26
|
+
* @param {Object|boolean} [options.orm] - ORM auto-bridge config
|
|
27
|
+
* @param {Object|boolean} [options.system] - System resources config
|
|
28
|
+
* @param {Array<Object>} [options.tools] - Additional custom tools
|
|
29
|
+
* @param {Array<Object>} [options.resources] - Additional custom resources
|
|
30
|
+
* @param {Array<Object>} [options.prompts] - Additional custom prompts
|
|
31
|
+
* @param {boolean} [options.discovery=true] - Auto-discover mcp/ folder
|
|
32
|
+
* @param {string} [options.name='webspresso-mcp'] - Server name
|
|
33
|
+
* @param {string} [options.version='1.0.0'] - Server version
|
|
34
|
+
* @param {string} [options.instructions] - Instructions for the LLM
|
|
35
|
+
* @returns {Object} Webspresso plugin definition
|
|
36
|
+
*/
|
|
37
|
+
function mcpPlugin(options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
path = '/_mcp',
|
|
40
|
+
enabled = true,
|
|
41
|
+
auth = {},
|
|
42
|
+
services: servicesOption = true,
|
|
43
|
+
orm: ormOption = true,
|
|
44
|
+
system: systemOption = true,
|
|
45
|
+
tools: extraTools = [],
|
|
46
|
+
resources: extraResources = [],
|
|
47
|
+
prompts: extraPrompts = [],
|
|
48
|
+
discovery = true,
|
|
49
|
+
name = 'webspresso-mcp',
|
|
50
|
+
version = '1.0.0',
|
|
51
|
+
instructions,
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
let serverInstance = null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: 'mcp',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
dependencies: [],
|
|
60
|
+
|
|
61
|
+
api: {
|
|
62
|
+
getServer: () => serverInstance,
|
|
63
|
+
registerTool: (tool) => serverInstance && serverInstance.registerTool(tool),
|
|
64
|
+
registerResource: (res) => serverInstance && serverInstance.registerResource(res),
|
|
65
|
+
registerPrompt: (prompt) => serverInstance && serverInstance.registerPrompt(prompt),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
register(ctx) {
|
|
69
|
+
if (!enabled) return;
|
|
70
|
+
|
|
71
|
+
serverInstance = new McpServer({
|
|
72
|
+
name,
|
|
73
|
+
version,
|
|
74
|
+
instructions,
|
|
75
|
+
context: {
|
|
76
|
+
app: ctx.app,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Register initial custom tools, resources, prompts
|
|
81
|
+
if (Array.isArray(extraTools)) {
|
|
82
|
+
serverInstance.registerTools(extraTools);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(extraResources)) {
|
|
85
|
+
for (const res of extraResources) {
|
|
86
|
+
if (res.uriTemplate) {
|
|
87
|
+
serverInstance.registerResourceTemplate(res);
|
|
88
|
+
} else {
|
|
89
|
+
serverInstance.registerResource(res);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(extraPrompts)) {
|
|
94
|
+
for (const prompt of extraPrompts) {
|
|
95
|
+
serverInstance.registerPrompt(prompt);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Register built-in prompts
|
|
100
|
+
const builtinPrompts = createBuiltinPrompts();
|
|
101
|
+
for (const p of builtinPrompts) {
|
|
102
|
+
serverInstance.registerPrompt(p);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Expose to ctx
|
|
106
|
+
ctx.mcp = {
|
|
107
|
+
server: serverInstance,
|
|
108
|
+
registerTool: (t) => serverInstance.registerTool(t),
|
|
109
|
+
registerResource: (r) => serverInstance.registerResource(r),
|
|
110
|
+
registerPrompt: (p) => serverInstance.registerPrompt(p),
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onRoutesReady(ctx) {
|
|
115
|
+
if (!enabled || !serverInstance) return;
|
|
116
|
+
|
|
117
|
+
const { app, db, routes } = ctx;
|
|
118
|
+
serverInstance.context.db = db;
|
|
119
|
+
|
|
120
|
+
// 1. Resolve ServiceRegistry
|
|
121
|
+
let registry = null;
|
|
122
|
+
if (ctx.options?.serviceRegistry) {
|
|
123
|
+
registry = ctx.options.serviceRegistry;
|
|
124
|
+
} else if (hasServiceRegistry()) {
|
|
125
|
+
registry = getServiceRegistry();
|
|
126
|
+
}
|
|
127
|
+
serverInstance.context.serviceRegistry = registry;
|
|
128
|
+
|
|
129
|
+
// 2. Auto-bridge Services to MCP Tools
|
|
130
|
+
if (servicesOption && registry) {
|
|
131
|
+
const sOpts = typeof servicesOption === 'object' ? servicesOption : {};
|
|
132
|
+
const serviceTools = createServiceTools({
|
|
133
|
+
registry,
|
|
134
|
+
include: sOpts.include,
|
|
135
|
+
exclude: sOpts.exclude,
|
|
136
|
+
readOnly: Boolean(sOpts.readOnly),
|
|
137
|
+
role: sOpts.role || 'admin',
|
|
138
|
+
});
|
|
139
|
+
serverInstance.registerTools(serviceTools);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Auto-bridge ORM Models to MCP Tools and Resources
|
|
143
|
+
if (ormOption && db) {
|
|
144
|
+
const oOpts = typeof ormOption === 'object' ? ormOption : {};
|
|
145
|
+
const ormTools = createOrmTools({
|
|
146
|
+
db,
|
|
147
|
+
readOnly: Boolean(oOpts.readOnly),
|
|
148
|
+
models: oOpts.models,
|
|
149
|
+
});
|
|
150
|
+
serverInstance.registerTools(ormTools);
|
|
151
|
+
|
|
152
|
+
const { resources, templates } = createOrmResources({ db });
|
|
153
|
+
for (const r of resources) {
|
|
154
|
+
serverInstance.registerResource(r);
|
|
155
|
+
}
|
|
156
|
+
for (const t of templates) {
|
|
157
|
+
serverInstance.registerResourceTemplate(t);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 4. Auto-bridge System Resources
|
|
162
|
+
if (systemOption) {
|
|
163
|
+
const sysResources = createSystemResources({
|
|
164
|
+
appContext: ctx,
|
|
165
|
+
serviceRegistry: registry,
|
|
166
|
+
routes,
|
|
167
|
+
db,
|
|
168
|
+
});
|
|
169
|
+
for (const r of sysResources) {
|
|
170
|
+
serverInstance.registerResource(r);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 5. Auto-discover custom items from mcp/ directory
|
|
175
|
+
if (discovery) {
|
|
176
|
+
discoverMcpDirectory(serverInstance, process.cwd());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 6. Mount HTTP & SSE Transport on Express
|
|
180
|
+
const authMiddleware =
|
|
181
|
+
auth.token || auth.localhostOnly || auth.verify ? createMcpAuthMiddleware(auth) : null;
|
|
182
|
+
|
|
183
|
+
mountSseTransport({
|
|
184
|
+
app,
|
|
185
|
+
server: serverInstance,
|
|
186
|
+
path,
|
|
187
|
+
authMiddleware,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
mcpPlugin,
|
|
195
|
+
McpServer,
|
|
196
|
+
createMcpServer: (opts) => new McpServer(opts),
|
|
197
|
+
startStdioTransport,
|
|
198
|
+
mountSseTransport,
|
|
199
|
+
createMcpAuthMiddleware,
|
|
200
|
+
createServiceTools,
|
|
201
|
+
createOrmTools,
|
|
202
|
+
createOrmResources,
|
|
203
|
+
createSystemResources,
|
|
204
|
+
createBuiltinPrompts,
|
|
205
|
+
discoverMcpDirectory,
|
|
206
|
+
redirectConsoleToStderr,
|
|
207
|
+
restoreConsole,
|
|
208
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso MCP Built-in Prompt Templates
|
|
3
|
+
* Provides standard LLM prompt templates following Webspresso architectural guidelines.
|
|
4
|
+
* @module plugins/mcp/prompts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates built-in prompt definitions
|
|
9
|
+
* @returns {Array<Object>}
|
|
10
|
+
*/
|
|
11
|
+
function createBuiltinPrompts() {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
name: 'scaffold_service',
|
|
15
|
+
description: 'Generates a Webspresso service file adhering to strict architecture guidelines.',
|
|
16
|
+
arguments: [
|
|
17
|
+
{
|
|
18
|
+
name: 'serviceName',
|
|
19
|
+
description: 'Dot-notation service name (e.g. "order.cancel" or "user.invite")',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'description',
|
|
24
|
+
description: 'Brief description of what the service does',
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
handler: async ({ serviceName, description = '' }) => {
|
|
29
|
+
const parts = serviceName.split('.');
|
|
30
|
+
const domain = parts.slice(0, -1).join('/');
|
|
31
|
+
const action = parts[parts.length - 1];
|
|
32
|
+
const filePath = `services/${domain ? `${domain}/` : ''}${action}.js`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
messages: [
|
|
36
|
+
{
|
|
37
|
+
role: 'user',
|
|
38
|
+
content: {
|
|
39
|
+
type: 'text',
|
|
40
|
+
text: `Please generate a Webspresso service file for "${serviceName}" at path "${filePath}".
|
|
41
|
+
Description: ${description || 'N/A'}
|
|
42
|
+
|
|
43
|
+
Follow Webspresso rules:
|
|
44
|
+
- Export an object with { schema, auth, timeout, transaction, handler }.
|
|
45
|
+
- Use Zod (const { z } = require('zod');) for schema validation.
|
|
46
|
+
- Set transaction: true if multi-step mutation.
|
|
47
|
+
- Use req.db / ctx.db.getRepository('ModelName') to access data. NEVER write raw knex table queries.
|
|
48
|
+
- Throw semantic errors: NotFoundError, ValidationError, UnauthorizedError, ForbiddenError.
|
|
49
|
+
- Do NOT use controllers.`,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'scaffold_model',
|
|
58
|
+
description: 'Generates an ORM model file using defineModel and zdb schema builder.',
|
|
59
|
+
arguments: [
|
|
60
|
+
{
|
|
61
|
+
name: 'modelName',
|
|
62
|
+
description: 'PascalCase model name (e.g. "Product", "Invoice")',
|
|
63
|
+
required: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'tableName',
|
|
67
|
+
description: 'Database table name (e.g. "products", "invoices")',
|
|
68
|
+
required: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
handler: async ({ modelName, tableName }) => {
|
|
72
|
+
const table = tableName || `${modelName.toLowerCase()}s`;
|
|
73
|
+
const filePath = `models/${modelName.toLowerCase()}.js`;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
messages: [
|
|
77
|
+
{
|
|
78
|
+
role: 'user',
|
|
79
|
+
content: {
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: `Please generate a Webspresso ORM model for "${modelName}" (table: "${table}") at path "${filePath}".
|
|
82
|
+
|
|
83
|
+
Follow Webspresso rules:
|
|
84
|
+
- Use const { defineModel, zdb } = require('webspresso');
|
|
85
|
+
- Use defineModel({ name, table, schema, relations, scopes, hidden, admin }).
|
|
86
|
+
- Build schema with zdb: zdb.id(), zdb.string(), zdb.integer(), zdb.boolean(), zdb.json(), zdb.dateTime().
|
|
87
|
+
- Define relations if relevant (hasMany, belongsTo).
|
|
88
|
+
- Enable admin: { enabled: true, icon: '...' } if appropriate.`,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'scaffold_api',
|
|
97
|
+
description: 'Generates a file-based API route module inside pages/api/...',
|
|
98
|
+
arguments: [
|
|
99
|
+
{
|
|
100
|
+
name: 'endpoint',
|
|
101
|
+
description: 'API endpoint route (e.g. "/api/notes" or "/api/notes/[id]")',
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'method',
|
|
106
|
+
description: 'HTTP method (GET, POST, PUT, PATCH, DELETE)',
|
|
107
|
+
required: true,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
handler: async ({ endpoint, method }) => {
|
|
111
|
+
const m = method.toLowerCase();
|
|
112
|
+
let relativePath = endpoint.replace(/^\/api\//, '');
|
|
113
|
+
const filePath = `pages/api/${relativePath}.${m}.js`;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
messages: [
|
|
117
|
+
{
|
|
118
|
+
role: 'user',
|
|
119
|
+
content: {
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `Please generate a Webspresso API route file at "${filePath}".
|
|
122
|
+
Endpoint: ${method.toUpperCase()} ${endpoint}
|
|
123
|
+
|
|
124
|
+
Follow Webspresso rules:
|
|
125
|
+
- 100% File-Based Routing: NEVER create a routes/ folder or use express.Router().
|
|
126
|
+
- Standard API Route Module Contract:
|
|
127
|
+
module.exports = {
|
|
128
|
+
schema: ({ z }) => ({
|
|
129
|
+
body: z.object({...}),
|
|
130
|
+
params: z.object({...}),
|
|
131
|
+
}),
|
|
132
|
+
middleware: [],
|
|
133
|
+
handler: async (req, res) => {
|
|
134
|
+
// req.db and req.service are injected
|
|
135
|
+
// req.input contains validated { body, query, params }
|
|
136
|
+
}
|
|
137
|
+
};`,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
createBuiltinPrompts,
|
|
149
|
+
};
|
|
@@ -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
|
+
};
|