@enfyra/mcp-server 0.1.11 → 0.1.13
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/README.md +1 -1
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +1 -0
- package/src/lib/mcp-instructions.js +2 -1
- package/src/lib/platform-operation-tools.js +154 -2
- package/src/lib/required-knowledge.js +1 -0
- package/src/lib/tool-routing.js +3 -3
- package/src/mcp-server-entry.mjs +12 -3
package/README.md
CHANGED
|
@@ -258,7 +258,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
258
258
|
- Write tools require `get_enfyra_required_knowledge` acknowledgement before mutating Enfyra state. Discovery, validation, and preview tools remain available without the acknowledgement so agents can read and plan first. If the acknowledgement is missing, the tool error tells the caller to read `get_enfyra_required_knowledge` and pass the required key.
|
|
259
259
|
- Script-backed records validate `sourceCode` through `/admin/script/validate` before saving.
|
|
260
260
|
- `validate_dynamic_script` checks handler, hook, flow, websocket, GraphQL, and bootstrap script source without saving.
|
|
261
|
-
- `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
261
|
+
- `validate_extension_code` locally rejects common extension component-resolution mistakes, such as `resolveComponent()` or lowercase auto-injected component tags like `<ubutton>`, then checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
262
262
|
- Dynamic script guidance distinguishes secure repositories (`@REPOS.main`, `@REPOS.secure.<table>`) from trusted internal repositories (`@REPOS.<table>`), and tells agents not to return raw trusted records to users.
|
|
263
263
|
- `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
|
|
264
264
|
- Long source/code values in read responses are written to `/tmp/enfyra-mcp-sources` and returned as length/hash/preview/tmpFile metadata so LLM callers can inspect full source from the file path without truncating tool output.
|
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -1597,6 +1597,7 @@ ensure_page_extension({
|
|
|
1597
1597
|
'Use enfyra_menu.label, not title.',
|
|
1598
1598
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1599
1599
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1600
|
+
'When editing an existing extension by id or name, use update_extension_code so local guards plus /enfyra_extension/preview and the save happen in one atomic call. Do not spend a second LLM step on validate_extension_code followed by update_record unless the user requested validation-only output.',
|
|
1600
1601
|
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI; that tool is the authority for theme, color, layout, modal, drawer, and shell registry details.',
|
|
1601
1602
|
'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
|
|
1602
1603
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
@@ -33,7 +33,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
33
33
|
'- Before mutating metadata, schema, routes, permissions, menus, packages, cache state, dynamic code, or extension UI, call `get_enfyra_required_knowledge`, read the global rules, and pass `globalRulesAckKey` into write tools. Dynamic server code also requires `dynamicCodeAckKey`; extension code also requires `extensionAckKey`.',
|
|
34
34
|
'- With non-root API tokens, call `get_permission_profile` before relying on admin helper tools or when debugging 403s. MCP admin helpers require ordinary route permissions for static admin routes such as `/admin/script/validate`, `/admin/test/run`, `/admin/flow/trigger/:id`, and `/admin/reload/*`.',
|
|
35
35
|
'- Prefer the most specific business operation tool over raw metadata CRUD. `discover_enfyra_workflows` provides the current operation-tool map and negative-routing avoidTools.',
|
|
36
|
-
'- Before saving standalone dynamic script
|
|
36
|
+
'- Before saving standalone dynamic script code, call `validate_dynamic_script` unless the chosen write tool already validates the code. For extension edits, prefer `update_extension_code`, `extension_workflow`, or `ensure_*_extension`; these validate and save atomically. Use `validate_extension_code` only for validation-only checks.',
|
|
37
|
+
'- Extension SFCs must use auto-injected components directly in templates, such as `<UButton>`, and must not call `resolveComponent()` for Nuxt UI/eApp components.',
|
|
37
38
|
'- For existing script-backed records, use `trace_metadata_usage` then `get_script_source`; edit with `patch_script_source` or `update_script_source` so source is hash-checked and validated.',
|
|
38
39
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
39
40
|
'',
|
|
@@ -12,6 +12,36 @@ import {
|
|
|
12
12
|
globalRulesAckParam,
|
|
13
13
|
} from './required-knowledge.js';
|
|
14
14
|
|
|
15
|
+
const AUTO_INJECTED_EXTENSION_COMPONENT_TAGS = [
|
|
16
|
+
'CommonDrawer',
|
|
17
|
+
'CommonModal',
|
|
18
|
+
'EmptyState',
|
|
19
|
+
'FormEditor',
|
|
20
|
+
'FormEditorLazy',
|
|
21
|
+
'NuxtLink',
|
|
22
|
+
'PermissionGate',
|
|
23
|
+
'UBadge',
|
|
24
|
+
'UButton',
|
|
25
|
+
'UCheckbox',
|
|
26
|
+
'UDropdownMenu',
|
|
27
|
+
'UForm',
|
|
28
|
+
'UFormField',
|
|
29
|
+
'UIcon',
|
|
30
|
+
'UInput',
|
|
31
|
+
'UModal',
|
|
32
|
+
'USelect',
|
|
33
|
+
'USelectMenu',
|
|
34
|
+
'USkeleton',
|
|
35
|
+
'USwitch',
|
|
36
|
+
'UTabs',
|
|
37
|
+
'UTextarea',
|
|
38
|
+
'UTooltip',
|
|
39
|
+
'Widget',
|
|
40
|
+
];
|
|
41
|
+
const AUTO_INJECTED_EXTENSION_COMPONENT_BY_LOWERCASE = new Map(
|
|
42
|
+
AUTO_INJECTED_EXTENSION_COMPONENT_TAGS.map((tag) => [tag.toLowerCase(), tag]),
|
|
43
|
+
);
|
|
44
|
+
|
|
15
45
|
function unwrapData(result) {
|
|
16
46
|
return Array.isArray(result?.data) ? result.data : [];
|
|
17
47
|
}
|
|
@@ -360,6 +390,7 @@ function getExtensionThemeContract() {
|
|
|
360
390
|
],
|
|
361
391
|
components: [
|
|
362
392
|
'Use Nuxt UI/eApp components for normal controls: UButton, UInput, UTextarea, USelectMenu/USelect, USwitch, UCheckbox, UTabs, UBadge, UModal, and CommonDrawer when available.',
|
|
393
|
+
'Use auto-injected components directly in the template with PascalCase names. Do not call resolveComponent() to manually resolve Nuxt UI/eApp components inside extension SFCs; it can compile but render unresolved lowercase DOM tags such as <ubutton>.',
|
|
363
394
|
'Buttons should have stable geometry: hover may change color, border, or shadow but must not move the button or resize its content. Disabled buttons keep disabled cursor/visual state.',
|
|
364
395
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
365
396
|
'Dynamic extensions resolve UModal to the app CommonModal. Do not pass ui.content: "eapp-surface-card" or "surface-card" to UModal/CommonModal; modal content uses the app modal surface and caller ui.content should only append z-index, width, or max-width classes.',
|
|
@@ -541,7 +572,66 @@ async function validateDynamicScript(apiUrl, sourceCode, scriptLanguage = 'javas
|
|
|
541
572
|
};
|
|
542
573
|
}
|
|
543
574
|
|
|
544
|
-
|
|
575
|
+
function readTemplateBlocks(code) {
|
|
576
|
+
const blocks = [];
|
|
577
|
+
const lower = String(code || '').toLowerCase();
|
|
578
|
+
let index = 0;
|
|
579
|
+
while (index < lower.length) {
|
|
580
|
+
const openStart = lower.indexOf('<template', index);
|
|
581
|
+
if (openStart === -1) break;
|
|
582
|
+
const boundary = lower[openStart + '<template'.length];
|
|
583
|
+
if (boundary && !/\s|>/.test(boundary)) {
|
|
584
|
+
index = openStart + 1;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const openEnd = lower.indexOf('>', openStart + '<template'.length);
|
|
588
|
+
if (openEnd === -1) break;
|
|
589
|
+
const closeStart = lower.indexOf('</template', openEnd + 1);
|
|
590
|
+
if (closeStart === -1) break;
|
|
591
|
+
blocks.push(String(code).slice(openEnd + 1, closeStart));
|
|
592
|
+
index = closeStart + '</template'.length;
|
|
593
|
+
}
|
|
594
|
+
return blocks;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function readTemplateTagName(template, start) {
|
|
598
|
+
const next = template[start + 1];
|
|
599
|
+
if (!next || next === '!' || next === '?') return null;
|
|
600
|
+
let index = start + (next === '/' ? 2 : 1);
|
|
601
|
+
while (/\s/.test(template[index] || '')) index += 1;
|
|
602
|
+
const nameStart = index;
|
|
603
|
+
while (/[\w.-]/.test(template[index] || '')) index += 1;
|
|
604
|
+
return index > nameStart ? template.slice(nameStart, index) : null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function validateExtensionCodeLocally(code) {
|
|
608
|
+
if (/\bresolveComponent\s*\(/.test(String(code || ''))) {
|
|
609
|
+
throw new Error('Invalid extension component resolution: do not call resolveComponent() in Enfyra extensions. Use auto-injected components such as <UButton> directly in the template so the app/compiler resolves them correctly.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const violations = [];
|
|
613
|
+
for (const template of readTemplateBlocks(code)) {
|
|
614
|
+
let index = 0;
|
|
615
|
+
while (index < template.length) {
|
|
616
|
+
const tagStart = template.indexOf('<', index);
|
|
617
|
+
if (tagStart === -1) break;
|
|
618
|
+
const tagName = readTemplateTagName(template, tagStart);
|
|
619
|
+
if (tagName && tagName === tagName.toLowerCase() && !tagName.includes('-')) {
|
|
620
|
+
const expected = AUTO_INJECTED_EXTENSION_COMPONENT_BY_LOWERCASE.get(tagName);
|
|
621
|
+
if (expected) violations.push({ tag: tagName, expected });
|
|
622
|
+
}
|
|
623
|
+
index = tagStart + 1;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (violations.length) {
|
|
627
|
+
const first = violations[0];
|
|
628
|
+
throw new Error(`Invalid extension component casing: use <${first.expected}> instead of <${first.tag}>. Enfyra/Nuxt UI auto-injected components must keep PascalCase in extension templates; lowercase tags render as unresolved DOM elements.`);
|
|
629
|
+
}
|
|
630
|
+
return { componentCasing: 'passed' };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function validateExtensionCode(apiUrl, code, name) {
|
|
634
|
+
const localChecks = validateExtensionCodeLocally(code);
|
|
545
635
|
const result = await fetchAPI(apiUrl, '/enfyra_extension/preview', {
|
|
546
636
|
method: 'POST',
|
|
547
637
|
body: JSON.stringify({ code, name }),
|
|
@@ -551,11 +641,51 @@ async function validateExtensionCode(apiUrl, code, name) {
|
|
|
551
641
|
}
|
|
552
642
|
return {
|
|
553
643
|
valid: true,
|
|
644
|
+
localChecks,
|
|
554
645
|
extensionId: result?.extensionId || name || null,
|
|
555
646
|
compiledLength: typeof result?.compiledCode === 'string' ? result.compiledCode.length : undefined,
|
|
556
647
|
};
|
|
557
648
|
}
|
|
558
649
|
|
|
650
|
+
async function updateExtensionCode(apiUrl, {
|
|
651
|
+
id,
|
|
652
|
+
name,
|
|
653
|
+
code,
|
|
654
|
+
description,
|
|
655
|
+
isEnabled,
|
|
656
|
+
version,
|
|
657
|
+
globalRulesAckKey,
|
|
658
|
+
extensionKnowledgeAckKey,
|
|
659
|
+
}) {
|
|
660
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
661
|
+
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|
|
662
|
+
if (!id && !name) throw new Error('Provide id or name to update an existing extension.');
|
|
663
|
+
const existing = id
|
|
664
|
+
? await findRecord(apiUrl, 'enfyra_extension', { id: { _eq: id } }, 'id,_id,name,type,menu.id')
|
|
665
|
+
: await findRecord(apiUrl, 'enfyra_extension', { name: { _eq: name } }, 'id,_id,name,type,menu.id');
|
|
666
|
+
if (!existing) throw new Error(`Extension not found: ${id || name}`);
|
|
667
|
+
const extensionId = getId(existing);
|
|
668
|
+
const validation = await validateExtensionCode(apiUrl, code, name || existing.name || extensionId);
|
|
669
|
+
const body = {
|
|
670
|
+
code,
|
|
671
|
+
...(description !== undefined ? { description } : {}),
|
|
672
|
+
...(isEnabled !== undefined ? { isEnabled } : {}),
|
|
673
|
+
...(version !== undefined ? { version } : {}),
|
|
674
|
+
};
|
|
675
|
+
const result = await fetchAPI(apiUrl, `/enfyra_extension/${encodeURIComponent(String(extensionId))}`, {
|
|
676
|
+
method: 'PATCH',
|
|
677
|
+
body: JSON.stringify(body),
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
action: 'extension_code_updated',
|
|
681
|
+
id: extensionId,
|
|
682
|
+
name: existing.name || name || null,
|
|
683
|
+
type: existing.type || null,
|
|
684
|
+
result,
|
|
685
|
+
validation,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
559
689
|
function normalizeMetadataTables(metadata) {
|
|
560
690
|
const tables = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
|
|
561
691
|
return Array.isArray(tables) ? tables : Object.values(tables || {});
|
|
@@ -1436,7 +1566,8 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1436
1566
|
'validate_extension_code',
|
|
1437
1567
|
[
|
|
1438
1568
|
'Validate Enfyra admin extension code before saving it to enfyra_extension.',
|
|
1439
|
-
'Use this
|
|
1569
|
+
'Use this only when the user explicitly wants a validation-only check. For normal edits, use update_extension_code or ensure_*_extension so successful validation saves in the same tool call.',
|
|
1570
|
+
'This calls /enfyra_extension/preview and does not save anything.',
|
|
1440
1571
|
'Call get_extension_theme_contract first when generating or reviewing UI.',
|
|
1441
1572
|
].join(' '),
|
|
1442
1573
|
{
|
|
@@ -1449,6 +1580,27 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1449
1580
|
}),
|
|
1450
1581
|
);
|
|
1451
1582
|
|
|
1583
|
+
server.tool(
|
|
1584
|
+
'update_extension_code',
|
|
1585
|
+
[
|
|
1586
|
+
'Business operation: update an existing Enfyra admin extension code by id or name.',
|
|
1587
|
+
'It runs local extension guards and /enfyra_extension/preview first, then saves the code in the same call only when validation succeeds.',
|
|
1588
|
+
'Use this instead of validate_extension_code followed by update_record when editing an existing page/widget/global extension.',
|
|
1589
|
+
'Call get_extension_theme_contract first when generating or reviewing UI.',
|
|
1590
|
+
].join(' '),
|
|
1591
|
+
{
|
|
1592
|
+
id: z.union([z.string(), z.number()]).optional().describe('Existing extension id. Provide id or name.'),
|
|
1593
|
+
name: z.string().optional().describe('Existing extension unique name. Provide id or name.'),
|
|
1594
|
+
code: z.string().describe('Vue SFC extension code.'),
|
|
1595
|
+
description: z.string().optional().describe('Optional replacement extension description. Omit to preserve.'),
|
|
1596
|
+
isEnabled: z.boolean().optional().describe('Optional enabled state. Omit to preserve.'),
|
|
1597
|
+
version: z.string().optional().describe('Optional extension version. Omit to preserve.'),
|
|
1598
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
1599
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
1600
|
+
},
|
|
1601
|
+
async (input) => jsonText(await updateExtensionCode(ENFYRA_API_URL, input)),
|
|
1602
|
+
);
|
|
1603
|
+
|
|
1452
1604
|
server.tool(
|
|
1453
1605
|
'get_extension_theme_contract',
|
|
1454
1606
|
'Return the concise Enfyra admin extension UI/theme/security contract. Call before writing or reviewing extension UI.',
|
|
@@ -175,6 +175,7 @@ export function buildRequiredKnowledgePayload() {
|
|
|
175
175
|
id: 'extension-runtime-contract',
|
|
176
176
|
rules: [
|
|
177
177
|
'Save extensions as enfyra_extension Vue SFC records; no static import statements in extension code.',
|
|
178
|
+
'Do not call resolveComponent() in extension SFCs. Use auto-injected components such as <UButton>, <UBadge>, <PermissionGate>, and <Widget> directly in the template so the app/compiler resolves them correctly.',
|
|
178
179
|
'Load app packages with getPackages(["package-name"]) inside extension runtime code.',
|
|
179
180
|
'Prefer FormEditor/FormEditorLazy for direct table-backed forms when the form maps to metadata fields.',
|
|
180
181
|
'For long admin setup workflows, open CommonDrawer immediately and show loading/error/content inside it.',
|
package/src/lib/tool-routing.js
CHANGED
|
@@ -67,14 +67,14 @@ export const TOOL_WORKFLOWS = [
|
|
|
67
67
|
firstTools: ['get_enfyra_required_knowledge', 'get_extension_theme_contract', 'inspect_feature'],
|
|
68
68
|
inspectTools: ['inspect_feature', 'trace_metadata_usage', 'get_script_source'],
|
|
69
69
|
knowledgeTools: ['get_enfyra_required_knowledge', 'get_extension_theme_contract', 'get_theme_class_reference'],
|
|
70
|
-
writeTools: ['extension_workflow', 'ensure_menu', 'reorder_menus', 'ensure_page_extension', 'ensure_global_extension', 'ensure_widget_extension'],
|
|
70
|
+
writeTools: ['extension_workflow', 'ensure_menu', 'reorder_menus', 'update_extension_code', 'ensure_page_extension', 'ensure_global_extension', 'ensure_widget_extension'],
|
|
71
71
|
verifyTools: ['validate_extension_code', 'inspect_feature'],
|
|
72
72
|
avoidTools: [
|
|
73
73
|
{
|
|
74
74
|
tool: 'create_record/update_record on enfyra_extension',
|
|
75
75
|
when: 'creating or changing extension code',
|
|
76
|
-
useInstead: '
|
|
77
|
-
reason: '
|
|
76
|
+
useInstead: 'update_extension_code for an existing extension id/name, or extension_workflow/ensure_*_extension for create/wire flows',
|
|
77
|
+
reason: 'Extension operation tools validate local guards plus /enfyra_extension/preview and save only after validation succeeds.',
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
tool: 'query_table on destination domain lists',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instruct
|
|
|
22
22
|
import { getExamples, listExampleCategories } from './lib/mcp-examples.js';
|
|
23
23
|
import { WORKFLOW_SURFACES, discoverWorkflowRoutes } from './lib/tool-routing.js';
|
|
24
24
|
import { registerTableTools } from './lib/table-tools.js';
|
|
25
|
-
import { registerPlatformOperationTools } from './lib/platform-operation-tools.js';
|
|
25
|
+
import { registerPlatformOperationTools, validateExtensionCode } from './lib/platform-operation-tools.js';
|
|
26
26
|
import { parseRecordData, prepareRecordMutation, validateScriptSourceIfPresent } from './lib/mutation-guards.js';
|
|
27
27
|
import {
|
|
28
28
|
assertDynamicCodeKnowledgeAck,
|
|
@@ -623,6 +623,11 @@ function assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, e
|
|
|
623
623
|
assertExtensionKnowledgeAckIf(tableName === 'enfyra_extension' && typeof payload.code === 'string', extensionKnowledgeAckKey);
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
async function validateExtensionCodeForGenericMutation(tableName, payload, fallbackName) {
|
|
627
|
+
if (tableName !== 'enfyra_extension' || typeof payload?.code !== 'string') return null;
|
|
628
|
+
return validateExtensionCode(ENFYRA_API_URL, payload.code, payload.name || fallbackName);
|
|
629
|
+
}
|
|
630
|
+
|
|
626
631
|
function parseQueryParamsArg(queryParams) {
|
|
627
632
|
const parsed = parseJsonArg(queryParams, {});
|
|
628
633
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
@@ -1501,7 +1506,7 @@ server.tool(
|
|
|
1501
1506
|
// CRUD TOOLS
|
|
1502
1507
|
// ============================================================================
|
|
1503
1508
|
|
|
1504
|
-
server.tool('create_record', 'Create a new record in any route-backed table. The tool validates body keys against live metadata
|
|
1509
|
+
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.', {
|
|
1505
1510
|
tableName: z.string().describe('Table name to insert into'),
|
|
1506
1511
|
data: z.string().describe('Record data as JSON string'),
|
|
1507
1512
|
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.'),
|
|
@@ -1513,15 +1518,17 @@ server.tool('create_record', 'Create a new record in any route-backed table. The
|
|
|
1513
1518
|
validateTableName(tableName);
|
|
1514
1519
|
assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
|
|
1515
1520
|
const prepared = await prepareGenericMutation(tableName, data);
|
|
1521
|
+
const extensionValidation = await validateExtensionCodeForGenericMutation(tableName, prepared.payload, prepared.payload?.name);
|
|
1516
1522
|
const query = parseQueryParamsArg(queryParams);
|
|
1517
1523
|
const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}`, query), { method: 'POST', body: JSON.stringify(prepared.payload) });
|
|
1518
1524
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1519
1525
|
...summarizeMutationResult(result, 'created', tableName),
|
|
1520
1526
|
scriptValidation: prepared.scriptValidation,
|
|
1527
|
+
extensionValidation,
|
|
1521
1528
|
}, null, 2) }] };
|
|
1522
1529
|
});
|
|
1523
1530
|
|
|
1524
|
-
server.tool('update_record', 'Update an existing record by ID using PATCH. The tool validates body keys against live metadata
|
|
1531
|
+
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.', {
|
|
1525
1532
|
tableName: z.string().describe('Table name'),
|
|
1526
1533
|
id: z.string().describe('Record ID to update'),
|
|
1527
1534
|
data: z.string().describe('Fields to update as JSON string'),
|
|
@@ -1534,11 +1541,13 @@ server.tool('update_record', 'Update an existing record by ID using PATCH. The t
|
|
|
1534
1541
|
validateTableName(tableName);
|
|
1535
1542
|
assertKnowledgeForGenericMutation(tableName, data, { knowledgeAckKey, extensionKnowledgeAckKey });
|
|
1536
1543
|
const prepared = await prepareGenericMutation(tableName, data);
|
|
1544
|
+
const extensionValidation = await validateExtensionCodeForGenericMutation(tableName, prepared.payload, id);
|
|
1537
1545
|
const query = parseQueryParamsArg(queryParams);
|
|
1538
1546
|
const result = await fetchAPI(ENFYRA_API_URL, appendQuery(`/${tableName}/${id}`, query), { method: 'PATCH', body: JSON.stringify(prepared.payload) });
|
|
1539
1547
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1540
1548
|
...summarizeMutationResult(result, 'updated', tableName),
|
|
1541
1549
|
scriptValidation: prepared.scriptValidation,
|
|
1550
|
+
extensionValidation,
|
|
1542
1551
|
}, null, 2) }] };
|
|
1543
1552
|
});
|
|
1544
1553
|
|