@enfyra/mcp-server 0.0.102 → 0.0.104
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
|
@@ -186,7 +186,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
186
186
|
- `validate_dynamic_script` checks handler, hook, flow, websocket, GraphQL, and bootstrap script source without saving.
|
|
187
187
|
- `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
188
188
|
- `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
|
|
189
|
-
- JSON responses include `compressionStats` with estimated token savings
|
|
189
|
+
- JSON responses include `compressionStats` with estimated token savings. Arrays of objects are converted to columnar form only when the compact shape is smaller than raw JSON.
|
|
190
190
|
- Relation tools reject physical FK/junction names.
|
|
191
191
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
192
192
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
35
35
|
'- Validate behavior with `test_rest_endpoint`, `run_admin_test`, `test_flow_step`, or the route-specific tool before claiming a dynamic feature works.',
|
|
36
36
|
'',
|
|
37
37
|
'### Core Contracts',
|
|
38
|
-
'- Tool JSON responses use `responseFormat: "json+columnar-v1"`.
|
|
38
|
+
'- Tool JSON responses use `responseFormat: "json+columnar-v1"`. Large arrays of objects may be encoded as `{ format: "columnar-v1", columns: [...], rows: [[...]], rowCount }` only when that is smaller than raw JSON; read each row value by matching `columns[index]` to `rows[n][index]`. Do not guess object keys inside `rows`. `compressionStats` estimates token savings and includes whether compression was applied; use it only when the user asks about savings.',
|
|
39
39
|
'- `query_table` and `get_all_routes` require explicit intent: pass `limit` for bounded reads or `all: true` for a complete list. Do not invent arbitrary limits such as 30 or 50.',
|
|
40
40
|
'- Read tools are minimal by default. Pass explicit `fields`; use metadata inspection before guessing field/relation names. Field exclusion mode exists: `fields=-compiledCode`, and `fields=id,-compiledCode` still means all readable fields except `compiledCode`.',
|
|
41
41
|
'- Mutations return ids/status by default. Re-read with `find_one_record` or `query_table` and explicit `fields` when the saved row matters.',
|
|
@@ -493,9 +493,10 @@ async function resolveApiEndpointWorkflowState(apiUrl, opts) {
|
|
|
493
493
|
const routeId = getId(route);
|
|
494
494
|
const availableMethods = methodNamesFromRecords(route?.availableMethods || [], methodIdNameMap);
|
|
495
495
|
const publicMethods = methodNamesFromRecords(route?.publicMethods || [], methodIdNameMap);
|
|
496
|
+
const methodAvailable = availableMethods.includes(methodName);
|
|
496
497
|
const routeNeedsUpdate = !!route && (
|
|
497
498
|
route.isEnabled === false
|
|
498
|
-
|| !
|
|
499
|
+
|| !methodAvailable
|
|
499
500
|
|| (access === 'public' && !publicMethods.includes(methodName))
|
|
500
501
|
|| (access === 'private' && publicMethods.includes(methodName))
|
|
501
502
|
|| (opts.description !== undefined && route.description !== opts.description)
|
|
@@ -549,8 +550,8 @@ async function resolveApiEndpointWorkflowState(apiUrl, opts) {
|
|
|
549
550
|
handlerId: getId(handler),
|
|
550
551
|
reason: handlerNeedsOverwrite && !opts.overwrite ? 'Existing handler differs. Re-run with overwrite=true to update it.' : undefined,
|
|
551
552
|
})
|
|
552
|
-
: step(route ? 'pending' : 'waiting', 'save_handler', 'Create route handler', {
|
|
553
|
-
reason: route ? undefined : 'Route must
|
|
553
|
+
: step(route && methodAvailable ? 'pending' : 'waiting', 'save_handler', 'Create route handler', {
|
|
554
|
+
reason: !route ? 'Route must exist first.' : methodAvailable ? undefined : 'Route method must be available first.',
|
|
554
555
|
}),
|
|
555
556
|
];
|
|
556
557
|
|
|
@@ -725,25 +726,40 @@ async function applyApiEndpointWorkflowStep(apiUrl, state, opts, stepId) {
|
|
|
725
726
|
async function runApiEndpointWorkflow(apiUrl, opts) {
|
|
726
727
|
let state = await resolveApiEndpointWorkflowState(apiUrl, opts);
|
|
727
728
|
const operations = [];
|
|
729
|
+
let completedEphemeralStepId = null;
|
|
728
730
|
if (opts.apply || opts.applyAll) {
|
|
729
731
|
const maxSteps = opts.applyAll ? 10 : 1;
|
|
730
732
|
for (let i = 0; i < maxSteps; i += 1) {
|
|
731
733
|
if (state.blocked || !state.firstRunnable) break;
|
|
732
734
|
const operation = await applyApiEndpointWorkflowStep(apiUrl, state, opts, opts.stepId);
|
|
733
735
|
operations.push(operation);
|
|
736
|
+
if (state.firstRunnable.id === 'smoke_test') {
|
|
737
|
+
completedEphemeralStepId = 'smoke_test';
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
734
740
|
if (!opts.applyAll) break;
|
|
735
741
|
state = await resolveApiEndpointWorkflowState(apiUrl, opts);
|
|
736
742
|
}
|
|
737
743
|
}
|
|
738
744
|
const latestState = operations.length ? await resolveApiEndpointWorkflowState(apiUrl, opts) : state;
|
|
745
|
+
const latestSteps = completedEphemeralStepId
|
|
746
|
+
? latestState.steps.map((item) => (
|
|
747
|
+
item.id === completedEphemeralStepId
|
|
748
|
+
? { ...item, status: 'completed', result: 'passed' }
|
|
749
|
+
: item
|
|
750
|
+
))
|
|
751
|
+
: latestState.steps;
|
|
752
|
+
const nextSteps = completedEphemeralStepId
|
|
753
|
+
? latestState.nextSteps.filter((item) => item.stepId !== completedEphemeralStepId)
|
|
754
|
+
: latestState.nextSteps;
|
|
739
755
|
return {
|
|
740
756
|
action: operations.length ? 'api_endpoint_workflow_advanced' : 'api_endpoint_workflow_planned',
|
|
741
757
|
endpoint: latestState.endpoint,
|
|
742
758
|
scriptValidation: latestState.scriptValidation,
|
|
743
|
-
steps:
|
|
759
|
+
steps: latestSteps,
|
|
744
760
|
operations,
|
|
745
|
-
complete:
|
|
746
|
-
nextSteps
|
|
761
|
+
complete: latestSteps.every((item) => ['completed', 'skipped'].includes(item.status)),
|
|
762
|
+
nextSteps,
|
|
747
763
|
cleanupHints: latestState.endpoint.routeId
|
|
748
764
|
? [
|
|
749
765
|
`Preview delete route handler with delete_record({ tableName: "enfyra_route_handler", id: ${JSON.stringify(latestState.endpoint.handlerId)} }) before cleanup.`,
|
|
@@ -66,60 +66,73 @@ function estimateTokens(jsonText) {
|
|
|
66
66
|
return Math.ceil(jsonText.length / 4);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function buildCompressionStats(originalPayload,
|
|
69
|
+
function buildCompressionStats(originalPayload, candidatePayload, selectedPayload, applied) {
|
|
70
70
|
const originalTokens = estimateTokens(safeJsonStringify(originalPayload));
|
|
71
|
-
const
|
|
72
|
-
const
|
|
71
|
+
const candidateTokens = estimateTokens(safeJsonStringify(candidatePayload));
|
|
72
|
+
const responseTokens = estimateTokens(safeJsonStringify(selectedPayload));
|
|
73
|
+
const candidateSavedTokens = originalTokens - candidateTokens;
|
|
74
|
+
const candidateSavedPercent = originalTokens > 0
|
|
75
|
+
? Number(((candidateSavedTokens / originalTokens) * 100).toFixed(2))
|
|
76
|
+
: 0;
|
|
77
|
+
const savedTokens = originalTokens - responseTokens;
|
|
73
78
|
const savedPercent = originalTokens > 0
|
|
74
79
|
? Number(((savedTokens / originalTokens) * 100).toFixed(2))
|
|
75
80
|
: 0;
|
|
76
81
|
return {
|
|
77
82
|
originalTokens,
|
|
78
|
-
compactTokens,
|
|
83
|
+
compactTokens: responseTokens,
|
|
79
84
|
savedTokens,
|
|
80
85
|
savedPercent,
|
|
86
|
+
applied,
|
|
87
|
+
candidateCompactTokens: candidateTokens,
|
|
88
|
+
candidateSavedTokens,
|
|
89
|
+
candidateSavedPercent,
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
function attachCompressionStats(originalPayload,
|
|
93
|
+
function attachCompressionStats(originalPayload, candidatePayload, selectedPayload, applied) {
|
|
85
94
|
if (
|
|
86
|
-
isPlainObject(
|
|
87
|
-
&&
|
|
88
|
-
&&
|
|
95
|
+
isPlainObject(selectedPayload)
|
|
96
|
+
&& selectedPayload.responseFormat === RESPONSE_FORMAT
|
|
97
|
+
&& selectedPayload[COMPRESSION_STATS_FIELD]
|
|
89
98
|
) {
|
|
90
|
-
return
|
|
99
|
+
return selectedPayload;
|
|
91
100
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
return {
|
|
102
|
+
...selectedPayload,
|
|
103
|
+
[COMPRESSION_STATS_FIELD]: buildCompressionStats(originalPayload, candidatePayload, selectedPayload, applied),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function wrapPayload(payload) {
|
|
108
|
+
if (!isPlainObject(payload)) {
|
|
97
109
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
responseFormat: RESPONSE_FORMAT,
|
|
111
|
+
value: payload,
|
|
100
112
|
};
|
|
101
113
|
}
|
|
102
114
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
responseFormat: RESPONSE_FORMAT,
|
|
116
|
+
...payload,
|
|
105
117
|
};
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
export function formatJsonPayload(payload) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (formatted.responseFormat === RESPONSE_FORMAT) {
|
|
117
|
-
return attachCompressionStats(payload, formatted);
|
|
121
|
+
if (
|
|
122
|
+
isPlainObject(payload)
|
|
123
|
+
&& payload.responseFormat === RESPONSE_FORMAT
|
|
124
|
+
&& payload[COMPRESSION_STATS_FIELD]
|
|
125
|
+
) {
|
|
126
|
+
return payload;
|
|
118
127
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
|
|
129
|
+
const originalPayload = wrapPayload(payload);
|
|
130
|
+
const columnarPayload = wrapPayload(toColumnar(payload));
|
|
131
|
+
const originalTokens = estimateTokens(safeJsonStringify(originalPayload));
|
|
132
|
+
const candidateTokens = estimateTokens(safeJsonStringify(columnarPayload));
|
|
133
|
+
const shouldApplyColumnar = candidateTokens < originalTokens;
|
|
134
|
+
const selectedPayload = shouldApplyColumnar ? columnarPayload : originalPayload;
|
|
135
|
+
return attachCompressionStats(originalPayload, columnarPayload, selectedPayload, shouldApplyColumnar);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
export function jsonContent(payload, { pretty = false } = {}) {
|