@enfyra/mcp-server 0.0.21 → 0.0.23

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.21",
3
+ "version": "0.0.23",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
64
64
  '',
65
65
  '### After a new table is created',
66
66
  '- 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.',
67
+ '- MCP **`create_table` supports `isSingleRecord` directly**. Set `isSingleRecord: true` in the create call for settings/config tables that should keep only one record; do not create first and then patch only for this flag.',
67
68
  '- In `create_table.relations`, each relation uses `targetTable` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
68
69
  '- **Never ask for or provide physical FK column names** when creating/updating relations. Do not include `fkCol`, `fkColumn`, `foreignKeyColumn`, `sourceColumn`, `targetColumn`, `junctionSourceColumn`, or `junctionTargetColumn` in create/update payloads unless you are only displaying existing metadata. Enfyra relation cascade derives physical FK/junction names from `propertyName` and table metadata, then hides FK columns from app form/schema definition.',
69
70
  '- For relation CRUD payloads, the public interface is the relation `propertyName`: example create body uses `"author": {"id": 1}`, not `"authorId"` or a physical FK column. Query/deep/filter keys also use relation `propertyName`.',
@@ -84,6 +85,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
84
85
  '- **Right:** `"publishedMethods": [{"id": 1}]` (publishes GET). Multiple: `[{"id": 1}, {"id": 2}]` (publishes GET + POST).',
85
86
  '- **To unset:** pass empty array `"publishedMethods": []`.',
86
87
  '',
88
+ '### Dynamic script `$repos` mutation return shape',
89
+ '- In handler/hook/flow/websocket scripts, `$ctx.$repos.<table>.create({ data })` and `$ctx.$repos.<table>.update({ id, data })` return the same collection-shaped result as `find`: `{ data: [...], count? }`.',
90
+ '- The `data` field is always an array for `create`/`update`, even when exactly one record was created or updated. If the script needs the record object, it must read `result.data[0]` or `result.data?.[0] ?? null`.',
91
+ '- Wrong: `const id = result.data.id`, `return result.data`, or assuming `create`/`update` returns the bare row object when the script needs one record.',
92
+ '- Right: `const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;`.',
93
+ '',
87
94
  '### Auth and publishedMethods (Enfyra server)',
88
95
  '- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
89
96
  '- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
@@ -94,6 +101,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
94
101
  '- **post-hooks always run** after the handler, including when the handler or a pre-hook throws — then `@ERROR` / `$ctx.$error` is set and `@DATA` is null.',
95
102
  '- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
96
103
  '',
104
+ '### Dynamic script syntax preference',
105
+ '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, and `@THROW400`–`@THROW503`.',
106
+ '- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
107
+ '- Preferred: `const result = await @REPOS.main.create({ data: @BODY });`.',
108
+ '- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
109
+ '',
97
110
  '### OAuth login (browser / frontend — not the MCP `login` tool)',
98
111
  '- **MCP `login`** uses **email + password** → `POST {base}/auth/login`. It cannot complete OAuth (no browser redirect).',
99
112
  '- **Supported providers (server):** `google`, `facebook`, `github` only.',
@@ -108,6 +108,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
108
108
  'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
109
109
  '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).',
110
110
  '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.',
111
+ 'Set `isSingleRecord: true` directly in create_table for settings/config tables that should keep only one record.',
111
112
  `Full URLs: ${apiBase}/<table_name> (example table post: ${apiBase}/post).`,
112
113
  'GraphQL is enabled separately per table through `gql_definition` or `update_table` with `graphqlEnabled`; it is not controlled by route availableMethods.',
113
114
  ].join(' '),
@@ -115,16 +116,19 @@ export function registerTableTools(server, ENFYRA_API_URL) {
115
116
  name: z.string().describe('Table name (e.g., "user_definition", "my_custom_table"). Must be unique, lowercase with underscores.'),
116
117
  alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
117
118
  description: z.string().optional().describe('Description of what this table stores.'),
119
+ isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to table_definition create.'),
118
120
  columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, defaultValue?, description?, options? }. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"status","type":"enum","options":["draft","published"]}]'),
119
121
  relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id or {"id": <id>}. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
120
122
  },
121
- async ({ name, alias, description, columns: columnsJson, relations: relationsJson }) => {
123
+ async ({ name, alias, description, isSingleRecord, columns: columnsJson, relations: relationsJson }) => {
122
124
  const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
123
125
  const userColumns = parseJsonArrayParam('columns', columnsJson);
124
126
  const userRelations = parseJsonArrayParam('relations', relationsJson).map(normalizeRelationForTablePatch);
127
+ const body = { name, alias, description, columns: [idColumn, ...userColumns], relations: userRelations };
128
+ if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
125
129
  const result = await fetchAPI(ENFYRA_API_URL, '/table_definition', {
126
130
  method: 'POST',
127
- body: JSON.stringify({ name, alias, description, columns: [idColumn, ...userColumns], relations: userRelations }),
131
+ body: JSON.stringify(body),
128
132
  });
129
133
  const createdTable = Array.isArray(result?.data) ? result.data[0] : result;
130
134
  const createdTableId = createdTable?.id ?? createdTable?._id;
@@ -349,7 +349,7 @@ server.tool(
349
349
  customRouteWorkflow: 'For a new endpoint use create_route against an existing table, then create_handler/create_pre_hook/create_post_hook. Do not create a table just to get a path.',
350
350
  },
351
351
  schemaManagement: {
352
- createTable: 'POST /table_definition supports columns and relations arrays in the same cascade call. MCP create_table exposes both.',
352
+ createTable: 'POST /table_definition supports isSingleRecord at create time and supports columns and relations arrays in the same cascade call. MCP create_table exposes isSingleRecord, columns, and relations directly.',
353
353
  updateTable: 'PATCH /table_definition/:id is the canonical path for table property changes and column/relation schema changes.',
354
354
  columns: 'column_definition has no REST route; use create_table/create_column/update_column/delete_column.',
355
355
  relations: routeTables.has('relation_definition')
@@ -552,6 +552,7 @@ server.tool(
552
552
  const payload = {
553
553
  transformer: {
554
554
  rule: 'Dynamic server scripts are transformed before sandbox execution. Macros expand to $ctx paths; comments are not transformed.',
555
+ preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use @BODY/@QUERY/@PARAMS/@USER/@REPOS/@HELPERS/@SOCKET/@TRIGGER/@DATA/@ERROR/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
555
556
  coreMacros: {
556
557
  '@BODY': '$ctx.$body',
557
558
  '@QUERY': '$ctx.$query',
@@ -620,7 +621,12 @@ server.tool(
620
621
  },
621
622
  },
622
623
  helpers: {
623
- repos: '$repos.main enforces route main table behavior; $repos.secure.<table> enforces field permissions; $repos.<table> is trusted/internal.',
624
+ repos: {
625
+ scopes: '$repos.main enforces route main table behavior; $repos.secure.<table> enforces field permissions; $repos.<table> is trusted/internal.',
626
+ mutationReturnShape: '$repos.<table>.create({ data }) and $repos.<table>.update({ id, data }) return a collection-shaped result: { data: [...], count? }. data is always an array for create/update, even for one created/updated record. If a script needs the single record object, it must read result.data[0] or result.data?.[0] ?? null.',
627
+ preferredExample: 'const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;',
628
+ wrongSingleRecordAccess: 'Do not use result.data.id, do not return result.data when one object is expected, and do not assume create/update returns the bare row object.',
629
+ },
624
630
  socketInHttpOrFlow: 'HTTP/flow context can emitToUser/emitToRoom/emitToGateway/broadcast, but cannot reply/join/leave/disconnect because there is no bound socket.',
625
631
  packages: 'Server packages installed through install_package are exposed as $ctx.$pkgs.packageName in server scripts.',
626
632
  files: 'Upload helpers are on $helpers; raw create_record on file_definition is not equivalent to multipart upload/storage rollback.',