@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.',
@@ -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 all metadata (tables, columns, relations, routes, hooks, etc.) from Enfyra', {}, async () => {
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
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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
- }, async ({ tableName }) => {
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
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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 in Enfyra with filters, sorting, and pagination', {
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 (default: 50, max: 500)'),
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
- if (limit) queryParams.set('limit', String(limit));
726
- if (fields) queryParams.set('fields', fields.join(','));
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
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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({ id: { _eq: id } });
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}?filter=${encodeURIComponent(filterObj)}&limit=1`,
883
+ `/${tableName}?${queryParams.toString()}`,
792
884
  );
793
885
  const one = result.data?.[0] ?? null;
794
- return { content: [{ type: 'text', text: JSON.stringify(one, null, 2) }] };
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}?filter=${encodeURIComponent(filter)}&limit=1`,
903
+ `/${tableName}?${queryParams.toString()}`,
801
904
  );
802
- return { content: [{ type: 'text', text: JSON.stringify(result.data?.[0] || null, null, 2) }] };
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: `Record created:\n${JSON.stringify(result, null, 2)}` }] };
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: `Record updated:\n${JSON.stringify(result, null, 2)}` }] };
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: `Record deleted:\n${JSON.stringify(result, null, 2)}` }] };
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
- }, 'logic'));
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, 'logic'));
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 all route definitions (path, mainTable, handlers, hooks, permissions). Call before create_route to avoid duplicate paths and to pick routeId for hooks/handlers.', {
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
- }, async ({ includeDisabled }) => {
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 result = await fetchAPI(ENFYRA_API_URL, `/route_definition?filter=${encodeURIComponent(JSON.stringify(filter))}&limit=500`);
1242
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
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: `Route created (ID: ${getId(created)}). Routes reloaded.\n${JSON.stringify(result, null, 2)}` }] };
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
- methods: z.array(z.enum(['GET', 'POST', 'PATCH', 'DELETE']))
1303
- .describe('Methods to create handlers for. Creates one handler per method.'),
1304
- logic: z.string().describe('Handler JavaScript code'),
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, logic, timeout }) => {
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 method of methods) {
1312
- const methodId = methodMap[method.toUpperCase()];
1313
- if (!methodId) throw new Error(`Unknown method: ${method}. Valid: ${Object.keys(methodMap).join(', ')}`);
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 }, logic };
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
- results.push(result);
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: `Handler(s) created for [${methods.join(', ')}]. Routes reloaded.\n${JSON.stringify(results, null, 2)}` }] };
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