@enfyra/mcp-server 0.0.52 → 0.0.53
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-examples.js +31 -0
- package/src/lib/mcp-instructions.js +6 -0
- package/src/mcp-server-entry.mjs +208 -37
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -216,6 +216,20 @@ create_column({
|
|
|
216
216
|
title: 'REST queries, filters, meta counts, and deep relation fetches',
|
|
217
217
|
useWhen: 'Use when fetching records, filtering by relations, loading nested data, or counting efficiently.',
|
|
218
218
|
examples: [
|
|
219
|
+
{
|
|
220
|
+
name: 'Minimal MCP query then explicit detail query',
|
|
221
|
+
code: `query_table({
|
|
222
|
+
tableName: "user_definition",
|
|
223
|
+
fields: ["id", "email"],
|
|
224
|
+
filter: "{\\"email\\":{\\"_contains\\":\\"@example.com\\"}}",
|
|
225
|
+
limit: 10
|
|
226
|
+
})`,
|
|
227
|
+
notes: [
|
|
228
|
+
'Always pass fields when you need more than ids; query_table without fields intentionally returns only the primary key.',
|
|
229
|
+
'Use inspect_table first when you do not know valid column names or relation propertyName values.',
|
|
230
|
+
'Use count_records when only the count is needed.',
|
|
231
|
+
],
|
|
232
|
+
},
|
|
219
233
|
{
|
|
220
234
|
name: 'List current user conversations through RLS',
|
|
221
235
|
code: `GET /enfyra/chat_conversation?fields=id,kind,title,lastMessage.id,lastMessage.text,lastMessage.createdAt&limit=0`,
|
|
@@ -269,6 +283,23 @@ create_column({
|
|
|
269
283
|
title: 'Custom handlers, pre-hooks, post-hooks, and script macros',
|
|
270
284
|
useWhen: 'Use when writing Enfyra dynamic JavaScript for REST behavior.',
|
|
271
285
|
examples: [
|
|
286
|
+
{
|
|
287
|
+
name: 'Create a route handler with current script fields',
|
|
288
|
+
code: `create_handler({
|
|
289
|
+
routeId: "<route_id>",
|
|
290
|
+
method: "POST",
|
|
291
|
+
scriptLanguage: "javascript",
|
|
292
|
+
sourceCode: \`const email = @BODY.email
|
|
293
|
+
if (!email) @THROW400("Email is required")
|
|
294
|
+
|
|
295
|
+
return { ok: true, email }\`
|
|
296
|
+
})`,
|
|
297
|
+
notes: [
|
|
298
|
+
'Use sourceCode, not logic. The server generates compiledCode.',
|
|
299
|
+
'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
|
|
300
|
+
'Do not pass name to route_handler_definition; one handler is identified by route + method.',
|
|
301
|
+
],
|
|
302
|
+
},
|
|
272
303
|
{
|
|
273
304
|
name: 'Custom register handler',
|
|
274
305
|
code: `const email = @BODY.email
|
|
@@ -36,6 +36,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
36
36
|
'- If generating concrete code, schema payloads, SSR app config, OAuth wiring, Socket.IO clients/events, flows, files, extensions, or permission/RLS examples, call **`get_enfyra_examples`** for the matching category before writing the final answer. Examples are grouped by category and are intentionally more concrete than these global rules.',
|
|
37
37
|
'- Treat hardcoded instructions as operating rules, but use live discovery as the final check for this running instance. Do not infer missing capabilities from a narrow tool schema; check metadata/routes or the relevant specialized tool first.',
|
|
38
38
|
'- If there is no dedicated MCP tool for a subsystem, use the route-backed metadata table with `query_table` / `create_record` / `update_record` / `delete_record`, after confirming that table has a route. If the table is no-route, use the canonical specialized tool or parent table workflow instead.',
|
|
39
|
+
'- MCP read tools are intentionally **minimal by default**. `query_table` without `fields` returns only the table primary key with a small hint. Always pass explicit `fields` when you need details, and use `inspect_table` / `inspect_route` before guessing field names.',
|
|
40
|
+
'- MCP mutation tools return only ids/status by default. If you need the saved row, immediately call `find_one_record` or `query_table` with explicit `fields`; do not expect create/update tools to echo full records.',
|
|
39
41
|
'',
|
|
40
42
|
'### Capability map (current Enfyra system)',
|
|
41
43
|
'- **Schema/metadata:** `table_definition`, `relation_definition`, and schema tools manage tables, columns, relations, validation, and migrations. `column_definition` is internal/no-route; columns are created/updated through table schema operations.',
|
|
@@ -74,6 +76,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
74
76
|
'- **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.',
|
|
75
77
|
'- **`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.',
|
|
76
78
|
'- **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).',
|
|
79
|
+
'- **Handler contract:** `create_handler` takes `routeId`, `method` (or `methods` for batch), `sourceCode`, optional `scriptLanguage`, and optional `timeout`. Do **not** send `logic`, `name`, or `compiledCode`; backend CRUD rejects `logic` and `compiledCode` is generated by the server.',
|
|
77
80
|
'',
|
|
78
81
|
'### After a new table is created',
|
|
79
82
|
'- MCP **`create_table` supports creating columns and relations in the same call**: pass `columns` and `relations` as JSON arrays. Use `create_relation` only when adding a relation to an existing table later.',
|
|
@@ -103,6 +106,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
103
106
|
`- **No** **GET** \`${base}/<table_name>/<id>\`. For one row by id use **GET** \`${getOneById}\` or MCP \`query_table\` / \`find_one_record\`.`,
|
|
104
107
|
'',
|
|
105
108
|
'### Relation field format (create_record / update_record)',
|
|
109
|
+
'- For generic MCP `create_record` and `update_record`, the `data` argument is a **JSON string**, not a JavaScript object. Example: `data: "{\\"name\\":\\"Starter\\"}"`. If the host gives a validation error saying `data` expected string, stringify the object before calling the tool.',
|
|
106
110
|
'- Relation fields (mainTable, publishedMethods, availableMethods, handlers, preHooks, postHooks, etc.) use **object references with `id`**:',
|
|
107
111
|
' - **Many-to-one:** `"mainTable": {"id": 4}` (single object with id)',
|
|
108
112
|
' - **One-to-many / many-to-many:** `"publishedMethods": [{"id": 1}, {"id": 2}]` (array of objects with id)',
|
|
@@ -134,6 +138,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
134
138
|
'- For encrypted persisted fields such as `*_encrypted`, use an Enfyra route pre-hook, not a Knex/database hook. Mutate the body before persistence: `const value = @BODY.field_encrypted; if (value && value.slice(0, 7) !== "enc:v1:") @BODY.field_encrypted = @HELPERS.$encrypt.encrypt(value);`.',
|
|
135
139
|
'- ASV exposes `$helpers.$encrypt.encrypt/decrypt` for encrypted strings and `$helpers.$ssh.generateKeyPair` for SSH keys. Do not generate `$helpers.$secrets` usage.',
|
|
136
140
|
'- Script-backed records use one shared persistence contract: `sourceCode` is the editable source, `scriptLanguage` controls compilation, and `compiledCode` is generated by the server from `sourceCode`. Do not hand-edit or send stale `compiledCode` from generated tools; save `sourceCode`/`scriptLanguage` through `PATCH /<script_table>/<id>` and let the server persist generated `compiledCode` internally. Public metadata may mark `compiledCode` non-updatable, but the server engine must still preserve the generated value after normalization.',
|
|
141
|
+
'- For route handlers specifically, the field is also `sourceCode`. Older names such as `logic` are wrong for current Enfyra REST CRUD and will be rejected. Use MCP `create_handler` so it writes `sourceCode` and resolves method ids correctly.',
|
|
137
142
|
'- MCP `create_pre_hook` and `create_post_hook` accept a user-facing `code` argument but persist it as `sourceCode` with `scriptLanguage`. Do not call raw `create_record` with a `code` field for hook tables; backend request validation rejects `code` on REST CRUD.',
|
|
138
143
|
'- Enfyra Cloud host provisioning with PgBouncer must preserve tenant database isolation. PgBouncer should use per-tenant `DATABASE_URLS` entries with each tenant `db_user`, `db_password`, and `db_name`, and PostgreSQL 16/SCRAM hosts need `AUTH_TYPE=plain`. Do not route all tenants through PostgreSQL `postgres` just to make PgBouncer connect; that bypasses tenant DB permissions.',
|
|
139
144
|
'- Enfyra Cloud Docker health checks must compare exact healthy states. `true healthy` passes, `true starting` keeps waiting, and `true unhealthy` fails. Do not use broad substring logic where `unhealthy` accidentally counts as healthy.',
|
|
@@ -192,6 +197,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
192
197
|
'### System tables — which have REST routes',
|
|
193
198
|
'- **Not all system tables have a REST route.** `query_table`, `find_one_record`, `create_record`, etc. all go through the dynamic REST API and will return **404** if the table has no registered route.',
|
|
194
199
|
'- **`column_definition` and `session_definition` have NO route** — do NOT call `query_table("column_definition", …)` or `query_table("session_definition", …)`. They will 404.',
|
|
200
|
+
'- Do not invent singular/legacy system route names such as `hook_definition`, `oauth_provider_definition`, or physical FK tables. If a route name is not listed by `get_all_routes`, it is not a REST endpoint for generic CRUD. Use the concrete tables (`pre_hook_definition`, `post_hook_definition`, `oauth_config_definition`, etc.) or the dedicated MCP tool.',
|
|
195
201
|
'- To check which tables are accessible via MCP tools, call `get_all_routes` and look for the route whose `mainTable.id` matches the table you need, or `get_all_metadata` to see all table names.',
|
|
196
202
|
'- **Tables confirmed to have REST routes (system):** `bootstrap_script_definition`, `column_rule_definition`, `cors_origin_definition`, `extension_definition`, `field_permission_definition`, `file_definition`, `file_permission_definition`, `flow_definition`, `flow_execution_definition`, `flow_step_definition`, `folder_definition`, `gql_definition`, `guard_definition`, `guard_rule_definition`, `menu_definition`, `method_definition`, `oauth_account_definition`, `oauth_config_definition`, `package_definition`, `post_hook_definition`, `pre_hook_definition`, `relation_definition`, `role_definition`, `route_definition`, `route_handler_definition`, `route_permission_definition`, `schema_migration_definition`, `setting_definition`, `storage_config_definition`, `table_definition`, `user_definition`, `websocket_definition`, `websocket_event_definition`.',
|
|
197
203
|
'- **Tables without REST routes (internal/system only):** `column_definition`, `session_definition`. Columns are managed indirectly via cascade on `table_definition` (POST/PATCH with columns arrays). The `create_table`, `create_column`/`add_column`, `update_column`, and `delete_column`/`remove_column` MCP tools handle this automatically.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -199,6 +199,31 @@ function summarizeRoutes(routesResult) {
|
|
|
199
199
|
}));
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
function summarizeMetadata(metadata, { search, limit } = {}) {
|
|
203
|
+
const tables = normalizeTables(metadata);
|
|
204
|
+
const q = search ? search.toLowerCase() : null;
|
|
205
|
+
const summarized = tables.map((table) => ({
|
|
206
|
+
id: table.id ?? table._id,
|
|
207
|
+
name: table.name,
|
|
208
|
+
alias: table.alias,
|
|
209
|
+
primaryKey: getPrimaryColumn(table)?.name || null,
|
|
210
|
+
columnCount: (table.columns || []).length,
|
|
211
|
+
relationCount: (table.relations || []).length,
|
|
212
|
+
routeHint: `Use get_table_metadata({ tableName: "${table.name}" }) for fields and relations.`,
|
|
213
|
+
}));
|
|
214
|
+
const matched = q
|
|
215
|
+
? summarized.filter((table) => JSON.stringify(table).toLowerCase().includes(q))
|
|
216
|
+
: summarized;
|
|
217
|
+
const outputLimit = limit || 30;
|
|
218
|
+
return {
|
|
219
|
+
tableCount: tables.length,
|
|
220
|
+
matchedTableCount: matched.length,
|
|
221
|
+
returnedTableCount: Math.min(matched.length, outputLimit),
|
|
222
|
+
search: search || null,
|
|
223
|
+
tables: matched.slice(0, outputLimit),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
function unwrapData(result) {
|
|
203
228
|
return Array.isArray(result?.data) ? result.data : [];
|
|
204
229
|
}
|
|
@@ -250,6 +275,29 @@ function pickCodeSummary(record, fieldName) {
|
|
|
250
275
|
};
|
|
251
276
|
}
|
|
252
277
|
|
|
278
|
+
function summarizeMutationResult(result, action, tableName) {
|
|
279
|
+
const record = firstDataRecord(result);
|
|
280
|
+
return {
|
|
281
|
+
action,
|
|
282
|
+
tableName,
|
|
283
|
+
id: getId(record),
|
|
284
|
+
statusCode: result?.statusCode,
|
|
285
|
+
success: result?.success,
|
|
286
|
+
detailHint: `Use find_one_record or query_table with explicit fields to inspect ${tableName}.`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function getTableSummary(tableName) {
|
|
291
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
|
|
292
|
+
const table = result?.data?.table || result?.data || result?.table || result;
|
|
293
|
+
return summarizeTable(table);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function getPrimaryFieldName(tableName) {
|
|
297
|
+
const table = await getTableSummary(tableName);
|
|
298
|
+
return table?.primaryKey || 'id';
|
|
299
|
+
}
|
|
300
|
+
|
|
253
301
|
async function fetchAll(path) {
|
|
254
302
|
return unwrapData(await fetchAPI(ENFYRA_API_URL, path));
|
|
255
303
|
}
|
|
@@ -290,16 +338,38 @@ const server = new McpServer(
|
|
|
290
338
|
// METADATA TOOLS
|
|
291
339
|
// ============================================================================
|
|
292
340
|
|
|
293
|
-
server.tool('get_all_metadata', 'Get
|
|
341
|
+
server.tool('get_all_metadata', 'Get concise metadata summary for all tables. Use get_table_metadata or inspect_table for detail.', {
|
|
342
|
+
includeFull: z.boolean().optional().default(false).describe('Return full raw metadata. Default false to keep MCP context small.'),
|
|
343
|
+
search: z.string().optional().describe('Optional table-name/alias substring filter.'),
|
|
344
|
+
limit: z.number().optional().describe('Maximum tables returned after search. Default 30.'),
|
|
345
|
+
}, async ({ includeFull, search, limit }) => {
|
|
294
346
|
const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
295
|
-
|
|
347
|
+
const payload = includeFull
|
|
348
|
+
? result
|
|
349
|
+
: {
|
|
350
|
+
statusCode: result?.statusCode,
|
|
351
|
+
success: result?.success,
|
|
352
|
+
...summarizeMetadata(result, { search, limit }),
|
|
353
|
+
detailHint: 'Default response is capped and minimal. Call get_table_metadata({ tableName }) or inspect_table({ tableName }) for columns, relations, and route context.',
|
|
354
|
+
};
|
|
355
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
296
356
|
});
|
|
297
357
|
|
|
298
|
-
server.tool('get_table_metadata', 'Get metadata for a specific table by name', {
|
|
358
|
+
server.tool('get_table_metadata', 'Get concise metadata for a specific table by name', {
|
|
299
359
|
tableName: z.string().describe('Table name (e.g., "user_definition", "route_definition")'),
|
|
300
|
-
|
|
360
|
+
includeFull: z.boolean().optional().default(false).describe('Return full raw table metadata. Default false to keep MCP context small.'),
|
|
361
|
+
}, async ({ tableName, includeFull }) => {
|
|
301
362
|
const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
|
|
302
|
-
|
|
363
|
+
const table = result?.data?.table || result?.data || result?.table || result;
|
|
364
|
+
const payload = includeFull
|
|
365
|
+
? result
|
|
366
|
+
: {
|
|
367
|
+
statusCode: result?.statusCode,
|
|
368
|
+
success: result?.success,
|
|
369
|
+
table: summarizeTable(table),
|
|
370
|
+
queryHint: `Use query_table({ tableName: "${tableName}", fields: [...] }) for records. query_table without fields returns only the primary key.`,
|
|
371
|
+
};
|
|
372
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
303
373
|
});
|
|
304
374
|
|
|
305
375
|
server.tool(
|
|
@@ -707,27 +777,41 @@ server.tool(
|
|
|
707
777
|
},
|
|
708
778
|
);
|
|
709
779
|
|
|
710
|
-
server.tool('query_table', 'Query any table
|
|
780
|
+
server.tool('query_table', 'Query any route-backed table. Default response is minimal; pass fields explicitly for detail.', {
|
|
711
781
|
tableName: z.string().describe('Table name to query'),
|
|
712
782
|
filter: z.string().optional().describe('Filter object as JSON string. Examples: \'{"status": {"_eq": "active"}}\''),
|
|
713
783
|
sort: z.string().optional().describe('Sort field. Prefix with - for descending (e.g., "createdAt", "-id")'),
|
|
714
784
|
page: z.number().optional().describe('Page number (default: 1)'),
|
|
715
|
-
limit: z.number().optional().describe('Items per page
|
|
716
|
-
fields: z.array(z.string()).optional().describe('Fields to select'),
|
|
785
|
+
limit: z.number().optional().describe('Items per page. Default: 10. Use count_records for counts.'),
|
|
786
|
+
fields: z.array(z.string()).optional().describe('Fields to select. If omitted, MCP selects only the table primary key to avoid oversized responses.'),
|
|
717
787
|
}, async ({ tableName, filter, sort, page, limit, fields }) => {
|
|
718
788
|
validateTableName(tableName);
|
|
719
789
|
validateFilter(filter);
|
|
720
790
|
|
|
721
791
|
const queryParams = new URLSearchParams();
|
|
792
|
+
const selectedFields = fields && fields.length > 0 ? fields : [await getPrimaryFieldName(tableName)];
|
|
722
793
|
if (filter) queryParams.set('filter', filter);
|
|
723
794
|
if (sort) queryParams.set('sort', sort);
|
|
724
795
|
if (page) queryParams.set('page', String(page));
|
|
725
|
-
|
|
726
|
-
|
|
796
|
+
queryParams.set('limit', String(limit || 10));
|
|
797
|
+
queryParams.set('fields', selectedFields.join(','));
|
|
727
798
|
|
|
728
799
|
const query = queryParams.toString();
|
|
729
800
|
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}${query ? `?${query}` : ''}`);
|
|
730
|
-
|
|
801
|
+
const payload = {
|
|
802
|
+
statusCode: result?.statusCode,
|
|
803
|
+
success: result?.success,
|
|
804
|
+
tableName,
|
|
805
|
+
fields: selectedFields,
|
|
806
|
+
limit: limit || 10,
|
|
807
|
+
minimalDefaultApplied: !(fields && fields.length > 0),
|
|
808
|
+
meta: result?.meta,
|
|
809
|
+
data: result?.data || [],
|
|
810
|
+
detailHint: fields && fields.length > 0
|
|
811
|
+
? undefined
|
|
812
|
+
: 'Only the primary key was returned because fields was omitted. Re-run query_table with explicit fields for details, or use inspect_table to find valid field names.',
|
|
813
|
+
};
|
|
814
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
731
815
|
});
|
|
732
816
|
|
|
733
817
|
server.tool(
|
|
@@ -780,26 +864,50 @@ server.tool(
|
|
|
780
864
|
tableName: z.string().describe('Table name'),
|
|
781
865
|
id: z.string().optional().describe('Record ID'),
|
|
782
866
|
filter: z.string().optional().describe('Filter as JSON string to find by'),
|
|
867
|
+
fields: z.array(z.string()).optional().describe('Fields to select. If omitted, returns only the primary key.'),
|
|
783
868
|
},
|
|
784
|
-
async ({ tableName, id, filter }) => {
|
|
869
|
+
async ({ tableName, id, filter, fields }) => {
|
|
785
870
|
validateTableName(tableName);
|
|
871
|
+
const primaryKey = await getPrimaryFieldName(tableName);
|
|
872
|
+
const selectedFields = fields && fields.length > 0 ? fields : [primaryKey];
|
|
786
873
|
if (id) {
|
|
787
874
|
// Enfyra route engine does not register GET /<table>/:id (only PATCH/DELETE use /:id). Use list + filter.
|
|
788
|
-
const filterObj = JSON.stringify({
|
|
875
|
+
const filterObj = JSON.stringify({ [primaryKey]: { _eq: id } });
|
|
876
|
+
const queryParams = new URLSearchParams({
|
|
877
|
+
filter: filterObj,
|
|
878
|
+
limit: '1',
|
|
879
|
+
fields: selectedFields.join(','),
|
|
880
|
+
});
|
|
789
881
|
const result = await fetchAPI(
|
|
790
882
|
ENFYRA_API_URL,
|
|
791
|
-
`/${tableName}
|
|
883
|
+
`/${tableName}?${queryParams.toString()}`,
|
|
792
884
|
);
|
|
793
885
|
const one = result.data?.[0] ?? null;
|
|
794
|
-
return { content: [{ type: 'text', text: JSON.stringify(
|
|
886
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
887
|
+
tableName,
|
|
888
|
+
primaryKey,
|
|
889
|
+
fields: selectedFields,
|
|
890
|
+
data: one,
|
|
891
|
+
detailHint: fields && fields.length > 0 ? undefined : 'Only the primary key was returned. Pass fields for details.',
|
|
892
|
+
}, null, 2) }] };
|
|
795
893
|
}
|
|
796
894
|
if (!filter) throw new Error('Provide id or filter');
|
|
797
895
|
validateFilter(filter);
|
|
896
|
+
const queryParams = new URLSearchParams({
|
|
897
|
+
filter,
|
|
898
|
+
limit: '1',
|
|
899
|
+
fields: selectedFields.join(','),
|
|
900
|
+
});
|
|
798
901
|
const result = await fetchAPI(
|
|
799
902
|
ENFYRA_API_URL,
|
|
800
|
-
`/${tableName}
|
|
903
|
+
`/${tableName}?${queryParams.toString()}`,
|
|
801
904
|
);
|
|
802
|
-
return { content: [{ type: 'text', text: JSON.stringify(
|
|
905
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
906
|
+
tableName,
|
|
907
|
+
fields: selectedFields,
|
|
908
|
+
data: result.data?.[0] || null,
|
|
909
|
+
detailHint: fields && fields.length > 0 ? undefined : 'Only the primary key was returned. Pass fields for details.',
|
|
910
|
+
}, null, 2) }] };
|
|
803
911
|
},
|
|
804
912
|
);
|
|
805
913
|
|
|
@@ -813,7 +921,7 @@ server.tool('create_record', 'Create a new record in any table', {
|
|
|
813
921
|
}, async ({ tableName, data }) => {
|
|
814
922
|
validateTableName(tableName);
|
|
815
923
|
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}`, { method: 'POST', body: data });
|
|
816
|
-
return { content: [{ type: 'text', text:
|
|
924
|
+
return { content: [{ type: 'text', text: JSON.stringify(summarizeMutationResult(result, 'created', tableName), null, 2) }] };
|
|
817
925
|
});
|
|
818
926
|
|
|
819
927
|
server.tool('update_record', 'Update an existing record by ID using PATCH', {
|
|
@@ -823,7 +931,7 @@ server.tool('update_record', 'Update an existing record by ID using PATCH', {
|
|
|
823
931
|
}, async ({ tableName, id, data }) => {
|
|
824
932
|
validateTableName(tableName);
|
|
825
933
|
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'PATCH', body: data });
|
|
826
|
-
return { content: [{ type: 'text', text:
|
|
934
|
+
return { content: [{ type: 'text', text: JSON.stringify(summarizeMutationResult(result, 'updated', tableName), null, 2) }] };
|
|
827
935
|
});
|
|
828
936
|
|
|
829
937
|
server.tool('delete_record', 'Delete a record by ID', {
|
|
@@ -832,7 +940,13 @@ server.tool('delete_record', 'Delete a record by ID', {
|
|
|
832
940
|
}, async ({ tableName, id }) => {
|
|
833
941
|
validateTableName(tableName);
|
|
834
942
|
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'DELETE' });
|
|
835
|
-
return { content: [{ type: 'text', text:
|
|
943
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
944
|
+
action: 'deleted',
|
|
945
|
+
tableName,
|
|
946
|
+
id,
|
|
947
|
+
statusCode: result?.statusCode,
|
|
948
|
+
success: result?.success,
|
|
949
|
+
}, null, 2) }] };
|
|
836
950
|
});
|
|
837
951
|
|
|
838
952
|
server.tool(
|
|
@@ -987,7 +1101,7 @@ function enrichRoute(route, state) {
|
|
|
987
1101
|
.map((item) => pickCodeSummary({
|
|
988
1102
|
...item,
|
|
989
1103
|
method: item.method ? { ...item.method, method: state.methodIdNameMap[String(getId(item.method))] || item.method.method || null } : item.method,
|
|
990
|
-
}, '
|
|
1104
|
+
}, 'sourceCode'));
|
|
991
1105
|
const routePreHooks = withMethodNames(
|
|
992
1106
|
state.preHooks.filter((item) => item.isGlobal || sameId(refId(item.route), routeId)),
|
|
993
1107
|
state.methodIdNameMap,
|
|
@@ -1138,7 +1252,7 @@ server.tool(
|
|
|
1138
1252
|
relations: table.relations?.map((relation) => ({ propertyName: relation.propertyName, description: relation.description })),
|
|
1139
1253
|
}));
|
|
1140
1254
|
const routeMatches = state.routes.filter((route) => matchesText(route));
|
|
1141
|
-
const handlerMatches = state.handlers.filter((handler) => matchesText(handler)).map((item) => pickCodeSummary(item, '
|
|
1255
|
+
const handlerMatches = state.handlers.filter((handler) => matchesText(handler)).map((item) => pickCodeSummary(item, 'sourceCode'));
|
|
1142
1256
|
const preHookMatches = state.preHooks.filter((hook) => matchesText(hook)).map((item) => pickCodeSummary(item, 'code'));
|
|
1143
1257
|
const postHookMatches = state.postHooks.filter((hook) => matchesText(hook)).map((item) => pickCodeSummary(item, 'code'));
|
|
1144
1258
|
const guardMatches = state.guards.filter((guard) => matchesText(guard));
|
|
@@ -1234,12 +1348,40 @@ server.tool(
|
|
|
1234
1348
|
},
|
|
1235
1349
|
);
|
|
1236
1350
|
|
|
1237
|
-
server.tool('get_all_routes', 'List
|
|
1351
|
+
server.tool('get_all_routes', 'List route definitions with minimal fields. Call inspect_route for handlers/hooks/permissions detail.', {
|
|
1238
1352
|
includeDisabled: z.boolean().optional().default(false).describe('Include disabled routes'),
|
|
1239
|
-
|
|
1353
|
+
search: z.string().optional().describe('Optional path or table substring filter. Use this before creating a route to check duplicates.'),
|
|
1354
|
+
limit: z.number().optional().describe('Maximum routes returned after search. Default 50 to keep response small.'),
|
|
1355
|
+
}, async ({ includeDisabled, search, limit }) => {
|
|
1240
1356
|
const filter = includeDisabled ? {} : { isEnabled: { _eq: true } };
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1357
|
+
const queryParams = new URLSearchParams({
|
|
1358
|
+
filter: JSON.stringify(filter),
|
|
1359
|
+
fields: 'id,path,mainTable.name,availableMethods.*,publishedMethods.*,isEnabled',
|
|
1360
|
+
limit: '1000',
|
|
1361
|
+
});
|
|
1362
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/route_definition?${queryParams.toString()}`);
|
|
1363
|
+
const routeLimit = limit || 50;
|
|
1364
|
+
const q = search ? search.toLowerCase() : null;
|
|
1365
|
+
const allRoutes = summarizeRoutes(result);
|
|
1366
|
+
const matchedRoutes = q
|
|
1367
|
+
? allRoutes.filter((route) => JSON.stringify({
|
|
1368
|
+
path: route.path,
|
|
1369
|
+
mainTable: route.mainTable,
|
|
1370
|
+
}).toLowerCase().includes(q))
|
|
1371
|
+
: allRoutes;
|
|
1372
|
+
const payload = {
|
|
1373
|
+
statusCode: result?.statusCode,
|
|
1374
|
+
success: result?.success,
|
|
1375
|
+
totalRouteCount: allRoutes.length,
|
|
1376
|
+
matchedRouteCount: matchedRoutes.length,
|
|
1377
|
+
returnedRouteCount: Math.min(matchedRoutes.length, routeLimit),
|
|
1378
|
+
search: search || null,
|
|
1379
|
+
routes: matchedRoutes.slice(0, routeLimit),
|
|
1380
|
+
detailHint: matchedRoutes.length > routeLimit
|
|
1381
|
+
? `Response truncated to ${routeLimit} routes. Re-run with search or a higher limit, then inspect_route({ path }) for details.`
|
|
1382
|
+
: 'Use inspect_route({ path }) or inspect_route({ routeId }) for handlers, hooks, permissions, and guards.',
|
|
1383
|
+
};
|
|
1384
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
1243
1385
|
});
|
|
1244
1386
|
|
|
1245
1387
|
server.tool(
|
|
@@ -1284,7 +1426,18 @@ server.tool(
|
|
|
1284
1426
|
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
1285
1427
|
|
|
1286
1428
|
const created = firstDataRecord(result);
|
|
1287
|
-
return { content: [{ type: 'text', text:
|
|
1429
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1430
|
+
action: 'created',
|
|
1431
|
+
route: {
|
|
1432
|
+
id: getId(created),
|
|
1433
|
+
path: created?.path,
|
|
1434
|
+
mainTableId,
|
|
1435
|
+
availableMethods: methods,
|
|
1436
|
+
publishedMethods: publishedMethods || [],
|
|
1437
|
+
},
|
|
1438
|
+
routesReloaded: true,
|
|
1439
|
+
next: `Use create_handler({ routeId: ${JSON.stringify(getId(created))}, method: "GET"|"POST"|"PATCH"|"DELETE", sourceCode }) for custom code.`,
|
|
1440
|
+
}, null, 2) }] };
|
|
1288
1441
|
},
|
|
1289
1442
|
);
|
|
1290
1443
|
|
|
@@ -1293,38 +1446,56 @@ server.tool(
|
|
|
1293
1446
|
[
|
|
1294
1447
|
'Create a handler for a route+method. One handler per (route, method) pair.',
|
|
1295
1448
|
'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.',
|
|
1449
|
+
'Use sourceCode, not logic/name. Enfyra compiles sourceCode into compiledCode; do not send compiledCode.',
|
|
1296
1450
|
'Handler code runs inside a sandbox with $ctx. Use macros: @BODY, @QUERY, @PARAMS, @USER, @REPOS, @HELPERS, @THROW400..@THROW503, @SOCKET, @PKGS, @LOGS, @SHARE.',
|
|
1297
1451
|
'Or use $ctx directly: $ctx.$body, $ctx.$repos.main.find(), $ctx.$helpers.$bcrypt.hash(), etc.',
|
|
1298
1452
|
'require("pkg") works for installed Server packages. console.log() writes to $share.$logs.',
|
|
1299
1453
|
].join(' '),
|
|
1300
1454
|
{
|
|
1301
1455
|
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|
|
1302
|
-
|
|
1303
|
-
.describe('
|
|
1304
|
-
|
|
1456
|
+
method: z.enum(['GET', 'POST', 'PATCH', 'DELETE']).optional()
|
|
1457
|
+
.describe('Single method to create. Prefer this for one handler.'),
|
|
1458
|
+
methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE'])).optional()
|
|
1459
|
+
.describe('Batch create multiple handlers. Use only when the same sourceCode applies to every method.'),
|
|
1460
|
+
sourceCode: z.string().describe('Handler JavaScript sourceCode. Do not use logic; backend CRUD rejects logic.'),
|
|
1461
|
+
scriptLanguage: z.enum(['javascript', 'typescript']).optional().default('javascript').describe('Script language for compiler. Default javascript.'),
|
|
1305
1462
|
timeout: z.number().optional().describe('Timeout in ms (default: system DEFAULT_HANDLER_TIMEOUT, usually 30000)'),
|
|
1306
1463
|
},
|
|
1307
|
-
async ({ routeId, methods,
|
|
1464
|
+
async ({ routeId, method, methods, sourceCode, scriptLanguage, timeout }) => {
|
|
1465
|
+
const methodNames = methods && methods.length > 0 ? methods : method ? [method] : [];
|
|
1466
|
+
if (methodNames.length === 0) throw new Error('Provide method or methods');
|
|
1308
1467
|
const methodMap = await getMethodMap();
|
|
1309
1468
|
const results = [];
|
|
1310
1469
|
|
|
1311
|
-
for (const
|
|
1312
|
-
const methodId = methodMap[
|
|
1313
|
-
if (!methodId) throw new Error(`Unknown method: ${
|
|
1470
|
+
for (const methodName of methodNames) {
|
|
1471
|
+
const methodId = methodMap[methodName.toUpperCase()];
|
|
1472
|
+
if (!methodId) throw new Error(`Unknown method: ${methodName}. Valid: ${Object.keys(methodMap).join(', ')}`);
|
|
1314
1473
|
|
|
1315
|
-
const body = { route: { id: routeId }, method: { id: methodId },
|
|
1474
|
+
const body = { route: { id: routeId }, method: { id: methodId }, sourceCode, scriptLanguage };
|
|
1316
1475
|
if (timeout) body.timeout = timeout;
|
|
1317
1476
|
|
|
1318
1477
|
const result = await fetchAPI(ENFYRA_API_URL, '/route_handler_definition', {
|
|
1319
1478
|
method: 'POST',
|
|
1320
1479
|
body: JSON.stringify(body),
|
|
1321
1480
|
});
|
|
1322
|
-
|
|
1481
|
+
const created = firstDataRecord(result);
|
|
1482
|
+
results.push({
|
|
1483
|
+
id: getId(created),
|
|
1484
|
+
routeId,
|
|
1485
|
+
method: methodName,
|
|
1486
|
+
scriptLanguage,
|
|
1487
|
+
timeout: created?.timeout ?? timeout ?? null,
|
|
1488
|
+
});
|
|
1323
1489
|
}
|
|
1324
1490
|
|
|
1325
1491
|
await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' }).catch(() => {});
|
|
1326
1492
|
|
|
1327
|
-
return { content: [{ type: 'text', text:
|
|
1493
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1494
|
+
action: 'created',
|
|
1495
|
+
handlers: results,
|
|
1496
|
+
routesReloaded: true,
|
|
1497
|
+
detailHint: 'Use inspect_route with the same routeId/path to inspect saved handlers.',
|
|
1498
|
+
}, null, 2) }] };
|
|
1328
1499
|
},
|
|
1329
1500
|
);
|
|
1330
1501
|
|