@enfyra/mcp-server 0.1.59 → 0.1.61

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.
@@ -70,6 +70,7 @@ import { validateMainTableRoutePath } from './lib/route-guards.js';
70
70
  import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
71
71
  import { startMcpUsageTelemetry } from './lib/mcp-usage-telemetry.js';
72
72
  import { startRuntimeCacheSocket } from './lib/runtime-cache-socket.js';
73
+ import { executeSequentialBatch } from './lib/sequential-batch.js';
73
74
  import { compactSourceFields, writeSourceArtifact } from './lib/source-artifacts.js';
74
75
  import { installToolsetFilter, normalizeMcpToolset, summarizeToolsetForInstructions } from './lib/toolset-filter.js';
75
76
  import { findRoutePermission, mergeMethodNames, normalizeMethodNames, resolveRoleByNameOrId, routeAvailableMethodNames, routePublicMethodNames, summarizeRouteAccess, summarizeRoutePermission, validateMethodsForRoute, } from './lib/route-permission-tools.js';
@@ -1487,11 +1488,11 @@ server.tool('find_one_record', 'Find a single record by ID or filter. By ID uses
1487
1488
  // ============================================================================
1488
1489
  // CRUD TOOLS
1489
1490
  // ============================================================================
1490
- server.tool('create_records', 'Create one or more route-backed records. Always pass records as a native JSON array; for one record, pass a one-item array. MCP preflights every item against live metadata first, then sends one POST per record sequentially; this is not a backend bulk endpoint or transaction. create_records only writes one table at a time, so when seeding related tables, follow create_tables cleanupHints.recordCreateOrder and create parent/target records before child/source records.', {
1491
+ server.tool('create_records', 'Create one or more route-backed records. Always pass records as a native JSON array; for one record, pass a one-item array. MCP preflights every item before the first POST, then writes sequentially; this is not a backend bulk endpoint or transaction. On a failed item, it returns the completed checkpoint and remaining indexes—retry only the remaining records after resolving the error.', {
1491
1492
  tableName: z.string().describe('Table name to insert into'),
1492
1493
  records: bulkObjectArrayParam(z, 'Records').describe('Records as a native JSON array. Each item must be a JSON object using metadata-backed column names and relation propertyName values.'),
1493
1494
  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.'),
1494
- 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.'),
1495
+ maxRecords: z.number().int().min(1).max(100).optional().default(20).describe('Safety cap for one MCP batch. Default is 20; explicitly raise it up to 100 only when partial-write recovery is acceptable.'),
1495
1496
  globalRulesAckKey: globalRulesAckParam(z),
1496
1497
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when any item contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1497
1498
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and any item contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
@@ -1509,19 +1510,35 @@ server.tool('create_records', 'Create one or more route-backed records. Always p
1509
1510
  extensionValidations.push(await validateExtensionCodeForGenericMutation(tableName, item.payload, item.payload?.name || item.index));
1510
1511
  }
1511
1512
  const query = parseQueryParamsArg(queryParams);
1512
- const created = [];
1513
- for (const item of prepared.records) {
1513
+ const batch = await executeSequentialBatch(prepared.records, async (item) => {
1514
1514
  const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}`, query), { method: 'POST', body: JSON.stringify(item.payload) });
1515
- created.push({
1515
+ return {
1516
1516
  index: item.index,
1517
1517
  ...summarizeMutationResult(result, 'created', tableName),
1518
- });
1518
+ };
1519
+ });
1520
+ if (batch.status === 'partial_failure') {
1521
+ return {
1522
+ isError: true,
1523
+ content: [{ type: 'text', text: JSON.stringify({
1524
+ action: 'create_records_partial_failure',
1525
+ tableName,
1526
+ requested: parsedRecords.length,
1527
+ createdCount: batch.completed.length,
1528
+ sequential: true,
1529
+ transactional: false,
1530
+ completed: batch.completed,
1531
+ failed: batch.failure,
1532
+ remainingIndexes: batch.remainingIndexes,
1533
+ retryHint: 'Resolve the failed item, then retry only the failed item and remaining indexes. Do not retry completed records unless the table has an idempotent unique key.',
1534
+ }, null, 2) }],
1535
+ };
1519
1536
  }
1520
1537
  return { content: [{ type: 'text', text: JSON.stringify({
1521
1538
  action: 'created_records',
1522
1539
  tableName,
1523
1540
  requested: parsedRecords.length,
1524
- createdCount: created.length,
1541
+ createdCount: batch.completed.length,
1525
1542
  sequential: true,
1526
1543
  transactional: false,
1527
1544
  preflight: {
@@ -1529,14 +1546,14 @@ server.tool('create_records', 'Create one or more route-backed records. Always p
1529
1546
  scriptValidatedBeforeAnyPost: prepared.records.some((item) => item.scriptValidation?.validated === true),
1530
1547
  extensionValidatedBeforeAnyPost: extensionValidations.some(Boolean),
1531
1548
  },
1532
- created,
1533
- detailHint: `Use query_table({ tableName: "${tableName}", fields: [...], limit: ${Math.min(created.length, 20)} }) to inspect created records when needed.`,
1549
+ created: batch.completed,
1550
+ detailHint: `Use query_table({ tableName: "${tableName}", fields: [...], limit: ${Math.min(batch.completed.length, 20)} }) to inspect created records when needed.`,
1534
1551
  }, null, 2) }] };
1535
1552
  });
1536
- server.tool('update_records', 'Update one or more records in one MCP call. Pass items as a native JSON array; for one update, pass one item. MCP preflights every item against live metadata and extension/script validators first, rejects duplicate ids, then PATCHes sequentially to avoid races and server overload.', {
1553
+ server.tool('update_records', 'Update one or more records in one MCP call. Pass items as a native JSON array; for one update, pass one item. MCP preflights every item, rejects duplicate ids, then PATCHes sequentially. On a failed item, it returns the completed checkpoint and remaining indexes so callers do not replay prior updates.', {
1537
1554
  tableName: z.string().describe('Table name'),
1538
1555
  items: bulkObjectArrayParam(z, 'Update items').describe('Native JSON array of update items: [{ "id": "...", "data": { ... }, "queryParams": { ... }? }]. data must use metadata-backed column names and relation propertyName values.'),
1539
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one MCP batch. Default/max is 100.'),
1556
+ maxItems: z.number().int().min(1).max(100).optional().default(20).describe('Safety cap for one MCP batch. Default is 20; explicitly raise it up to 100 only when partial-write recovery is acceptable.'),
1540
1557
  globalRulesAckKey: globalRulesAckParam(z),
1541
1558
  knowledgeAckKey: dynamicCodeKnowledgeAckParam(z).optional().describe('Required only when any item.data contains sourceCode. Use dynamicCodeAckKey from get_enfyra_required_knowledge.'),
1542
1559
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required only when tableName is enfyra_extension and any item.data contains code. Use extensionAckKey from get_enfyra_required_knowledge.'),
@@ -1559,21 +1576,36 @@ server.tool('update_records', 'Update one or more records in one MCP call. Pass
1559
1576
  preparedItems.push({ index, id: item.id, queryParams: item.queryParams, prepared });
1560
1577
  extensionValidations.push(await validateExtensionCodeForGenericMutation(tableName, prepared.payload, item.id));
1561
1578
  }
1562
- const updated = [];
1563
- for (const item of preparedItems) {
1579
+ const batch = await executeSequentialBatch(preparedItems, async (item) => {
1564
1580
  const query = parseQueryParamsArg(JSON.stringify(item.queryParams || {}));
1565
1581
  const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${encodeURIComponent(String(item.id))}`, query), { method: 'PATCH', body: JSON.stringify(item.prepared.payload) });
1566
- updated.push({
1582
+ return {
1567
1583
  index: item.index,
1568
1584
  id: item.id,
1569
1585
  ...summarizeMutationResult(result, 'updated', tableName),
1570
- });
1586
+ };
1587
+ });
1588
+ if (batch.status === 'partial_failure') {
1589
+ return {
1590
+ isError: true,
1591
+ content: [{ type: 'text', text: JSON.stringify({
1592
+ action: 'update_records_partial_failure',
1593
+ tableName,
1594
+ requested: parsedItems.length,
1595
+ updatedCount: batch.completed.length,
1596
+ sequential: true,
1597
+ completed: batch.completed,
1598
+ failed: batch.failure,
1599
+ remainingIndexes: batch.remainingIndexes,
1600
+ retryHint: 'Resolve the failed item, then retry only the failed item and remaining indexes. Do not replay completed updates unless the new value is deliberately idempotent.',
1601
+ }, null, 2) }],
1602
+ };
1571
1603
  }
1572
1604
  return { content: [{ type: 'text', text: JSON.stringify({
1573
1605
  action: 'updated_records',
1574
1606
  tableName,
1575
1607
  requested: parsedItems.length,
1576
- updatedCount: updated.length,
1608
+ updatedCount: batch.completed.length,
1577
1609
  sequential: true,
1578
1610
  duplicateIdsRejected: true,
1579
1611
  preflight: {
@@ -1581,7 +1613,7 @@ server.tool('update_records', 'Update one or more records in one MCP call. Pass
1581
1613
  scriptValidatedBeforeAnyPatch: preparedItems.some((item) => item.prepared.scriptValidation?.validated === true),
1582
1614
  extensionValidatedBeforeAnyPatch: extensionValidations.some(Boolean),
1583
1615
  },
1584
- updated,
1616
+ updated: batch.completed,
1585
1617
  }, null, 2) }] };
1586
1618
  });
1587
1619
  server.tool('get_script_source', [
@@ -2330,9 +2362,9 @@ server.tool('test_rest_endpoint', [
2330
2362
  ].join(' '), {
2331
2363
  method: z.string().optional().default('GET').describe('HTTP method name. Must exist in enfyra_method.name for Enfyra route-backed calls.'),
2332
2364
  path: z.string().describe('Enfyra API path, e.g. /enfyra_route?limit=1'),
2333
- query: z.string().optional().describe('Optional query params JSON object, merged onto path query string'),
2334
- body: z.string().optional().describe('Optional JSON request body string'),
2335
- headers: z.string().optional().describe('Optional headers JSON object'),
2365
+ query: z.string().optional().describe('Optional JSON-encoded query object string, e.g. {"limit":1,"filter":{"status":{"_eq":"ready"}}}; merged onto the path query string.'),
2366
+ body: z.string().optional().describe('Optional JSON request body string, e.g. {"title":"Example"}.'),
2367
+ headers: z.string().optional().describe('Optional JSON-encoded headers object string.'),
2336
2368
  useAuth: z.boolean().optional().default(true).describe('Attach MCP admin Bearer token. Set false to test public access.'),
2337
2369
  }, async ({ method, path, query, body, headers, useAuth }) => {
2338
2370
  const httpMethod = normalizeMethodNameInput(method || 'GET');