@enfyra/mcp-server 0.1.11 → 0.1.12

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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
@@ -34,6 +34,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
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
36
  '- Before saving standalone dynamic script or extension code, call `validate_dynamic_script` or `validate_extension_code` unless the chosen ensure/update tool already validates the code.',
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
 
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
+
544
633
  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,6 +641,7 @@ 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
  };
@@ -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.',