@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.
- package/dist/lib/dynamic-repository-builder.js +36 -6
- package/dist/lib/dynamic-repository-builder.js.map +1 -1
- package/dist/lib/runtime-zone-tools.js +15 -3
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/sequential-batch.d.ts +14 -0
- package/dist/lib/sequential-batch.js +24 -0
- package/dist/lib/sequential-batch.js.map +1 -0
- package/dist/lib/tool-routing.js +6 -6
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.js +1 -0
- package/dist/lib/toolset-filter.js.map +1 -1
- package/dist/mcp-server-entry.js +52 -20
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp-server-entry.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
2334
|
-
body: z.string().optional().describe('Optional JSON request body string'),
|
|
2335
|
-
headers: z.string().optional().describe('Optional headers
|
|
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');
|