@enfyra/mcp-server 0.0.56 → 0.0.58

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.56",
3
+ "version": "0.0.58",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -203,10 +203,21 @@ create_column({
203
203
  defaultValue: "pending",
204
204
  isPublished: true,
205
205
  description: "Email verification state controlled by server hooks."
206
+ })
207
+
208
+ create_column({
209
+ tableId: "<project_env_table_id>",
210
+ name: "value",
211
+ type: "text",
212
+ isNullable: false,
213
+ isPublished: false,
214
+ isEncrypted: true,
215
+ description: "Encrypted environment value."
206
216
  })`,
207
217
  notes: [
208
218
  'Run schema-changing calls sequentially. Do not parallelize create_column calls.',
209
219
  'create_column fetches table_definition and patches only real persisted columns with id/_id; generated metadata projections such as createdAt, updatedAt, or relation FK display fields are skipped.',
220
+ 'Use isEncrypted=true for encryption at rest. Add isUpdatable=false separately only when the field should be immutable.',
210
221
  'Use hooks or field permissions to prevent clients from updating server-owned fields.',
211
222
  ],
212
223
  },
@@ -361,7 +372,7 @@ if (value && value.slice(0, 7) !== "enc:v1:") {
361
372
  'Do not call raw create_record with a code field for pre_hook_definition or post_hook_definition; backend CRUD rejects code.',
362
373
  'Use Enfyra pre-hooks for request-body normalization before canonical CRUD persists the record.',
363
374
  'Do not implement encrypted field normalization as a Knex/database hook.',
364
- 'Use $encrypt for encryption and $ssh.generateKeyPair for SSH key generation; do not use $secrets.',
375
+ 'Use isEncrypted columns for database encryption and $helpers.$crypto.generateSshKeyPair for SSH key generation; do not use $helpers.$ssh or $helpers.$secrets.',
365
376
  ],
366
377
  },
367
378
  {
@@ -139,8 +139,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
139
139
  '### Dynamic script syntax preference',
140
140
  '- 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`.',
141
141
  '- Use Enfyra native throw helpers for intentional errors: `@THROW400("message")`, `@THROW403()`, `@THROW404("resource", id)`, or `$ctx.$throw[400]("message")`. Do not generate `throw new Error(...)` for user/domain errors in handlers, hooks, flows, websocket events, OAuth scripts, or admin-generated scripts.',
142
- '- 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);`.',
143
- '- ASV exposes `$helpers.$encrypt.encrypt/decrypt` for encrypted strings and `$helpers.$ssh.generateKeyPair` for SSH keys. Do not generate `$helpers.$secrets` usage.',
142
+ '- For regular app data that must be encrypted at rest, create the column with `isEncrypted=true`; Enfyra database-query hooks will encrypt on insert/update and decrypt after select. `isEncrypted` does not imply immutability; use `isUpdatable=false` separately only when the field itself must be immutable. Do not filter or sort on encrypted fields. Legacy Cloud control-plane fields named `*_encrypted` may still use route pre-hooks until migrated.',
143
+ '- Enfyra scripts use `$helpers.$crypto` for bounded crypto helpers such as `randomUUID()`, `randomBytes(size, encoding)`, `sha256(value, encoding)`, `hmacSha256(value, secret, encoding)`, and `generateSshKeyPair(comment)`. Do not generate legacy `$helpers.$ssh` or `$helpers.$secrets` usage.',
144
144
  '- 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.',
145
145
  '- 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.',
146
146
  '- 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.',
@@ -205,7 +205,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
205
205
  '- To check which tables have canonical CRUD routes, call `get_all_routes` and look for `mainTable`. Custom routes intentionally have no `mainTable`; inspect their handlers/hooks to see which repos they touch.',
206
206
  '- **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`.',
207
207
  '- **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 by reading full table metadata first.',
208
- '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isPublished=false` directly when creating secret/internal fields such as `*_encrypted`. When patching an existing table, only persisted columns with an `id`/`_id` belong in the cascade payload; metadata projections such as `createdAt`, `updatedAt`, or relation-derived FK display fields without an id are not valid column-definition patch rows. Never rebuild a schema cascade from `table_definition?fields=columns.*`, because nested relation fields may be paginated/truncated.',
208
+ '- Use `create_column`/`add_column` for new scalar fields. These tools accept column metadata such as `isNullable`, `isUnique`, `isPublished`, `isUpdatable`, `isEncrypted`, `isPrimary`, `isGenerated`, `isSystem`, `defaultValue`, `description`, and `options`; set `isUpdatable=false` for immutable fields and set `isPublished=false` directly when creating secret/internal fields. `isEncrypted=true` encrypts stored values at rest but does not change `isUpdatable`; encrypted fields must not be used for filter/sort. When patching an existing table, only persisted columns with an `id`/`_id` belong in the cascade payload; metadata projections such as `createdAt`, `updatedAt`, or relation-derived FK display fields without an id are not valid column-definition patch rows. Never rebuild a schema cascade from `table_definition?fields=columns.*`, because nested relation fields may be paginated/truncated.',
209
209
  '- Prefer `create_relation`/`add_relation` and `delete_relation`/`remove_relation` for relation schema changes because they preserve the full table relation list, serialize schema mutations, verify unrelated relation ids survived, and handle schema-confirm retry. Direct `create_record` on `relation_definition` only edits metadata and is not the canonical schema migration path; generic record mutations also reject physical FK/junction fields on `relation_definition`.',
210
210
  '- Destructive schema tools and generic `delete_record` return a preview unless `confirm=true` is passed. This applies to `delete_record`, `delete_table`, `delete_column`/`remove_column`, and `delete_relation`/`remove_relation`; do not add `confirm=true` until the user has explicitly approved the destructive operation.',
211
211
  '',
@@ -208,12 +208,14 @@ async function verifyRelationCascade(ENFYRA_API_URL, tableId, beforeIds, {
208
208
  return afterRelations;
209
209
  }
210
210
 
211
- function buildColumnDefinition({
211
+ export function buildColumnDefinition({
212
212
  name,
213
213
  type,
214
214
  isNullable,
215
215
  isUnique,
216
216
  isPublished,
217
+ isUpdatable,
218
+ isEncrypted,
217
219
  isPrimary,
218
220
  isGenerated,
219
221
  isSystem,
@@ -225,6 +227,8 @@ function buildColumnDefinition({
225
227
  if (isNullable !== undefined) column.isNullable = isNullable;
226
228
  if (isUnique !== undefined) column.isUnique = isUnique;
227
229
  if (isPublished !== undefined) column.isPublished = isPublished;
230
+ if (isUpdatable !== undefined) column.isUpdatable = isUpdatable;
231
+ if (isEncrypted !== undefined) column.isEncrypted = isEncrypted;
228
232
  if (isPrimary !== undefined) column.isPrimary = isPrimary;
229
233
  if (isGenerated !== undefined) column.isGenerated = isGenerated;
230
234
  if (isSystem !== undefined) column.isSystem = isSystem;
@@ -378,6 +382,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
378
382
  isNullable: z.boolean().optional().default(true).describe('Set to false if column cannot be null.'),
379
383
  isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
380
384
  isPublished: z.boolean().optional().describe('Set column visibility baseline. Use false for secrets and internal fields.'),
385
+ isUpdatable: z.boolean().optional().describe('Set false for immutable fields that cannot be updated after creation. Independent from isEncrypted.'),
386
+ isEncrypted: z.boolean().optional().describe('Set true to encrypt this column at the Enfyra database-query layer. This does not change isUpdatable. Encrypted fields cannot be filtered or sorted.'),
381
387
  isPrimary: z.boolean().optional().describe('Set true only for primary key columns; normally only create_table auto id uses this.'),
382
388
  isGenerated: z.boolean().optional().describe('Set true only for generated columns such as auto id.'),
383
389
  isSystem: z.boolean().optional().describe('Set true only for system-managed columns. Avoid for normal app fields.'),
@@ -448,7 +454,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
448
454
  name: z.string().describe('Table name (e.g., "user_definition", "my_custom_table"). Must be unique, lowercase with underscores.'),
449
455
  description: z.string().optional().describe('Description of what this table stores.'),
450
456
  isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to table_definition create.'),
451
- 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"]}]'),
457
+ columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, defaultValue?, description?, options? }. Set isEncrypted=true for values encrypted at rest; set isUpdatable=false separately only when the field should be immutable. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"api_key","type":"varchar","isEncrypted":true,"isPublished":false}]'),
452
458
  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"}]'),
453
459
  indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
454
460
  uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
@@ -611,11 +617,12 @@ export function registerTableTools(server, ENFYRA_API_URL) {
611
617
  type: z.string().optional().describe('New column type.'),
612
618
  isNullable: z.boolean().optional().describe('Set nullable.'),
613
619
  isPublished: z.boolean().optional().describe('Set column visibility baseline. false = unpublished (omitted from response unless allowed by field permission rules).'),
620
+ isUpdatable: z.boolean().optional().describe('Set false for immutable fields that should be stripped from update payloads.'),
614
621
  defaultValue: z.string().optional().describe('New default value as JSON string.'),
615
622
  description: z.string().optional().describe('New description.'),
616
623
  options: z.string().optional().describe('New options as JSON string.'),
617
624
  },
618
- async ({ tableId, columnId, name, type, isNullable, isPublished, defaultValue, description, options }) => withSchemaQueue(async () => {
625
+ async ({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options }) => withSchemaQueue(async () => {
619
626
  const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
620
627
  if (!tableData) {
621
628
  return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
@@ -634,6 +641,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
634
641
  if (type !== undefined) rest.type = type;
635
642
  if (isNullable !== undefined) rest.isNullable = isNullable;
636
643
  if (isPublished !== undefined) rest.isPublished = isPublished;
644
+ if (isUpdatable !== undefined) rest.isUpdatable = isUpdatable;
637
645
  if (defaultValue !== undefined) rest.defaultValue = defaultValue;
638
646
  if (description !== undefined) rest.description = description;
639
647
  if (options !== undefined) rest.options = JSON.parse(options);
@@ -175,6 +175,8 @@ function summarizeTable(table) {
175
175
  isPrimary: !!column.isPrimary,
176
176
  isNullable: column.isNullable,
177
177
  isPublished: column.isPublished,
178
+ isUpdatable: column.isUpdatable !== false,
179
+ isEncrypted: column.isEncrypted === true,
178
180
  })),
179
181
  relations: (table.relations || []).map((relation) => ({
180
182
  id: relation.id ?? relation._id,
@@ -702,6 +704,9 @@ server.tool(
702
704
  keyRule: 'Do not include NODE_NAME, user_cache:, or Redis namespace prefixes in scripts. Prefer TTL-based set(key, value, ttlMs); setNoExpire may still be evicted by the user-cache soft allocation.',
703
705
  },
704
706
  throws: '@THROW400 through @THROW503 and @THROW map to $ctx.$throw helpers.',
707
+ helpers: {
708
+ crypto: '$ctx.$helpers.$crypto exposes bounded runtime crypto helpers: randomUUID(), randomBytes(size, encoding), sha256(value, encoding), hmacSha256(value, secret, encoding), and generateSshKeyPair(comment). Use generateSshKeyPair for Cloud host SSH keys. Do not use legacy $ctx.$helpers.$ssh.',
709
+ },
705
710
  },
706
711
  contexts: {
707
712
  preHook: {