@enfyra/mcp-server 0.1.31 → 0.1.32
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 +4 -3
- package/dist/lib/mcp-instructions.d.ts +5 -1
- package/dist/lib/mcp-instructions.js +11 -9
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/platform-operation-tools.js +141 -3
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +4 -4
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/runtime-zone-tools.d.ts +44 -0
- package/dist/lib/runtime-zone-tools.js +196 -0
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/table-tools.d.ts +1 -0
- package/dist/lib/table-tools.js +48 -6
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/tool-routing.d.ts +1 -1
- package/dist/lib/tool-routing.js +42 -28
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.d.ts +9 -0
- package/dist/lib/toolset-filter.js +126 -0
- package/dist/lib/toolset-filter.js.map +1 -0
- package/dist/mcp-server-entry.js +48 -15
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -242,6 +242,7 @@ Without a target flag, interactive mode asks which client to configure. Non-inte
|
|
|
242
242
|
| `ENFYRA_APP_URL` | App/admin URL used by setup | `http://localhost:3000` |
|
|
243
243
|
| `ENFYRA_API_URL` | Runtime API base written into MCP client config | Generated by setup |
|
|
244
244
|
| `ENFYRA_API_TOKEN` | Programmatic token from the Enfyra admin UI `/me` | Required |
|
|
245
|
+
| `ENFYRA_MCP_TOOLSET` | Tool visibility mode: `guided` for curated default tools, or `full` for every low-level escape hatch | `guided` |
|
|
245
246
|
|
|
246
247
|
For normal apps and demos, enter the app/admin URL such as `http://localhost:3000` or `https://demo.enfyra.io`. Treat the direct Enfyra backend host as private infrastructure unless you are debugging Enfyra core/server internals.
|
|
247
248
|
|
|
@@ -280,13 +281,13 @@ The MCP server includes safety guards for LLM callers:
|
|
|
280
281
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
281
282
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
|
282
283
|
- `discover_enfyra_workflows` maps task intent to workflow surfaces before the agent loads detailed examples or guesses between similar tools.
|
|
283
|
-
- Platform operation tools such as `api_endpoint_workflow`, `extension_workflow`, `
|
|
284
|
+
- Platform operation tools such as `api_endpoint_workflow`, `extension_workflow`, `search_admin_extensions`, `debug_field_exposure`, `enable_route`, `disable_route`, `delete_route`, `public_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `plan_flow_steps`, fixed-type flow step tools, `ensure_menu`, `reorder_menus`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
284
285
|
- Schema changes are serialized.
|
|
285
286
|
- Destructive deletes return a preview before requiring `confirm=true`.
|
|
286
287
|
|
|
287
288
|
## Query Notes
|
|
288
289
|
|
|
289
|
-
Use explicit `fields` in read tools. Include mode is the default, such as `fields=id,email`. Any excluded field switches that scope to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Dotted exclusions such as `fields=-owner.avatar` work for relation fields when the relation exists in metadata. Every list/query call must pass either `limit` for a bounded page or `all: true` for a complete list. When a caller needs every matching row, pass `all: true` to `query_table`, `get_all_routes`, or `get_all_tables`; the tool should not choose an arbitrary page size like 30 or 50.
|
|
290
|
+
Use explicit `fields` in read tools. Include mode is the default, such as `fields=id,email`. Any excluded field switches that scope to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Dotted exclusions such as `fields=-owner.avatar` work for relation fields when the relation exists in metadata. Every broad list/query call must pass either `limit` for a bounded page or `all: true` for a complete list. Locator searches on `get_all_routes` and `get_all_tables` may omit `limit` when `search` is provided; they return a small bounded lookup window. When a caller needs every matching row, pass `all: true` to `query_table`, `get_all_routes`, or `get_all_tables`; the tool should not choose an arbitrary page size like 30 or 50.
|
|
290
291
|
|
|
291
292
|
## Enfyra URL Pattern
|
|
292
293
|
|
|
@@ -318,7 +319,7 @@ Do not create custom login/logout/me routes that manually set Enfyra token cooki
|
|
|
318
319
|
|
|
319
320
|
## Tool Summary
|
|
320
321
|
|
|
321
|
-
|
|
322
|
+
By default, the MCP server starts with `ENFYRA_MCP_TOOLSET=guided`, a curated tool surface optimized for weaker LLMs and one-shot success. It exposes workflow routing, focused discovery, runtime zone search, schema tools, query/CRUD envelopes, row-scope pre-hooks, focused extension patching, operation-level route/permission/extension/flow/websocket tools, validation, and narrow verification tools. Set `ENFYRA_MCP_TOOLSET=full` only for expert debugging or compatibility work that needs low-level escape hatches such as raw route construction, cache reloads, method metadata, broad metadata reads, or raw log file reads.
|
|
322
323
|
|
|
323
324
|
Routes have two separate controls. `isEnabled` controls runtime registration: disabled routes return `404`. Use `enable_route` and `disable_route` for this lifecycle. `publicMethods` controls anonymous access for enabled routes; use `public_route_methods` and `private_route_methods` for that access boundary.
|
|
324
325
|
|
|
@@ -3,4 +3,8 @@ export declare function buildGraphqlUrls(apiBaseUrl: any): {
|
|
|
3
3
|
graphqlHttpUrl: string;
|
|
4
4
|
graphqlSchemaUrl: string;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
type McpInstructionOptions = {
|
|
7
|
+
toolsetSummary?: string | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function buildMcpServerInstructions(apiBaseUrl: any, options?: McpInstructionOptions): string;
|
|
10
|
+
export {};
|
|
@@ -6,37 +6,39 @@ export function buildGraphqlUrls(apiBaseUrl) {
|
|
|
6
6
|
graphqlSchemaUrl: `${base}/graphql-schema`,
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
-
export function buildMcpServerInstructions(apiBaseUrl) {
|
|
9
|
+
export function buildMcpServerInstructions(apiBaseUrl, options = {}) {
|
|
10
10
|
const base = String(apiBaseUrl || '').replace(/\/$/, '');
|
|
11
11
|
const { graphqlHttpUrl, graphqlSchemaUrl } = buildGraphqlUrls(apiBaseUrl);
|
|
12
|
+
const toolsetSummary = options?.toolsetSummary || null;
|
|
12
13
|
return [
|
|
13
14
|
'## Enfyra MCP',
|
|
14
15
|
'',
|
|
15
16
|
`API base for this session: \`${base}\`.`,
|
|
16
17
|
`GraphQL endpoints: \`${graphqlHttpUrl}\` and \`${graphqlSchemaUrl}\`.`,
|
|
18
|
+
...(toolsetSummary ? ['', toolsetSummary] : []),
|
|
17
19
|
'',
|
|
18
20
|
'### Work Flow',
|
|
19
21
|
'- For a quick target/base sanity check, call `get_enfyra_api_context`; do not call broad discovery just to confirm which instance this MCP is connected to.',
|
|
20
22
|
'- When the task intent is clear but the right tool path is not, call `discover_enfyra_workflows` with the intent, risk, and optional surface. Use `detail: "plan"` before writes and follow `primaryPath` in order; do not choose from the flat tool list first.',
|
|
21
23
|
'- Discover before deciding. For architecture/capability questions call `discover_enfyra_system`; for DB/pk/runtime/cache context call `discover_runtime_context`; for filters/deep/sort/relation query shape call `discover_query_capabilities`. Run broad discovery tools sequentially, not in parallel.',
|
|
22
24
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, `inspect_feature`, and DB-backed runtime zone tools for the table/route/feature/surface being changed instead of loading broad metadata.',
|
|
23
|
-
'- For DB-backed artifacts
|
|
25
|
+
'- For admin UI/menu/extensions use `search_admin_extensions`; for other DB-backed artifacts use `search_runtime_zone`: search then inspect with `nextInspect.input`.',
|
|
24
26
|
'- Load examples only when needed. Use `get_enfyra_examples` by category. Before extension UI, call `get_extension_theme_contract`; call `get_theme_class_reference` for exact eapp/Nuxt UI theme classes.',
|
|
25
27
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
26
|
-
'- Before mutating metadata/schema/routes/permissions/menus/packages/cache/code/extensions, call `get_enfyra_required_knowledge
|
|
28
|
+
'- Before mutating metadata/schema/routes/permissions/menus/packages/cache/code/extensions, call `get_enfyra_required_knowledge` and pass `globalRulesAckKey` plus required code/extension ack keys.',
|
|
27
29
|
'- With non-root API tokens, call `get_permission_profile` before admin helper tools or 403 debugging.',
|
|
28
30
|
'- Prefer the most specific business operation tool over raw metadata CRUD. `discover_enfyra_workflows` provides the current operation-tool map and negative-routing avoidTools.',
|
|
29
|
-
'- Before saving standalone dynamic script code, call `validate_dynamic_script` or `/admin/script/validate` unless the write tool validates. For extensions, prefer atomic save tools
|
|
31
|
+
'- Before saving standalone dynamic script code, call `validate_dynamic_script` or `/admin/script/validate` unless the write tool validates. For extensions, prefer atomic save tools.',
|
|
30
32
|
'- Extension SFCs must use auto-injected components directly in templates, such as `<UButton>`, and must not call `resolveComponent()` for Nuxt UI/eApp components.',
|
|
31
33
|
'- 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.',
|
|
32
34
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
33
35
|
'',
|
|
34
36
|
'### Core Contracts',
|
|
35
37
|
'- Tool JSON responses use `responseFormat: "json+columnar-v1"`. If rows are columnar, read values by matching `columns[index]` to `rows[n][index]`; do not guess row keys.',
|
|
36
|
-
'- `query_table
|
|
38
|
+
'- `query_table` needs `limit` or `all:true`; do not invent arbitrary limits. `get_all_routes`/`get_all_tables` omit limit with `search`.',
|
|
37
39
|
'- Read tools are minimal by default. Pass explicit `fields`; inspect metadata before guessing. Field exclusion mode: `fields=-compiledCode`; `fields=id,-compiledCode` means all readable fields except `compiledCode`.',
|
|
38
|
-
'- Mutations return ids/status
|
|
39
|
-
'- Mutation tools are plural-only
|
|
40
|
+
'- Mutations return ids/status. Re-read with explicit `fields` only when saved shape matters.',
|
|
41
|
+
'- Mutation tools are plural-only: pass native JSON arrays, using one-item arrays for single writes.',
|
|
40
42
|
'- For schema creation, do not declare `id`, `_id`, `createdAt`, or `updatedAt`; Enfyra manages them. `create_tables` strips them and reports `skippedAutoColumns`; `create_columns` rejects them.',
|
|
41
43
|
'- Relation-based indexes/uniques must reference relation `propertyName` values that exist on the same table. Put the owning relations in the same `create_tables` item for one-pass creation, or add relation-based uniques later with `update_tables` after relations exist.',
|
|
42
44
|
'- Fields in `uniques`, including composite groups like `["event","attendee"]`, must not also appear in `indexes`; uniques already index them. `create_tables` preflights the whole batch.',
|
|
@@ -55,9 +57,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
55
57
|
'- Relation filters use relation propertyName values, not physical FK-shaped names: use `{ incident: { id: { _eq: id } } }`, not `{ incidentId: { _eq: id } }`.',
|
|
56
58
|
'- `query_table` accepts native object `filter`, `deep`, and `aggregate`; always pass `limit` or `all:true`. Deep keys are relation names; MCP auto-adds missing top-level deep fields. Deep options: `fields`, `filter`, `sort`, `limit`, `page`, `deep`; never `_fields`.',
|
|
57
59
|
'- For counts, prefer `count_records` or `meta=filterCount/totalCount`. Do not guess `_sum`/`_count`; call `discover_query_capabilities` before `aggregate`.',
|
|
58
|
-
'- If REST exposes `isPublished=false` fields via fields/deep, treat
|
|
60
|
+
'- If REST exposes `isPublished=false` fields via fields/deep, use `debug_field_exposure`; treat confirmed leaks as core issues, not UI/hook fixes.',
|
|
59
61
|
'- Script source is `sourceCode`; `compiledCode` is generated and may differ textually because macros expand. Do not warn about source/compiled mismatch unless validation or runtime behavior proves the compiled artifact is stale.',
|
|
60
|
-
'- For user/domain errors use `@THROW`, not `throw new Error(...)`. Numeric helpers are raw HTTP messages
|
|
62
|
+
'- For user/domain errors use `@THROW`, not `throw new Error(...)`. Numeric helpers are raw HTTP messages; details must be an object/array. Semantic helpers: `notFound(resource, identifier)`, `duplicate(resource, field, value)`.',
|
|
61
63
|
'- Destructive operations are preview-first. Do not pass `confirm=true` until the user explicitly approves.',
|
|
62
64
|
'- Treat permission and security as the first design step for any route, handler, flow, extension, or data surface.',
|
|
63
65
|
'- Admin UI `usePermissions()` and backend RoleGuard use route permissions; use `audit_route_access`/`ensure_route_access`.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,eAAe;QACf,6JAA6J;QAC7J,kQAAkQ;QAClQ,2SAA2S;QAC3S,oMAAoM;QACpM,sKAAsK;QACtK,2MAA2M;QAC3M,8HAA8H;QAC9H,qMAAqM;QACrM,uGAAuG;QACvG,iLAAiL;QACjL,uLAAuL;QACvL,oKAAoK;QACpK,+LAA+L;QAC/L,wJAAwJ;QACxJ,EAAE;QACF,oBAAoB;QACpB,4KAA4K;QAC5K,0IAA0I;QAC1I,yNAAyN;QACzN,8FAA8F;QAC9F,qGAAqG;QACrG,mMAAmM;QACnM,+QAA+Q;QAC/Q,2LAA2L;QAC3L,oLAAoL;QACpL,yJAAyJ;QACzJ,6NAA6N;QAC7N,+LAA+L;QAC/L,qKAAqK;QACrK,2NAA2N;QAC3N,4HAA4H;QAC5H,yMAAyM;QACzM,mMAAmM;QACnM,8QAA8Q;QAC9Q,iRAAiR;QACjR,2KAA2K;QAC3K,gKAAgK;QAChK,4QAA4Q;QAC5Q,6JAA6J;QAC7J,oJAAoJ;QACpJ,sOAAsO;QACtO,qOAAqO;QACrO,4GAA4G;QAC5G,oHAAoH;QACpH,4HAA4H;QAC5H,yKAAyK;QACzK,yMAAyM;QACzM,EAAE;QACF,6BAA6B;QAC7B,4NAA4N;QAC5N,uMAAuM;QACvM,sMAAsM;QACtM,EAAE;QACF,4BAA4B;QAC5B,qQAAqQ;QACrQ,wKAAwK;QACxK,qKAAqK;QACrK,+PAA+P;QAC/P,EAAE;QACF,yBAAyB;QACzB,kNAAkN;QAClN,oNAAoN;QACpN,6VAA6V;QAC7V,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { fetchAPI } from './fetch.js';
|
|
3
4
|
import { validatePortableScriptSource, validateScriptSourceIfPresent } from './mutation-guards.js';
|
|
4
5
|
import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertExtensionKnowledgeAck, assertGlobalRulesAck, dynamicCodeKnowledgeAckParam, extensionKnowledgeAckParam, globalRulesAckParam, } from './required-knowledge.js';
|
|
@@ -646,6 +647,70 @@ async function updateExtensionCode(apiUrl, { id, name, code, description, isEnab
|
|
|
646
647
|
validation,
|
|
647
648
|
};
|
|
648
649
|
}
|
|
650
|
+
function sha256Text(value) {
|
|
651
|
+
return createHash('sha256').update(String(value ?? '')).digest('hex');
|
|
652
|
+
}
|
|
653
|
+
async function patchExtensionCode(apiUrl, { id, name, search, replace, expectedSha256, apply, description, isEnabled, version, globalRulesAckKey, extensionKnowledgeAckKey, }) {
|
|
654
|
+
assertGlobalRulesAck(globalRulesAckKey);
|
|
655
|
+
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|
|
656
|
+
if (!id && !name)
|
|
657
|
+
throw new Error('Provide id or name to patch an existing extension.');
|
|
658
|
+
if (!search)
|
|
659
|
+
throw new Error('search must be a non-empty exact code fragment.');
|
|
660
|
+
const existing = id
|
|
661
|
+
? await findRecord(apiUrl, 'enfyra_extension', { id: { _eq: id } }, 'id,_id,name,type,menu.id,code')
|
|
662
|
+
: await findRecord(apiUrl, 'enfyra_extension', { name: { _eq: name } }, 'id,_id,name,type,menu.id,code');
|
|
663
|
+
if (!existing)
|
|
664
|
+
throw new Error(`Extension not found: ${id || name}`);
|
|
665
|
+
const extensionId = getId(existing);
|
|
666
|
+
const currentCode = String(existing.code ?? '');
|
|
667
|
+
const currentSha256 = sha256Text(currentCode);
|
|
668
|
+
if (expectedSha256 && expectedSha256 !== currentSha256) {
|
|
669
|
+
throw new Error(`Extension code hash mismatch. Expected ${expectedSha256}, got ${currentSha256}. Re-read the extension before patching.`);
|
|
670
|
+
}
|
|
671
|
+
const occurrences = currentCode.split(search).length - 1;
|
|
672
|
+
if (occurrences !== 1) {
|
|
673
|
+
throw new Error(`Expected search fragment to occur exactly once; found ${occurrences}. Use a more specific fragment or update_extension_code for a full replacement.`);
|
|
674
|
+
}
|
|
675
|
+
const nextCode = currentCode.replace(search, replace);
|
|
676
|
+
const nextSha256 = sha256Text(nextCode);
|
|
677
|
+
const preview = {
|
|
678
|
+
action: apply ? 'extension_code_patch_applied' : 'extension_code_patch_previewed',
|
|
679
|
+
id: extensionId,
|
|
680
|
+
name: existing.name || name || null,
|
|
681
|
+
type: existing.type || null,
|
|
682
|
+
currentSha256,
|
|
683
|
+
nextSha256,
|
|
684
|
+
currentLength: currentCode.length,
|
|
685
|
+
nextLength: nextCode.length,
|
|
686
|
+
occurrences,
|
|
687
|
+
apply: Boolean(apply),
|
|
688
|
+
};
|
|
689
|
+
if (!apply) {
|
|
690
|
+
return {
|
|
691
|
+
...preview,
|
|
692
|
+
nextStep: {
|
|
693
|
+
tool: 'patch_extension_code',
|
|
694
|
+
input: { id: extensionId, expectedSha256: currentSha256, search, replace, apply: true },
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const result = await updateExtensionCode(apiUrl, {
|
|
699
|
+
id: extensionId,
|
|
700
|
+
name: undefined,
|
|
701
|
+
code: nextCode,
|
|
702
|
+
description,
|
|
703
|
+
isEnabled,
|
|
704
|
+
version,
|
|
705
|
+
globalRulesAckKey,
|
|
706
|
+
extensionKnowledgeAckKey,
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
...preview,
|
|
710
|
+
result,
|
|
711
|
+
validation: result.validation,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
649
714
|
function normalizeMetadataTables(metadata) {
|
|
650
715
|
const tables = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
|
|
651
716
|
return Array.isArray(tables) ? tables : Object.values(tables || {});
|
|
@@ -948,6 +1013,30 @@ function chooseFlowStepTool(intent) {
|
|
|
948
1013
|
return FLOW_STEP_TOOL_GUIDANCE.find((item) => item.type === 'query');
|
|
949
1014
|
return FLOW_STEP_TOOL_GUIDANCE.find((item) => item.type === 'script');
|
|
950
1015
|
}
|
|
1016
|
+
function planFlowSteps(steps) {
|
|
1017
|
+
const items = Array.isArray(steps) ? steps : [];
|
|
1018
|
+
return items.map((step, index) => {
|
|
1019
|
+
const intent = typeof step === 'string' ? step : step?.intent;
|
|
1020
|
+
const key = typeof step === 'object' && step?.key ? String(step.key) : `step_${index + 1}`;
|
|
1021
|
+
const recommendation = chooseFlowStepTool(intent);
|
|
1022
|
+
return {
|
|
1023
|
+
order: index + 1,
|
|
1024
|
+
key,
|
|
1025
|
+
intent,
|
|
1026
|
+
tool: recommendation.tool,
|
|
1027
|
+
type: recommendation.type,
|
|
1028
|
+
suggestedInput: {
|
|
1029
|
+
key,
|
|
1030
|
+
name: typeof step === 'object' && step?.name ? step.name : key.replace(/_/g, ' '),
|
|
1031
|
+
order: index + 1,
|
|
1032
|
+
...(recommendation.config ? { config: recommendation.config } : {}),
|
|
1033
|
+
...(recommendation.sourceCode ? { sourceCode: recommendation.sourceCode } : {}),
|
|
1034
|
+
...(recommendation.condition ? { condition: recommendation.condition } : {}),
|
|
1035
|
+
},
|
|
1036
|
+
reason: recommendation.when,
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
951
1040
|
function normalizeEndpointAccess(anonymousAccess, makePublic) {
|
|
952
1041
|
if (makePublic !== undefined)
|
|
953
1042
|
return makePublic ? 'public' : 'private';
|
|
@@ -1087,6 +1176,12 @@ async function resolveApiEndpointWorkflowState(apiUrl, opts) {
|
|
|
1087
1176
|
}
|
|
1088
1177
|
const firstRunnable = steps.find((item) => item.status === 'pending') || null;
|
|
1089
1178
|
const blocked = steps.find((item) => item.status === 'blocked') || null;
|
|
1179
|
+
const pendingAckParams = firstRunnable
|
|
1180
|
+
? [
|
|
1181
|
+
'globalRulesAckKey',
|
|
1182
|
+
...(firstRunnable.id === 'save_handler' ? ['knowledgeAckKey'] : []),
|
|
1183
|
+
]
|
|
1184
|
+
: [];
|
|
1090
1185
|
const nextSteps = blocked
|
|
1091
1186
|
? [{ tool: 'api_endpoint_workflow', input: { path: normalizedPath, method: methodName, overwrite: true }, reason: blocked.reason }]
|
|
1092
1187
|
: firstRunnable
|
|
@@ -1094,7 +1189,10 @@ async function resolveApiEndpointWorkflowState(apiUrl, opts) {
|
|
|
1094
1189
|
tool: 'api_endpoint_workflow',
|
|
1095
1190
|
input: { path: normalizedPath, method: methodName, apply: true },
|
|
1096
1191
|
stepId: firstRunnable.id,
|
|
1097
|
-
|
|
1192
|
+
requiredAckParams: pendingAckParams,
|
|
1193
|
+
requiresKnowledgeAck: pendingAckParams.length
|
|
1194
|
+
? `Pass ${pendingAckParams.join(' and ')} from get_enfyra_required_knowledge when applying this step.`
|
|
1195
|
+
: undefined,
|
|
1098
1196
|
}]
|
|
1099
1197
|
: [];
|
|
1100
1198
|
return {
|
|
@@ -1501,6 +1599,23 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1501
1599
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
1502
1600
|
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
1503
1601
|
}, async (input) => jsonText(await updateExtensionCode(ENFYRA_API_URL, input)));
|
|
1602
|
+
server.tool('patch_extension_code', [
|
|
1603
|
+
'Focused operation: patch an existing Enfyra admin extension code by exact search/replace.',
|
|
1604
|
+
'Use this for small UI fixes instead of rewriting the whole Vue SFC. It hash-checks the current code, validates with /enfyra_extension/preview, and saves only when apply=true.',
|
|
1605
|
+
'Default apply=false returns a preview and nextStep input.',
|
|
1606
|
+
].join(' '), {
|
|
1607
|
+
id: z.union([z.string(), z.number()]).optional().describe('Existing extension id. Provide id or name.'),
|
|
1608
|
+
name: z.string().optional().describe('Existing extension unique name. Provide id or name.'),
|
|
1609
|
+
search: z.string().describe('Exact code fragment that must occur once.'),
|
|
1610
|
+
replace: z.string().describe('Replacement code fragment.'),
|
|
1611
|
+
expectedSha256: z.string().optional().describe('Optional SHA-256 of current extension code from a prior inspect/read. Rejects stale patches.'),
|
|
1612
|
+
apply: z.boolean().optional().default(false).describe('Preview by default. Set true to validate and save.'),
|
|
1613
|
+
description: z.string().optional().describe('Optional replacement extension description. Omit to preserve.'),
|
|
1614
|
+
isEnabled: z.boolean().optional().describe('Optional enabled state. Omit to preserve.'),
|
|
1615
|
+
version: z.string().optional().describe('Optional extension version. Omit to preserve.'),
|
|
1616
|
+
globalRulesAckKey: globalRulesAckParam(z),
|
|
1617
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
1618
|
+
}, async (input) => jsonText(await patchExtensionCode(ENFYRA_API_URL, input)));
|
|
1504
1619
|
server.tool('get_extension_theme_contract', 'Return the concise Enfyra admin extension UI/theme/security contract. Call before writing or reviewing extension UI.', {}, async () => jsonText(getExtensionThemeContract()));
|
|
1505
1620
|
server.tool('get_theme_class_reference', [
|
|
1506
1621
|
'Return the authoritative Enfyra theme & color class reference: class -> CSS variable -> Nuxt UI semantic color -> intent.',
|
|
@@ -1529,8 +1644,8 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1529
1644
|
description: z.string().optional().describe('Extension description.'),
|
|
1530
1645
|
isEnabled: z.boolean().optional().default(true).describe('Enable extension.'),
|
|
1531
1646
|
version: z.string().optional().default('1.0.0').describe('Extension version.'),
|
|
1532
|
-
apply: z.boolean().optional().default(false).describe('false returns plan only; true applies exactly the next pending step.'),
|
|
1533
|
-
applyAll: z.boolean().optional().default(false).describe('true applies all safe pending steps in order. Prefer apply=true for production changes.'),
|
|
1647
|
+
apply: z.boolean().optional().default(false).describe('false returns plan only; true applies exactly the next pending step. When true, always pass globalRulesAckKey; also pass knowledgeAckKey when saving handler sourceCode.'),
|
|
1648
|
+
applyAll: z.boolean().optional().default(false).describe('true applies all safe pending steps in order. Prefer apply=true for production changes. When true, always pass globalRulesAckKey and pass knowledgeAckKey if handler sourceCode may be saved.'),
|
|
1534
1649
|
stepId: z.string().optional().describe('Optional pending step id to apply. Omit to apply the next pending step.'),
|
|
1535
1650
|
globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when apply/applyAll mutates metadata. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
|
|
1536
1651
|
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z).optional().describe('Required when apply/applyAll saves extension code. Use extensionAckKey from get_enfyra_required_knowledge.'),
|
|
@@ -2103,6 +2218,29 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2103
2218
|
],
|
|
2104
2219
|
});
|
|
2105
2220
|
});
|
|
2221
|
+
server.tool('plan_flow_steps', 'Dry-run helper: choose the ordered Enfyra flow step tools for a whole flow plan before mutating flow metadata.', {
|
|
2222
|
+
steps: z.array(z.union([
|
|
2223
|
+
z.string(),
|
|
2224
|
+
z.object({
|
|
2225
|
+
key: z.string().optional().describe('Stable step key. Generated when omitted.'),
|
|
2226
|
+
name: z.string().optional().describe('Human label. Defaults from key.'),
|
|
2227
|
+
intent: z.string().describe('Plain-language description of this step.'),
|
|
2228
|
+
}),
|
|
2229
|
+
])).min(1).max(30).describe('Ordered step intents. Use this before ensure_*_flow_step calls when a flow has multiple steps.'),
|
|
2230
|
+
}, async ({ steps }) => {
|
|
2231
|
+
const plan = planFlowSteps(steps);
|
|
2232
|
+
return jsonText({
|
|
2233
|
+
action: 'flow_steps_planned',
|
|
2234
|
+
stepCount: plan.length,
|
|
2235
|
+
plan,
|
|
2236
|
+
nextSteps: [
|
|
2237
|
+
'Create or update the flow with ensure_manual_flow or ensure_scheduled_flow first.',
|
|
2238
|
+
'Call each planned ensure_*_flow_step in order, adding flowName or flowId plus table/query/config details.',
|
|
2239
|
+
'Use ensure_script_flow_step only for steps where the plan chose script because fixed step types are insufficient.',
|
|
2240
|
+
'Use test_flow_step for script/condition/high-risk steps before triggering the full flow.',
|
|
2241
|
+
],
|
|
2242
|
+
});
|
|
2243
|
+
});
|
|
2106
2244
|
server.tool('ensure_script_flow_step', 'Business operation: create or update one script flow step. Use this for JavaScript/TypeScript flow logic instead of choosing type=script manually.', {
|
|
2107
2245
|
flowName: z.string().optional().describe('Flow name. Use flowName or flowId.'),
|
|
2108
2246
|
flowId: z.union([z.string(), z.number()]).optional().describe('Flow id. Use flowName or flowId.'),
|