@enfyra/mcp-server 0.1.12 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enfyra/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.17.0",
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
package/src/index.mjs
CHANGED
|
File without changes
|
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,7 @@ 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
37
|
'- Extension SFCs must use auto-injected components directly in templates, such as `<UButton>`, and must not call `resolveComponent()` for Nuxt UI/eApp components.',
|
|
38
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.',
|
|
39
39
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
@@ -630,7 +630,7 @@ export function validateExtensionCodeLocally(code) {
|
|
|
630
630
|
return { componentCasing: 'passed' };
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
async function validateExtensionCode(apiUrl, code, name) {
|
|
633
|
+
export async function validateExtensionCode(apiUrl, code, name) {
|
|
634
634
|
const localChecks = validateExtensionCodeLocally(code);
|
|
635
635
|
const result = await fetchAPI(apiUrl, '/enfyra_extension/preview', {
|
|
636
636
|
method: 'POST',
|
|
@@ -647,6 +647,45 @@ async function validateExtensionCode(apiUrl, code, name) {
|
|
|
647
647
|
};
|
|
648
648
|
}
|
|
649
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
|
+
|
|
650
689
|
function normalizeMetadataTables(metadata) {
|
|
651
690
|
const tables = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
|
|
652
691
|
return Array.isArray(tables) ? tables : Object.values(tables || {});
|
|
@@ -1527,7 +1566,8 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1527
1566
|
'validate_extension_code',
|
|
1528
1567
|
[
|
|
1529
1568
|
'Validate Enfyra admin extension code before saving it to enfyra_extension.',
|
|
1530
|
-
'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.',
|
|
1531
1571
|
'Call get_extension_theme_contract first when generating or reviewing UI.',
|
|
1532
1572
|
].join(' '),
|
|
1533
1573
|
{
|
|
@@ -1540,6 +1580,27 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1540
1580
|
}),
|
|
1541
1581
|
);
|
|
1542
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
|
+
|
|
1543
1604
|
server.tool(
|
|
1544
1605
|
'get_extension_theme_contract',
|
|
1545
1606
|
'Return the concise Enfyra admin extension UI/theme/security contract. Call before writing or reviewing extension UI.',
|
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
|
|