@enfyra/mcp-server 0.0.13 → 0.0.15
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/package.json +1 -1
- package/src/lib/mcp-instructions.js +20 -2
- package/src/lib/table-tools.js +2 -1
- package/src/mcp-server-entry.mjs +188 -53
package/package.json
CHANGED
|
@@ -33,8 +33,14 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
33
33
|
'- **Direct to Nest:** `http://localhost:1105` — no `/api` suffix on default Nest. Wrong: `http://localhost:1105/api/table_definition` (404) unless a proxy adds `/api`.',
|
|
34
34
|
'- GraphQL: `{base}/graphql` and `{base}/graphql-schema` always share this same base.',
|
|
35
35
|
'',
|
|
36
|
+
'### Routes vs tables (custom endpoints, handlers, hooks)',
|
|
37
|
+
'- If the user asks for a **new route**, **URL path**, **custom API endpoint**, **handler**, **pre-hook**, **post-hook**, or to **test** that kind of logic: use MCP **`create_route`** with **`mainTableId`** of an **existing** table (resolve id via **`get_all_tables`**, **`get_all_metadata`**, or **`get_all_routes`**).',
|
|
38
|
+
'- **Wrong pattern:** calling **`create_table`** just to get an HTTP path, then overriding handlers on the **default** auto route `/{table_name}`. That adds unnecessary schema and breaks the usual CRUD surface for that table.',
|
|
39
|
+
'- **`create_table`** is only when the user needs **new persisted data** (new entity + columns). It is **not** the right tool when the goal is only a new path or custom script.',
|
|
40
|
+
'- **Right pattern:** **`create_route`** → optional **`create_handler`** / **`create_pre_hook`** / **`create_post_hook`** on **that route’s id** (from **`get_all_routes`** after create). Same underlying table can have **multiple** routes (e.g. default CRUD at `/orders` and custom `/orders/stats` both pointing at `mainTable` orders).',
|
|
41
|
+
'',
|
|
36
42
|
'### After a new table is created',
|
|
37
|
-
'- Enfyra creates a route at `/{table_name}` using the table **name** from `create_table` (not the alias).',
|
|
43
|
+
'- Enfyra creates a **default** route at `/{table_name}` using the table **name** from `create_table` (not the alias). Prefer **`create_route`** for additional or custom paths instead of new tables.',
|
|
38
44
|
'- **Four REST HTTP operations** on that resource:',
|
|
39
45
|
` - **GET** \`${getList}\` — list / filter (query: filter, sort, page, limit, fields, meta).`,
|
|
40
46
|
` - **POST** \`${getList}\` — create (JSON body).`,
|
|
@@ -100,10 +106,21 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
100
106
|
'- Enfyra uses **Socket.IO**. Gateways and events are stored in **`websocket_definition`** and **`websocket_event_definition`**; manage via REST (MCP `create_record`, `update_record`, `query_table` on those tables).',
|
|
101
107
|
'- **Gateway** (`websocket_definition`): `path` = namespace (e.g. `/chat`), `requireAuth` (JWT in `auth.token`), `connectionHandlerScript` (runs on connect), `connectionHandlerTimeout`, `isEnabled`.',
|
|
102
108
|
'- **Event** (`websocket_event_definition`): `gateway` → gateway id, `eventName` (client emits), `handlerScript`, `timeout`, `isEnabled`.',
|
|
103
|
-
'- **@SOCKET
|
|
109
|
+
'- **@SOCKET in scripts (prefer template `@SOCKET.method()` over `$ctx.$socket.method()`):**',
|
|
110
|
+
'- `@SOCKET.reply(event, data)` — send to this client only (WS context only).',
|
|
111
|
+
'- `@SOCKET.join(room)` — join a room (WS context only).',
|
|
112
|
+
'- `@SOCKET.leave(room)` — leave a room (WS context only).',
|
|
113
|
+
'- `@SOCKET.emitToUser(userId, event, data)` — send to a specific user (across all gateways).',
|
|
114
|
+
'- `@SOCKET.emitToRoom(room, event, data)` — send to a named room (across all gateways).',
|
|
115
|
+
'- `@SOCKET.emitToGateway(path, event, data)` — broadcast to all connections on a gateway/namespace.',
|
|
116
|
+
'- `@SOCKET.broadcast(event, data)` — broadcast to all connections on all gateways.',
|
|
117
|
+
'- `@SOCKET.disconnect()` — force-disconnect the current socket from the gateway (WS context only). Use in connection handler to reject, or in event handler to kick user.',
|
|
118
|
+
'- In **HTTP** handler/hook context: `reply`, `join`, `leave`, `disconnect` are not available (no socket). Use `emitToUser`, `emitToRoom`, `emitToGateway`, `broadcast`.',
|
|
104
119
|
'- **Context**: Connection — `@BODY` = {id, ip, headers}, `@USER` if auth. Event — `@BODY` = payload, `@USER` if auth. Both have `@SOCKET`.',
|
|
120
|
+
'- **ACK + results (recommended UX):** client can emit an event with Socket.IO ack callback. Server immediately acks `{ queued: true, requestId, eventName }` (or `{ queued: false, error }`). The handler result is returned asynchronously via `ws:result` or `ws:error` with the same `requestId`.',
|
|
105
121
|
'- **Client**: `io("<HTTP_ORIGIN>/namespace", {auth: {token: JWT}})`. Use the **origin where Socket.IO is served** (usually the **Nest** HTTP origin, e.g. `http://localhost:1105/chat` in local server-only setups). If Socket.IO is exposed only through the Nuxt app, use that host and your deployment’s WS path—**do not** assume port 3000 without checking `API_URL` / proxy config. Gateway `path` in metadata = Socket.IO **namespace**.',
|
|
106
122
|
'- **Workflow**: Create gateway → `create_record` on `websocket_definition`. Create event → `create_record` on `websocket_event_definition` with `gateway: {id}`. Changes auto-reload; test handlers before saving.',
|
|
123
|
+
'- **Test WS handler (recommended):** `POST {base}/admin/test/run` with `{ kind:"websocket_event", gatewayPath, eventName, timeoutMs, payload, script }` to run a websocket event script without a real client. Returns `{ success, result, logs, emitted }`.',
|
|
107
124
|
'',
|
|
108
125
|
'### Flows (Automated Workflows)',
|
|
109
126
|
'- Enfyra supports automated workflows via **`flow_definition`**, **`flow_step_definition`**, and **`flow_execution_definition`** tables.',
|
|
@@ -199,6 +216,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
199
216
|
'- API testing is available at `/settings/api-tester` in the app UI.',
|
|
200
217
|
'',
|
|
201
218
|
'### MCP tool → HTTP',
|
|
219
|
+
'- **Routes:** `create_route` / `create_handler` / `create_pre_hook` / `create_post_hook` persist to `route_definition`, `route_handler_definition`, etc. (REST CRUD on those tables). Prefer **`create_route`** for new paths — not `create_table`.',
|
|
202
220
|
`- \`get_all_metadata\` → GET \`${base}/metadata\``,
|
|
203
221
|
`- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
|
|
204
222
|
`- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args)`,
|
package/src/lib/table-tools.js
CHANGED
|
@@ -39,9 +39,10 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
39
39
|
'create_table',
|
|
40
40
|
[
|
|
41
41
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
42
|
+
'**Not** for adding a custom API path or handler only — for that use **`create_route`** with an existing `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
42
43
|
'Use create_column to add more columns after creation (columns are managed via cascade PATCH on table_definition, NOT via /column_definition).',
|
|
43
44
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
44
|
-
'Enfyra auto-creates a REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
45
|
+
'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
45
46
|
'REST surface for that route (matches server route engine): 4 HTTP operations — GET `/<table>` (list/filter), POST `/<table>` (create), PATCH `/<table>/:id` (update), DELETE `/<table>/:id` (delete).',
|
|
46
47
|
'There is NO `GET /<table>/:id`. To fetch one row by id, use GET `/<table>?filter={"id":{"_eq":"<id>"}}&limit=1` or tool query_table / find_one_record.',
|
|
47
48
|
`Full URLs: ${apiBase}/<table_name> (example table post: ${apiBase}/post).`,
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -177,7 +177,26 @@ server.tool('delete_record', 'Delete a record by ID', {
|
|
|
177
177
|
// ROUTE & HANDLER TOOLS
|
|
178
178
|
// ============================================================================
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
let _methodMap = null;
|
|
181
|
+
async function getMethodMap() {
|
|
182
|
+
if (_methodMap) return _methodMap;
|
|
183
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?limit=0');
|
|
184
|
+
_methodMap = {};
|
|
185
|
+
for (const m of result.data) {
|
|
186
|
+
_methodMap[m.method] = m.id || m._id;
|
|
187
|
+
}
|
|
188
|
+
return _methodMap;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveMethodIds(methodMap, names) {
|
|
192
|
+
return names.map(m => {
|
|
193
|
+
const id = methodMap[m.toUpperCase()];
|
|
194
|
+
if (!id) throw new Error(`Unknown method "${m}". Valid: ${Object.keys(methodMap).join(', ')}`);
|
|
195
|
+
return { id };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
server.tool('get_all_routes', 'List all route definitions (path, mainTable, handlers, hooks, permissions). Call before create_route to avoid duplicate paths and to pick routeId for hooks/handlers.', {
|
|
181
200
|
includeDisabled: z.boolean().optional().default(false).describe('Include disabled routes'),
|
|
182
201
|
}, async ({ includeDisabled }) => {
|
|
183
202
|
const filter = includeDisabled ? {} : { isEnabled: { _eq: true } };
|
|
@@ -185,57 +204,168 @@ server.tool('get_all_routes', 'Get all route definitions with handlers, hooks, a
|
|
|
185
204
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
186
205
|
});
|
|
187
206
|
|
|
188
|
-
server.tool(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
server.tool(
|
|
208
|
+
'create_route',
|
|
209
|
+
[
|
|
210
|
+
'**Use this when the user wants a new API route or path** — not `create_table`. A route links a URL path to an existing table (`mainTableId`) and sets HTTP/GQL methods.',
|
|
211
|
+
'Do NOT create a new table_definition only to expose an endpoint; pick `mainTableId` from existing metadata unless the user explicitly needs new tables/columns.',
|
|
212
|
+
'availableMethods = which verbs the route responds to. publishedMethods = which verbs are public (no auth).',
|
|
213
|
+
'After creation the tool auto-reloads routes. Then create handlers for specific methods via create_handler on this route id.',
|
|
214
|
+
'Flow: resolve table id → create_route → create_handler (per method) → optionally create_pre_hook / create_post_hook → test via HTTP or admin test APIs (see server instructions).',
|
|
215
|
+
].join(' '),
|
|
216
|
+
{
|
|
217
|
+
path: z.string().describe('URL path, must start with / (e.g., "/my-endpoint")'),
|
|
218
|
+
mainTableId: z.union([z.string(), z.number()]).describe('ID of the table_definition this route operates on. The route\'s $repos.main will query this table.'),
|
|
219
|
+
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE', 'GQL_QUERY', 'GQL_MUTATION']))
|
|
220
|
+
.describe('HTTP/GQL methods this route supports (availableMethods). Common: ["GET","POST","PATCH","DELETE"]'),
|
|
221
|
+
publishedMethods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE', 'GQL_QUERY', 'GQL_MUTATION'])).optional()
|
|
222
|
+
.describe('Methods accessible WITHOUT auth token. Omit = all methods require auth.'),
|
|
223
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable route immediately'),
|
|
224
|
+
description: z.string().optional().describe('Route description'),
|
|
225
|
+
},
|
|
226
|
+
async ({ path: routePath, mainTableId, methods, publishedMethods, isEnabled, description }) => {
|
|
227
|
+
const methodMap = await getMethodMap();
|
|
201
228
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
body: JSON.stringify({ routeId, logic, timeout: timeout || 30000 }),
|
|
210
|
-
});
|
|
211
|
-
return { content: [{ type: 'text', text: `Handler created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
212
|
-
});
|
|
229
|
+
const body = {
|
|
230
|
+
path: routePath.startsWith('/') ? routePath : '/' + routePath,
|
|
231
|
+
mainTable: { id: mainTableId },
|
|
232
|
+
isEnabled,
|
|
233
|
+
description,
|
|
234
|
+
availableMethods: resolveMethodIds(methodMap, methods),
|
|
235
|
+
};
|
|
213
236
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])).optional().describe('Methods this hook applies to'),
|
|
218
|
-
order: z.number().optional().default(0).describe('Hook execution order'),
|
|
219
|
-
}, async ({ routeId, code, methods, order }) => {
|
|
220
|
-
const result = await fetchAPI(ENFYRA_API_URL, '/pre_hook_definition', {
|
|
221
|
-
method: 'POST',
|
|
222
|
-
body: JSON.stringify({ routeId, code, methods: methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], order }),
|
|
223
|
-
});
|
|
224
|
-
return { content: [{ type: 'text', text: `Pre-hook created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
225
|
-
});
|
|
237
|
+
if (publishedMethods && publishedMethods.length > 0) {
|
|
238
|
+
body.publishedMethods = resolveMethodIds(methodMap, publishedMethods);
|
|
239
|
+
}
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/route_definition', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
body: JSON.stringify(body),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
247
|
+
|
|
248
|
+
return { content: [{ type: 'text', text: `Route created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
server.tool(
|
|
253
|
+
'create_handler',
|
|
254
|
+
[
|
|
255
|
+
'Create a handler for a route+method. One handler per (route, method) pair.',
|
|
256
|
+
'Attach to the route the user cares about (`get_all_routes`): typically a path from `create_route`, not a spurious table created only for handlers.',
|
|
257
|
+
'Handler code runs inside a sandbox with $ctx. Use macros: @BODY, @QUERY, @PARAMS, @USER, @REPOS, @HELPERS, @THROW400..@THROW503, @SOCKET, @PKGS, @LOGS, @SHARE.',
|
|
258
|
+
'Or use $ctx directly: $ctx.$body, $ctx.$repos.main.find(), $ctx.$helpers.$bcrypt.hash(), etc.',
|
|
259
|
+
'require("pkg") works for installed Server packages. console.log() writes to $share.$logs.',
|
|
260
|
+
].join(' '),
|
|
261
|
+
{
|
|
262
|
+
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
263
|
+
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE', 'GQL_QUERY', 'GQL_MUTATION']))
|
|
264
|
+
.describe('Methods to create handlers for. Creates one handler per method.'),
|
|
265
|
+
logic: z.string().describe('Handler JavaScript code'),
|
|
266
|
+
timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
|
|
267
|
+
},
|
|
268
|
+
async ({ routeId, methods, logic, timeout }) => {
|
|
269
|
+
const methodMap = await getMethodMap();
|
|
270
|
+
const results = [];
|
|
271
|
+
|
|
272
|
+
for (const method of methods) {
|
|
273
|
+
const methodId = methodMap[method.toUpperCase()];
|
|
274
|
+
if (!methodId) throw new Error(`Unknown method: ${method}. Valid: ${Object.keys(methodMap).join(', ')}`);
|
|
275
|
+
|
|
276
|
+
const body = { route: { id: routeId }, method: { id: methodId }, logic };
|
|
277
|
+
if (timeout) body.timeout = timeout;
|
|
278
|
+
|
|
279
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/route_handler_definition', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
body: JSON.stringify(body),
|
|
282
|
+
});
|
|
283
|
+
results.push(result);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
287
|
+
|
|
288
|
+
return { content: [{ type: 'text', text: `Handler(s) created for [${methods.join(', ')}]. Routes reloaded.\n${JSON.stringify(results, null, 2)}` }] };
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
server.tool(
|
|
293
|
+
'create_pre_hook',
|
|
294
|
+
[
|
|
295
|
+
'Create a pre-hook that runs BEFORE the handler. Use to validate, transform, or inject data.',
|
|
296
|
+
'Use `routeId` from `create_route` or `get_all_routes` — do not create a new table just to get a route id.',
|
|
297
|
+
'Macros: @BODY, @QUERY, @PARAMS, @USER, @REPOS, @HELPERS, @THROW400..@THROW503.',
|
|
298
|
+
'If the hook returns a value, that value becomes the response (handler is skipped).',
|
|
299
|
+
].join(' '),
|
|
300
|
+
{
|
|
301
|
+
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
302
|
+
name: z.string().describe('Hook name (unique per route)'),
|
|
303
|
+
code: z.string().describe('Hook JavaScript code'),
|
|
304
|
+
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
|
|
305
|
+
.describe('Methods this hook applies to. Default: all REST methods.'),
|
|
306
|
+
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
307
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
308
|
+
},
|
|
309
|
+
async ({ routeId, name, code, methods, priority, isEnabled }) => {
|
|
310
|
+
const methodMap = await getMethodMap();
|
|
311
|
+
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
312
|
+
|
|
313
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/pre_hook_definition', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
route: { id: routeId },
|
|
317
|
+
name,
|
|
318
|
+
code,
|
|
319
|
+
methods: resolveMethodIds(methodMap, methodNames),
|
|
320
|
+
priority,
|
|
321
|
+
isEnabled,
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
326
|
+
|
|
327
|
+
return { content: [{ type: 'text', text: `Pre-hook "${name}" created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
server.tool(
|
|
332
|
+
'create_post_hook',
|
|
333
|
+
[
|
|
334
|
+
'Create a post-hook that runs AFTER the handler. Use to transform responses or add metadata.',
|
|
335
|
+
'Use `routeId` from `create_route` or `get_all_routes` — do not create a new table just to get a route id.',
|
|
336
|
+
'Macros: @DATA (handler result), @STATUS (HTTP status code), @BODY, @QUERY, @USER, @SHARE.',
|
|
337
|
+
'Must return a value — that becomes the final response.',
|
|
338
|
+
].join(' '),
|
|
339
|
+
{
|
|
340
|
+
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
341
|
+
name: z.string().describe('Hook name (unique per route)'),
|
|
342
|
+
code: z.string().describe('Hook JavaScript code'),
|
|
343
|
+
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
|
|
344
|
+
.describe('Methods this hook applies to. Default: all REST methods.'),
|
|
345
|
+
priority: z.number().optional().default(0).describe('Execution order (lower = first)'),
|
|
346
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable hook immediately'),
|
|
347
|
+
},
|
|
348
|
+
async ({ routeId, name, code, methods, priority, isEnabled }) => {
|
|
349
|
+
const methodMap = await getMethodMap();
|
|
350
|
+
const methodNames = methods || ['GET', 'POST', 'PATCH', 'DELETE'];
|
|
351
|
+
|
|
352
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/post_hook_definition', {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
route: { id: routeId },
|
|
356
|
+
name,
|
|
357
|
+
code,
|
|
358
|
+
methods: resolveMethodIds(methodMap, methodNames),
|
|
359
|
+
priority,
|
|
360
|
+
isEnabled,
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
365
|
+
|
|
366
|
+
return { content: [{ type: 'text', text: `Post-hook "${name}" created (ID: ${result.id}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
|
|
367
|
+
},
|
|
368
|
+
);
|
|
239
369
|
|
|
240
370
|
// Register table tools
|
|
241
371
|
registerTableTools(server, ENFYRA_API_URL);
|
|
@@ -410,8 +540,8 @@ server.tool(
|
|
|
410
540
|
pkgDescription = exactMatch.package.description || '';
|
|
411
541
|
}
|
|
412
542
|
|
|
413
|
-
// Step 2: Check if already installed
|
|
414
|
-
const checkFilter = JSON.stringify({ name: { _eq: name } });
|
|
543
|
+
// Step 2: Check if already installed (same name AND type)
|
|
544
|
+
const checkFilter = JSON.stringify({ name: { _eq: name }, type: { _eq: type } });
|
|
415
545
|
const existing = await fetchAPI(ENFYRA_API_URL, `/package_definition?filter=${encodeURIComponent(checkFilter)}&limit=1`);
|
|
416
546
|
if (existing.data && existing.data.length > 0) {
|
|
417
547
|
return {
|
|
@@ -456,7 +586,7 @@ server.tool(
|
|
|
456
586
|
|
|
457
587
|
server.tool('create_menu', 'Create a menu item in the navigation', {
|
|
458
588
|
label: z.string().describe('Menu label'),
|
|
459
|
-
type: z.enum(['
|
|
589
|
+
type: z.enum(['Menu', 'Dropdown Menu']).default('Menu').describe('Menu type: "Menu" for leaf items, "Dropdown Menu" for items with children'),
|
|
460
590
|
icon: z.string().optional().describe('Lucide icon name'),
|
|
461
591
|
path: z.string().optional().describe('Route path for type=route'),
|
|
462
592
|
externalUrl: z.string().optional().describe('External URL for type=link'),
|
|
@@ -464,7 +594,11 @@ server.tool('create_menu', 'Create a menu item in the navigation', {
|
|
|
464
594
|
isEnabled: z.boolean().optional().default(true).describe('Enable menu'),
|
|
465
595
|
description: z.string().optional().describe('Menu description'),
|
|
466
596
|
}, async (data) => {
|
|
467
|
-
const
|
|
597
|
+
const body = { ...data };
|
|
598
|
+
if (body.path && !body.path.startsWith('/')) {
|
|
599
|
+
body.path = '/' + body.path;
|
|
600
|
+
}
|
|
601
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/menu_definition', { method: 'POST', body: JSON.stringify(body) });
|
|
468
602
|
return { content: [{ type: 'text', text: `Menu created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
469
603
|
});
|
|
470
604
|
|
|
@@ -481,6 +615,7 @@ server.tool(
|
|
|
481
615
|
menuId: z.string().optional().describe('Required for type=page — menu_definition id from create_menu. Omit for widget'),
|
|
482
616
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
|
|
483
617
|
description: z.string().optional().describe('Extension description'),
|
|
618
|
+
version: z.string().optional().default('1.0.0').describe('Extension version'),
|
|
484
619
|
},
|
|
485
620
|
async (data) => {
|
|
486
621
|
const body = { ...data };
|