@enfyra/mcp-server 0.1.18 → 0.1.19

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.
@@ -26,7 +26,7 @@ import { WORKFLOW_SURFACES, discoverWorkflowRoutes } from './lib/tool-routing.js
26
26
  import { getSupportedColumnTypesFromMetadata, registerTableTools } from './lib/table-tools.js';
27
27
  import { registerPlatformOperationTools, validateExtensionCode } from './lib/platform-operation-tools.js';
28
28
  import { registerRuntimeZoneTools } from './lib/runtime-zone-tools.js';
29
- import { parseRecordData, prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
29
+ import { parseRecordBatchData, parseRecordData, prepareRecordBatchMutation, prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
30
30
  import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertExtensionKnowledgeAckIf, assertGlobalRulesAck, buildRequiredKnowledgePayload, dynamicCodeKnowledgeAckParam, extensionKnowledgeAckParam, globalRulesAckParam, } from './lib/required-knowledge.js';
31
31
  import { validateMainTableRoutePath } from './lib/route-guards.js';
32
32
  import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
@@ -558,11 +558,28 @@ async function prepareGenericMutation(tableName, data) {
558
558
  data,
559
559
  });
560
560
  }
561
+ async function prepareGenericBatchMutation(tableName, records) {
562
+ const { tables } = await getMetadataTables();
563
+ return prepareRecordBatchMutation({
564
+ fetchAPI,
565
+ apiUrl: ENFYRA_API_URL,
566
+ tables,
567
+ tableName,
568
+ records,
569
+ });
570
+ }
561
571
  function assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey }) {
562
572
  const payload = parseRecordData(data);
563
573
  assertDynamicCodeKnowledgeAckIf(SCRIPT_BACKED_TABLE_SET.has(tableName) && typeof payload.sourceCode === 'string', knowledgeAckKey);
564
574
  assertExtensionKnowledgeAckIf(tableName === 'enfyra_extension' && typeof payload.code === 'string', extensionKnowledgeAckKey);
565
575
  }
576
+ function assertKnowledgeForGenericBatchMutation(tableName, records, { knowledgeAckKey, extensionKnowledgeAckKey }) {
577
+ const payloads = parseRecordBatchData(records);
578
+ for (const payload of payloads) {
579
+ assertDynamicCodeKnowledgeAckIf(SCRIPT_BACKED_TABLE_SET.has(tableName) && typeof payload.sourceCode === 'string', knowledgeAckKey);
580
+ assertExtensionKnowledgeAckIf(tableName === 'enfyra_extension' && typeof payload.code === 'string', extensionKnowledgeAckKey);
581
+ }
582
+ }
566
583
  async function validateExtensionCodeForGenericMutation(tableName, payload, fallbackName) {
567
584
  if (tableName !== 'enfyra_extension' || typeof payload?.code !== 'string')
568
585
  return null;
@@ -844,7 +861,7 @@ server.tool('discover_enfyra_system', [
844
861
  publicAccess: 'publicMethods controls anonymous REST access per route/method; otherwise Bearer JWT + routePermissions apply.',
845
862
  routeTables: sample(routeTableList),
846
863
  noRouteTables: sample(noRouteTableList),
847
- canonicalCrudTools: 'query_table/create_record/update_record/delete_record use dynamic REST routes and only work for route-backed tables.',
864
+ canonicalCrudTools: 'query_table/create_record/create_records/update_record/delete_record use dynamic REST routes and only work for route-backed tables. create_record is single-object only; create_records accepts an array, preflights every item against live metadata, then posts sequentially.',
848
865
  customRouteWorkflow: 'For a new endpoint use create_route without mainTableId, then create_handler/create_pre_hook/create_post_hook. Do not create a table just to get a path.',
849
866
  routeSamples: sample(routes, 25),
850
867
  detailHint: 'Use get_all_routes({ search, limit }) or inspect_route({ path }) for route details. Use inspect_table({ tableName }) for table detail.',
@@ -1362,9 +1379,9 @@ server.tool('find_one_record', 'Find a single record by ID or filter. By ID uses
1362
1379
  // ============================================================================
1363
1380
  // CRUD TOOLS
1364
1381
  // ============================================================================
1365
- server.tool('create_record', 'Create a new record in any route-backed table. The tool validates body keys against live metadata, validates sourceCode before saving script-backed records, and validates enfyra_extension.code before saving extension records.', {
1382
+ server.tool('create_record', 'Create exactly one record in a route-backed table. Pass a single JSON object, not an array. The tool preflights body keys against live metadata before POST, validates sourceCode before saving script-backed records, and validates enfyra_extension.code before saving extension records. For multiple records, use create_records.', {
1366
1383
  tableName: z.string().describe('Table name to insert into'),
1367
- data: z.string().describe('Record data as JSON string'),
1384
+ data: z.string().describe('Single record data as a JSON object string. Arrays are intentionally rejected; use create_records for batch seeding.'),
1368
1385
  queryParams: z.string().optional().describe('Optional query params as JSON object string, e.g. {"expired_at":"2026-09-20"}. Use for route contracts that intentionally keep workflow fields out of the validated body.'),
1369
1386
  globalRulesAckKey: globalRulesAckParam(z),
1370
1387
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
@@ -1383,6 +1400,52 @@ server.tool('create_record', 'Create a new record in any route-backed table. The
1383
1400
  extensionValidation,
1384
1401
  }, null, 2) }] };
1385
1402
  });
1403
+ server.tool('create_records', 'Create multiple records in one MCP call for route-backed table seeding. Pass a JSON array string. MCP preflights every item against live metadata first, then sends one POST per record sequentially; this is not a backend bulk endpoint or transaction.', {
1404
+ tableName: z.string().describe('Table name to insert into'),
1405
+ records: z.string().describe('Records as a JSON array string. Each item must be a JSON object using metadata-backed column names and relation propertyName values.'),
1406
+ queryParams: z.string().optional().describe('Optional query params as JSON object string applied to every POST, for route contracts that intentionally keep workflow fields out of the validated body.'),
1407
+ maxRecords: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one MCP batch. Default/max is 100. For larger imports, split intentionally.'),
1408
+ globalRulesAckKey: globalRulesAckParam(z),
1409
+ knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when any item contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1410
+ extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and any item contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
1411
+ }, async ({ tableName, records, queryParams, maxRecords, globalRulesAckKey, knowledgeAckKey, extensionKnowledgeAckKey }) => {
1412
+ assertGlobalRulesAck(globalRulesAckKey);
1413
+ validateTableName(tableName);
1414
+ const parsedRecords = parseRecordBatchData(records);
1415
+ if (parsedRecords.length > maxRecords) {
1416
+ throw new Error(`create_records received ${parsedRecords.length} records, above maxRecords=${maxRecords}. Split the batch deliberately.`);
1417
+ }
1418
+ assertKnowledgeForGenericBatchMutation(tableName, parsedRecords, { knowledgeAckKey, extensionKnowledgeAckKey });
1419
+ const prepared = await prepareGenericBatchMutation(tableName, parsedRecords);
1420
+ const extensionValidations = [];
1421
+ for (const item of prepared.records) {
1422
+ extensionValidations.push(await validateExtensionCodeForGenericMutation(tableName, item.payload, item.payload?.name || item.index));
1423
+ }
1424
+ const query = parseQueryParamsArg(queryParams);
1425
+ const created = [];
1426
+ for (const item of prepared.records) {
1427
+ const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}`, query), { method: 'POST', body: JSON.stringify(item.payload) });
1428
+ created.push({
1429
+ index: item.index,
1430
+ ...summarizeMutationResult(result, 'created', tableName),
1431
+ });
1432
+ }
1433
+ return { content: [{ type: 'text', text: JSON.stringify({
1434
+ action: 'created_records',
1435
+ tableName,
1436
+ requested: parsedRecords.length,
1437
+ createdCount: created.length,
1438
+ sequential: true,
1439
+ transactional: false,
1440
+ preflight: {
1441
+ liveMetadataFieldsValidated: true,
1442
+ scriptValidatedBeforeAnyPost: prepared.records.some((item) => item.scriptValidation?.validated === true),
1443
+ extensionValidatedBeforeAnyPost: extensionValidations.some(Boolean),
1444
+ },
1445
+ created,
1446
+ detailHint: `Use query_table({ tableName: "${tableName}", fields: [...], limit: ${Math.min(created.length, 20)} }) to inspect created records when needed.`,
1447
+ }, null, 2) }] };
1448
+ });
1386
1449
  server.tool('update_record', 'Update an existing record by ID using PATCH. The tool validates body keys against live metadata, validates sourceCode before saving script-backed records, and validates enfyra_extension.code before saving extension records. Prefer update_extension_code for normal extension edits.', {
1387
1450
  tableName: z.string().describe('Table name'),
1388
1451
  id: z.string().describe('Record ID to update'),