@enfyra/mcp-server 0.1.13 → 0.1.15
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 +14 -1
- package/dist/index.d.ts +5 -0
- package/{src/index.mjs → dist/index.js} +8 -10
- package/dist/index.js.map +1 -0
- package/dist/lib/auth.d.ts +34 -0
- package/dist/lib/auth.js +161 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config-local.d.ts +1 -0
- package/dist/lib/config-local.js +719 -0
- package/dist/lib/config-local.js.map +1 -0
- package/dist/lib/extension-search-tools.d.ts +59 -0
- package/dist/lib/extension-search-tools.js +483 -0
- package/dist/lib/extension-search-tools.js.map +1 -0
- package/dist/lib/fetch.d.ts +28 -0
- package/dist/lib/fetch.js +106 -0
- package/dist/lib/fetch.js.map +1 -0
- package/dist/lib/mcp-examples.d.ts +99 -0
- package/dist/lib/mcp-examples.js +2285 -0
- package/dist/lib/mcp-examples.js.map +1 -0
- package/dist/lib/mcp-instructions.d.ts +11 -0
- package/dist/lib/mcp-instructions.js +78 -0
- package/dist/lib/mcp-instructions.js.map +1 -0
- package/dist/lib/mutation-guards.d.ts +33 -0
- package/dist/lib/mutation-guards.js +106 -0
- package/dist/lib/mutation-guards.js.map +1 -0
- package/dist/lib/platform-operation-tools.d.ts +12 -0
- package/dist/lib/platform-operation-tools.js +2304 -0
- package/dist/lib/platform-operation-tools.js.map +1 -0
- package/dist/lib/required-knowledge.d.ts +32 -0
- package/dist/lib/required-knowledge.js +181 -0
- package/dist/lib/required-knowledge.js.map +1 -0
- package/dist/lib/response-format.d.ts +7 -0
- package/dist/lib/response-format.js +179 -0
- package/dist/lib/response-format.js.map +1 -0
- package/dist/lib/route-guards.d.ts +1 -0
- package/dist/lib/route-guards.js +19 -0
- package/dist/lib/route-guards.js.map +1 -0
- package/dist/lib/route-permission-tools.d.ts +91 -0
- package/dist/lib/route-permission-tools.js +151 -0
- package/dist/lib/route-permission-tools.js.map +1 -0
- package/dist/lib/source-artifacts.d.ts +27 -0
- package/dist/lib/source-artifacts.js +82 -0
- package/dist/lib/source-artifacts.js.map +1 -0
- package/dist/lib/table-tools.d.ts +62 -0
- package/dist/lib/table-tools.js +774 -0
- package/dist/lib/table-tools.js.map +1 -0
- package/dist/lib/tool-routing.d.ts +297 -0
- package/dist/lib/tool-routing.js +585 -0
- package/dist/lib/tool-routing.js.map +1 -0
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/mcp-server-entry.d.ts +4 -0
- package/dist/mcp-server-entry.js +2787 -0
- package/dist/mcp-server-entry.js.map +1 -0
- package/package.json +17 -10
- package/src/lib/auth.js +0 -179
- package/src/lib/config-local.mjs +0 -718
- package/src/lib/fetch.js +0 -111
- package/src/lib/mcp-examples.js +0 -2289
- package/src/lib/mcp-instructions.js +0 -80
- package/src/lib/mutation-guards.js +0 -118
- package/src/lib/platform-operation-tools.js +0 -2616
- package/src/lib/required-knowledge.js +0 -188
- package/src/lib/response-format.js +0 -187
- package/src/lib/route-guards.js +0 -24
- package/src/lib/route-permission-tools.js +0 -160
- package/src/lib/source-artifacts.js +0 -82
- package/src/lib/table-tools.js +0 -907
- package/src/lib/tool-routing.js +0 -589
- package/src/mcp-server-entry.mjs +0 -3177
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
export const GLOBAL_RULES_ACK_KEY = 'EFYRA::GLOBAL-RULES::UNIQUE-FIELDS-ARE-INDEXED::2bW-20260701B';
|
|
2
|
-
export const DYNAMIC_CODE_KNOWLEDGE_ACK_KEY = 'EFYRA::SECURE-REPO-CONTRACT::R9x-kelp-42Q::NO-RAW-TRUSTED';
|
|
3
|
-
export const EXTENSION_KNOWLEDGE_ACK_KEY = 'EFYRA::EXTENSION-THEME-CONTRACT::VIOLET-IS-NOT-A-PLAN::7mQ';
|
|
4
|
-
|
|
5
|
-
const REQUIRED_KNOWLEDGE_VERSION = '2026-07-01.global-rules-v2';
|
|
6
|
-
|
|
7
|
-
export function globalRulesAckParam(z) {
|
|
8
|
-
return z.string().describe('Required global-rules acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the global Enfyra MCP rules, then pass globalRulesAckKey exactly.');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function dynamicCodeKnowledgeAckParam(z) {
|
|
12
|
-
return z.string().describe('Required dynamic-code acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the dynamic server code knowledge, then pass dynamicCodeAckKey exactly.');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function extensionKnowledgeAckParam(z) {
|
|
16
|
-
return z.string().describe('Required extension acknowledgement key from get_enfyra_required_knowledge. Call that tool, read the extension/theme knowledge, then pass extensionAckKey exactly.');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function assertGlobalRulesAck(key) {
|
|
20
|
-
if (key !== GLOBAL_RULES_ACK_KEY) {
|
|
21
|
-
throw new Error('Missing or invalid global-rules acknowledgement. Call get_enfyra_required_knowledge, read the global Enfyra MCP rules, then pass globalRulesAckKey as globalRulesAckKey.');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function assertGlobalRulesAckIf(condition, key) {
|
|
26
|
-
if (condition) assertGlobalRulesAck(key);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function assertDynamicCodeKnowledgeAck(key) {
|
|
30
|
-
if (key !== DYNAMIC_CODE_KNOWLEDGE_ACK_KEY) {
|
|
31
|
-
throw new Error('Missing or invalid dynamic-code knowledge acknowledgement. Call get_enfyra_required_knowledge, read the dynamic server code contracts, then pass dynamicCodeAckKey as knowledgeAckKey.');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function assertDynamicCodeKnowledgeAckIf(condition, key) {
|
|
36
|
-
if (condition) assertDynamicCodeKnowledgeAck(key);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function assertExtensionKnowledgeAck(key) {
|
|
40
|
-
if (key !== EXTENSION_KNOWLEDGE_ACK_KEY) {
|
|
41
|
-
throw new Error('Missing or invalid extension knowledge acknowledgement. Call get_enfyra_required_knowledge, read the extension/theme contracts, then pass extensionAckKey as extensionKnowledgeAckKey.');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function assertExtensionKnowledgeAckIf(condition, key) {
|
|
46
|
-
if (condition) assertExtensionKnowledgeAck(key);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function buildRequiredKnowledgePayload() {
|
|
50
|
-
return {
|
|
51
|
-
version: REQUIRED_KNOWLEDGE_VERSION,
|
|
52
|
-
purpose: 'Read this before mutating Enfyra metadata, schema, routes, permissions, menus, packages, cache state, dynamic server code, or extension UI through MCP.',
|
|
53
|
-
globalRulesAckKey: GLOBAL_RULES_ACK_KEY,
|
|
54
|
-
dynamicCodeAckKey: DYNAMIC_CODE_KNOWLEDGE_ACK_KEY,
|
|
55
|
-
extensionAckKey: EXTENSION_KNOWLEDGE_ACK_KEY,
|
|
56
|
-
usage: [
|
|
57
|
-
'Pass globalRulesAckKey exactly as globalRulesAckKey when calling MCP tools that mutate Enfyra metadata, schema, routes, permissions, menus, packages, cache state, dynamic code, or extension UI.',
|
|
58
|
-
'Pass dynamicCodeAckKey exactly as knowledgeAckKey when calling MCP tools that create or update dynamic server code.',
|
|
59
|
-
'Pass extensionAckKey exactly as extensionKnowledgeAckKey when calling MCP tools that create or update Enfyra extension code.',
|
|
60
|
-
],
|
|
61
|
-
globalRules: [
|
|
62
|
-
{
|
|
63
|
-
id: 'examples-are-reasoning-anchors',
|
|
64
|
-
rules: [
|
|
65
|
-
'Examples explain transferable decisions, not copy-paste mandates.',
|
|
66
|
-
'Preserve platform contracts and safety boundaries, then adapt names, routes, fields, menus, labels, and lifecycle to live metadata and the user goal.',
|
|
67
|
-
'When examples use chat, order, report, cloud, email, or support domains, treat those as analogies unless the current task is exactly that domain.',
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'discover-before-changing',
|
|
72
|
-
rules: [
|
|
73
|
-
'Inspect live metadata/routes/features before schema, route, permission, extension, flow, or handler changes.',
|
|
74
|
-
'Use narrow inspection tools for the table, route, feature, or script being changed instead of broad discovery after the target is known.',
|
|
75
|
-
'Read sourceCode, not compiledCode, for editable dynamic scripts.',
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'mutations-are-intentional',
|
|
80
|
-
rules: [
|
|
81
|
-
'Prefer business operation tools over generic CRUD when a specific tool exists.',
|
|
82
|
-
'Destructive operations are preview-first; pass confirm=true only after explicit user approval.',
|
|
83
|
-
'Do not manually reload caches unless natural partial reload is proven stale or a concrete reload error requires it.',
|
|
84
|
-
'Never fabricate ids, field names, relation names, paths, package names, or permission scopes.',
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'schema-constraints',
|
|
89
|
-
rules: [
|
|
90
|
-
'For table schema, a field that appears in any uniques group must not appear in indexes.',
|
|
91
|
-
'A unique constraint already creates the indexed unique lookup for its fields.',
|
|
92
|
-
'Use uniques for data integrity and indexes only for non-unique query-performance fields that are not already unique.',
|
|
93
|
-
'Before create_table or update_table with indexes/uniques, inspect the current table and remove indexes that reference unique fields.',
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'security-first',
|
|
98
|
-
rules: [
|
|
99
|
-
'Treat permission and owner/tenant scope as the first design step for any route, handler, hook, flow, extension, websocket, or data surface.',
|
|
100
|
-
'Route permission only lets authenticated users reach a route after RoleGuard; handlers, hooks, RLS, and scripts still enforce record ownership and tenant/project scope.',
|
|
101
|
-
'Do not expose unpublished fields, private relation facts, secret values, token hashes, stack traces, SQL, provider payloads, or generated passwords to user-facing clients.',
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: 'shell-signals',
|
|
106
|
-
rules: [
|
|
107
|
-
'For app shell menu/account-panel notifications, decide the signal source before choosing count or dot.',
|
|
108
|
-
'Use a count only when the shell receives an exact or bounded count from a notification/summary source.',
|
|
109
|
-
'Use a dot when realtime only proves new attention exists.',
|
|
110
|
-
'Do not fetch destination domain lists such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
dynamicServerCode: [
|
|
115
|
-
{
|
|
116
|
-
id: 'secure-vs-trusted-repositories',
|
|
117
|
-
rules: [
|
|
118
|
-
'@REPOS.main is the secure repository for the current route main table and preserves normal route query behavior.',
|
|
119
|
-
'@REPOS.secure.<table> is the secure explicit-table repository. Use it for public/user-facing custom handlers, hooks, websocket scripts, flows that return data, and third-party app integrations.',
|
|
120
|
-
'@REPOS.<table> is the trusted internal repository. It may read/write hidden fields and is only for server-owned maintenance/admin logic that intentionally needs that access.',
|
|
121
|
-
'Never return raw trusted-repository records to users. Project or sanitize output before returning it.',
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: 'authorization-is-separate',
|
|
126
|
-
rules: [
|
|
127
|
-
'Secure repository selection does not prove the user is allowed to access a record.',
|
|
128
|
-
'Handlers and hooks still need route access, owner/tenant filters, membership checks, and explicit mutation authorization.',
|
|
129
|
-
'For canonical table reads and RLS, merge security filters into @QUERY.filter and preserve @QUERY.fields, @QUERY.deep, @QUERY.sort, @QUERY.limit, @QUERY.page, @QUERY.meta, @QUERY.aggregate, and debugMode.',
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: 'hidden-field-query-surfaces',
|
|
134
|
-
rules: [
|
|
135
|
-
'Unpublished fields and private relations are sensitive even when the field is not selected.',
|
|
136
|
-
'Do not expose filter predicate-oracle behavior over hidden fields in user-facing endpoints.',
|
|
137
|
-
'Do not expose aggregate, _max, _min, _count, sort helpers, or counts over unpublished fields/private relations unless the endpoint intentionally exposes that fact.',
|
|
138
|
-
'If a normal REST read returns an isPublished=false field through fields/deep/dotted projection, treat it as an Enfyra core bug and verify the minimal REST repro.',
|
|
139
|
-
],
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
id: 'dynamic-script-shape',
|
|
143
|
-
rules: [
|
|
144
|
-
'Use sourceCode and scriptLanguage; never send compiledCode.',
|
|
145
|
-
'Prefer macros such as @BODY, @QUERY, @PARAMS, @USER, @REQ, @RES, @REPOS, @HELPERS, @STORAGE, @SOCKET, and @THROW* when available.',
|
|
146
|
-
'Repository reads use filter, not where.',
|
|
147
|
-
'Create/update repository calls return collection-shaped data arrays; read result.data?.[0] for a single row.',
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
extensions: [
|
|
152
|
-
{
|
|
153
|
-
id: 'theme-contract-first',
|
|
154
|
-
rules: [
|
|
155
|
-
'Call get_extension_theme_contract before writing or reviewing page, widget, or global extension UI.',
|
|
156
|
-
'Call get_theme_class_reference when an exact eapp-* class or Nuxt UI color mapping is needed.',
|
|
157
|
-
'Use eapp-surface-*, eapp-text-*, eapp-divide-y, and eapp-divider for neutral app-shell surfaces.',
|
|
158
|
-
'Use eapp-primary-* or Nuxt UI primary only for runtime-primary identity/accent intent controlled by the app color picker.',
|
|
159
|
-
'Use semantic state colors only for true status/error/warning/success/info indicators, not for large KPI/list containers.',
|
|
160
|
-
'Do not use raw CSS variable utilities such as text-[var(...)], bg-[var(...)], or border-[var(...)] when class tokens exist.',
|
|
161
|
-
'Do not hard-code concrete palettes such as color="violet" or Tailwind palette accents for themeable UI.',
|
|
162
|
-
],
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
id: 'extension-shell-boundary',
|
|
166
|
-
rules: [
|
|
167
|
-
'Extension roots render inside the Enfyra admin app shell. Do not add root-level page padding such as p-4 sm:p-6 xl:p-8.',
|
|
168
|
-
'Page extensions should be full-bleed by default and responsive from the first version.',
|
|
169
|
-
'Do not wrap whole pages in decorative cards; use cards only for repeated items, modals, or genuinely framed tools.',
|
|
170
|
-
'Use NuxtLink or Nuxt UI components with :to for visible navigation links; reserve navigateTo for imperative side effects.',
|
|
171
|
-
'Admin extension links for record management should point to /data/<table>, not public website paths stored on records.',
|
|
172
|
-
],
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
id: 'extension-runtime-contract',
|
|
176
|
-
rules: [
|
|
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.',
|
|
179
|
-
'Load app packages with getPackages(["package-name"]) inside extension runtime code.',
|
|
180
|
-
'Prefer FormEditor/FormEditorLazy for direct table-backed forms when the form maps to metadata fields.',
|
|
181
|
-
'For long admin setup workflows, open CommonDrawer immediately and show loading/error/content inside it.',
|
|
182
|
-
'Use Widget with numeric enfyra_extension widget ids; pass safe reactive props/events and keep page-level mutation ownership in the page unless the widget intentionally owns the workflow.',
|
|
183
|
-
'Use useMenuNotificationRegistry for sidebar menu counts/dots and useAccountPanelRegistry count/badge fields for account panel notifications; register these from global extensions when they should update across the shell.',
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
};
|
|
188
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
const RESPONSE_FORMAT = 'json+columnar-v1';
|
|
2
|
-
const COLUMNAR_FORMAT = 'columnar-v1';
|
|
3
|
-
const COMPRESSION_STATS_FIELD = 'compressionStats';
|
|
4
|
-
|
|
5
|
-
function isPlainObject(value) {
|
|
6
|
-
if (!value || typeof value !== 'object') return false;
|
|
7
|
-
const proto = Object.getPrototypeOf(value);
|
|
8
|
-
return proto === Object.prototype || proto === null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function valueForColumn(record, column) {
|
|
12
|
-
return Object.prototype.hasOwnProperty.call(record, column) ? record[column] : null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function collectColumns(records) {
|
|
16
|
-
const columns = [];
|
|
17
|
-
const seen = new Set();
|
|
18
|
-
for (const record of records) {
|
|
19
|
-
for (const key of Object.keys(record)) {
|
|
20
|
-
if (seen.has(key)) continue;
|
|
21
|
-
seen.add(key);
|
|
22
|
-
columns.push(key);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return columns;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function toColumnar(value, seen = new WeakSet()) {
|
|
29
|
-
if (Array.isArray(value)) {
|
|
30
|
-
if (value.length > 0 && value.every(isPlainObject)) {
|
|
31
|
-
const columns = collectColumns(value);
|
|
32
|
-
return {
|
|
33
|
-
format: COLUMNAR_FORMAT,
|
|
34
|
-
columns,
|
|
35
|
-
rows: value.map((record) => columns.map((column) => toColumnar(valueForColumn(record, column), seen))),
|
|
36
|
-
rowCount: value.length,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
return value.map((item) => toColumnar(item, seen));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!isPlainObject(value)) return value;
|
|
43
|
-
if (seen.has(value)) return '[Circular]';
|
|
44
|
-
seen.add(value);
|
|
45
|
-
|
|
46
|
-
const output = {};
|
|
47
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
48
|
-
output[key] = toColumnar(entry, seen);
|
|
49
|
-
}
|
|
50
|
-
seen.delete(value);
|
|
51
|
-
return output;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function safeJsonStringify(value) {
|
|
55
|
-
const seen = new WeakSet();
|
|
56
|
-
return JSON.stringify(value, (_key, entry) => {
|
|
57
|
-
if (!entry || typeof entry !== 'object') return entry;
|
|
58
|
-
if (seen.has(entry)) return '[Circular]';
|
|
59
|
-
seen.add(entry);
|
|
60
|
-
return entry;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function estimateTokens(jsonText) {
|
|
65
|
-
if (!jsonText) return 0;
|
|
66
|
-
return Math.ceil(jsonText.length / 4);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function buildCompressionStats(originalPayload, candidatePayload, selectedPayload, applied) {
|
|
70
|
-
const originalTokens = estimateTokens(safeJsonStringify(originalPayload));
|
|
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;
|
|
78
|
-
const savedPercent = originalTokens > 0
|
|
79
|
-
? Number(((savedTokens / originalTokens) * 100).toFixed(2))
|
|
80
|
-
: 0;
|
|
81
|
-
return {
|
|
82
|
-
originalTokens,
|
|
83
|
-
compactTokens: responseTokens,
|
|
84
|
-
savedTokens,
|
|
85
|
-
savedPercent,
|
|
86
|
-
applied,
|
|
87
|
-
candidateCompactTokens: candidateTokens,
|
|
88
|
-
candidateSavedTokens,
|
|
89
|
-
candidateSavedPercent,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function attachCompressionStats(originalPayload, candidatePayload, selectedPayload, applied) {
|
|
94
|
-
if (
|
|
95
|
-
isPlainObject(selectedPayload)
|
|
96
|
-
&& selectedPayload.responseFormat === RESPONSE_FORMAT
|
|
97
|
-
&& selectedPayload[COMPRESSION_STATS_FIELD]
|
|
98
|
-
) {
|
|
99
|
-
return selectedPayload;
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
...selectedPayload,
|
|
103
|
-
[COMPRESSION_STATS_FIELD]: buildCompressionStats(originalPayload, candidatePayload, selectedPayload, applied),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function wrapPayload(payload) {
|
|
108
|
-
if (!isPlainObject(payload)) {
|
|
109
|
-
return {
|
|
110
|
-
responseFormat: RESPONSE_FORMAT,
|
|
111
|
-
value: payload,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
responseFormat: RESPONSE_FORMAT,
|
|
116
|
-
...payload,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function formatJsonPayload(payload) {
|
|
121
|
-
if (
|
|
122
|
-
isPlainObject(payload)
|
|
123
|
-
&& payload.responseFormat === RESPONSE_FORMAT
|
|
124
|
-
&& payload[COMPRESSION_STATS_FIELD]
|
|
125
|
-
) {
|
|
126
|
-
return payload;
|
|
127
|
-
}
|
|
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);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function jsonContent(payload, { pretty = false } = {}) {
|
|
139
|
-
return {
|
|
140
|
-
content: [{
|
|
141
|
-
type: 'text',
|
|
142
|
-
text: JSON.stringify(formatJsonPayload(payload), null, pretty ? 2 : 0),
|
|
143
|
-
}],
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function tryParseJson(text) {
|
|
148
|
-
if (typeof text !== 'string') return null;
|
|
149
|
-
const trimmed = text.trim();
|
|
150
|
-
if (!trimmed || !/^[\[{]/.test(trimmed)) return null;
|
|
151
|
-
try {
|
|
152
|
-
return JSON.parse(trimmed);
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function formatContentItem(item) {
|
|
159
|
-
if (!item || item.type !== 'text') return item;
|
|
160
|
-
const parsed = tryParseJson(item.text);
|
|
161
|
-
if (!parsed) return item;
|
|
162
|
-
return {
|
|
163
|
-
...item,
|
|
164
|
-
text: JSON.stringify(formatJsonPayload(parsed)),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function formatToolResult(result) {
|
|
169
|
-
if (!result || !Array.isArray(result.content)) return result;
|
|
170
|
-
return {
|
|
171
|
-
...result,
|
|
172
|
-
content: result.content.map(formatContentItem),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function installColumnarToolFormatter(server) {
|
|
177
|
-
const registerTool = server.tool.bind(server);
|
|
178
|
-
server.tool = (name, description, schema, handler) => {
|
|
179
|
-
if (typeof handler !== 'function') {
|
|
180
|
-
return registerTool(name, description, schema, handler);
|
|
181
|
-
}
|
|
182
|
-
return registerTool(name, description, schema, async (...args) => {
|
|
183
|
-
const result = await handler(...args);
|
|
184
|
-
return formatToolResult(result);
|
|
185
|
-
});
|
|
186
|
-
};
|
|
187
|
-
}
|
package/src/lib/route-guards.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
function getId(item) {
|
|
2
|
-
return item?.id ?? item?._id ?? null;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function sameId(a, b) {
|
|
6
|
-
return String(a) === String(b);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function validateMainTableRoutePath(tables, mainTableId, routePath) {
|
|
10
|
-
const table = tables.find((item) => sameId(getId(item), mainTableId));
|
|
11
|
-
if (!table) {
|
|
12
|
-
throw new Error(`Unknown table id "${mainTableId}"`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const canonicalPath = `/${table.name}`;
|
|
16
|
-
if (routePath !== canonicalPath) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`mainTableId is only allowed for canonical table route "${canonicalPath}". ` +
|
|
19
|
-
`Omit mainTableId for custom route "${routePath}" and query explicit repos in handlers/hooks.`
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return table;
|
|
24
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
export function normalizeMethodName(method) {
|
|
2
|
-
return String(method || '').trim().toUpperCase();
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function normalizeMethodNames(methods) {
|
|
6
|
-
return [...new Set((methods || []).map(normalizeMethodName).filter(Boolean))];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getRecordId(record) {
|
|
10
|
-
return record?.id ?? record?._id ?? null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getReferenceId(value) {
|
|
14
|
-
return typeof value === 'object' && value !== null ? getRecordId(value) : value;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function sameRecordId(a, b) {
|
|
18
|
-
if (a === null || a === undefined || b === null || b === undefined) return false;
|
|
19
|
-
return String(a) === String(b);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function resolveRoleByNameOrId(roles, { roleId, roleName } = {}) {
|
|
23
|
-
if (roleId && roleName) {
|
|
24
|
-
throw new Error('Provide roleId or roleName, not both.');
|
|
25
|
-
}
|
|
26
|
-
if (!roleId && !roleName) return null;
|
|
27
|
-
const normalizedRoleName = roleName ? String(roleName).trim().toLowerCase() : null;
|
|
28
|
-
const role = roles.find((item) => (
|
|
29
|
-
roleId
|
|
30
|
-
? sameRecordId(getRecordId(item), roleId)
|
|
31
|
-
: String(item?.name || '').trim().toLowerCase() === normalizedRoleName
|
|
32
|
-
));
|
|
33
|
-
if (!role) throw new Error(`Role not found: ${roleId || roleName}`);
|
|
34
|
-
return role;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function methodNamesFromRecords(methods, methodIdNameMap = {}) {
|
|
38
|
-
return normalizeMethodNames((methods || []).map((method) => (
|
|
39
|
-
method?.name || methodIdNameMap[String(getRecordId(method))] || method
|
|
40
|
-
)));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function routeAvailableMethodNames(route, methodIdNameMap = {}) {
|
|
44
|
-
return methodNamesFromRecords(route?.availableMethods || [], methodIdNameMap);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function routePublicMethodNames(route, methodIdNameMap = {}) {
|
|
48
|
-
return methodNamesFromRecords(route?.publicMethods || [], methodIdNameMap);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function permissionMethodNames(permission, methodIdNameMap = {}) {
|
|
52
|
-
return methodNamesFromRecords(permission?.methods || [], methodIdNameMap);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function validateMethodsForRoute(route, methods, methodMap, methodIdNameMap = {}) {
|
|
56
|
-
const normalizedMethods = normalizeMethodNames(methods);
|
|
57
|
-
const knownMethods = new Set(Object.keys(methodMap || {}).map(normalizeMethodName));
|
|
58
|
-
const unknown = normalizedMethods.filter((method) => !knownMethods.has(method));
|
|
59
|
-
if (unknown.length) {
|
|
60
|
-
throw new Error(`Unknown enfyra_method.name values: ${unknown.join(', ')}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const availableMethods = routeAvailableMethodNames(route, methodIdNameMap);
|
|
64
|
-
if (availableMethods.length) {
|
|
65
|
-
const unavailable = normalizedMethods.filter((method) => !availableMethods.includes(method));
|
|
66
|
-
if (unavailable.length) {
|
|
67
|
-
throw new Error(`Route ${route?.path} does not list methods as available: ${unavailable.join(', ')}. Available: ${availableMethods.join(', ')}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return normalizedMethods;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function sortedIdStrings(values) {
|
|
75
|
-
return [...new Set((values || []).map((value) => String(getReferenceId(value))).filter(Boolean))].sort();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function sameIdSet(a, b) {
|
|
79
|
-
const left = sortedIdStrings(a);
|
|
80
|
-
const right = sortedIdStrings(b);
|
|
81
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function routePermissionMatchesScope(permission, { roleId, allowedUserIds } = {}) {
|
|
85
|
-
const expectedUsers = sortedIdStrings(allowedUserIds);
|
|
86
|
-
const permissionUsers = sortedIdStrings(permission?.allowedUsers || []);
|
|
87
|
-
const permissionRoleId = getReferenceId(permission?.role);
|
|
88
|
-
|
|
89
|
-
if (roleId && !sameRecordId(permissionRoleId, roleId)) return false;
|
|
90
|
-
if (!roleId && permissionRoleId !== null && permissionRoleId !== undefined) return false;
|
|
91
|
-
return sameIdSet(permissionUsers, expectedUsers);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function findRoutePermission(routePermissions, routeId, scope) {
|
|
95
|
-
return (routePermissions || []).find((permission) => (
|
|
96
|
-
sameRecordId(getReferenceId(permission?.route), routeId)
|
|
97
|
-
&& routePermissionMatchesScope(permission, scope)
|
|
98
|
-
)) || null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function mergeMethodNames(existingMethods, requestedMethods, mode = 'merge') {
|
|
102
|
-
const requested = normalizeMethodNames(requestedMethods);
|
|
103
|
-
if (mode === 'replace') return requested;
|
|
104
|
-
return normalizeMethodNames([...normalizeMethodNames(existingMethods), ...requested]);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function summarizeRoutePermission(permission, methodIdNameMap = {}) {
|
|
108
|
-
return {
|
|
109
|
-
id: getRecordId(permission),
|
|
110
|
-
isEnabled: permission?.isEnabled !== false,
|
|
111
|
-
description: permission?.description || null,
|
|
112
|
-
route: permission?.route?.path || permission?.route || null,
|
|
113
|
-
role: permission?.role ? {
|
|
114
|
-
id: getReferenceId(permission.role),
|
|
115
|
-
name: permission.role.name || null,
|
|
116
|
-
} : null,
|
|
117
|
-
allowedUsers: (permission?.allowedUsers || []).map((user) => ({
|
|
118
|
-
id: getReferenceId(user),
|
|
119
|
-
email: user?.email || null,
|
|
120
|
-
})),
|
|
121
|
-
methods: permissionMethodNames(permission, methodIdNameMap),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function summarizeRouteAccess(route, routePermissions, methodIdNameMap = {}, expected = {}) {
|
|
126
|
-
const routeId = getRecordId(route);
|
|
127
|
-
const permissions = (routePermissions || [])
|
|
128
|
-
.filter((permission) => sameRecordId(getReferenceId(permission?.route), routeId))
|
|
129
|
-
.map((permission) => summarizeRoutePermission(permission, methodIdNameMap));
|
|
130
|
-
|
|
131
|
-
const expectedMethods = normalizeMethodNames(expected.methods || []);
|
|
132
|
-
const expectedRoleId = expected.roleId ? String(expected.roleId) : null;
|
|
133
|
-
const expectedAllowedUsers = sortedIdStrings(expected.allowedUserIds || []);
|
|
134
|
-
const hasExpectedScope = !!(expectedRoleId || expected.roleRequired || expected.allowedUserIds !== undefined);
|
|
135
|
-
const matchingPermissions = permissions.filter((permission) => {
|
|
136
|
-
if (!hasExpectedScope) return true;
|
|
137
|
-
if (expectedRoleId && String(permission.role?.id) !== expectedRoleId) return false;
|
|
138
|
-
if (!expectedRoleId && expected.roleRequired && permission.role) return false;
|
|
139
|
-
if (!sameIdSet(permission.allowedUsers.map((user) => user.id), expectedAllowedUsers)) return false;
|
|
140
|
-
return true;
|
|
141
|
-
});
|
|
142
|
-
const grantedMethods = normalizeMethodNames(matchingPermissions.flatMap((permission) => (
|
|
143
|
-
permission.isEnabled ? permission.methods : []
|
|
144
|
-
)));
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
id: routeId,
|
|
148
|
-
path: route?.path,
|
|
149
|
-
isEnabled: route?.isEnabled !== false,
|
|
150
|
-
availableMethods: routeAvailableMethodNames(route, methodIdNameMap),
|
|
151
|
-
publicMethods: routePublicMethodNames(route, methodIdNameMap),
|
|
152
|
-
skipRoleGuardMethods: methodNamesFromRecords(route?.skipRoleGuardMethods || [], methodIdNameMap),
|
|
153
|
-
permissions,
|
|
154
|
-
expected: expectedMethods.length ? {
|
|
155
|
-
methods: expectedMethods,
|
|
156
|
-
grantedMethods,
|
|
157
|
-
missingMethods: expectedMethods.filter((method) => !grantedMethods.includes(method)),
|
|
158
|
-
} : null,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_PREVIEW_CHARS = 1200;
|
|
7
|
-
const DEFAULT_INLINE_LIMIT = 1400;
|
|
8
|
-
const SOURCE_FIELD_NAMES = new Set([
|
|
9
|
-
'sourceCode',
|
|
10
|
-
'code',
|
|
11
|
-
'compiledCode',
|
|
12
|
-
'handlerScript',
|
|
13
|
-
'connectionHandlerScript',
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
function sha256(value) {
|
|
17
|
-
return createHash('sha256').update(String(value), 'utf8').digest('hex');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function safePart(value) {
|
|
21
|
-
const source = String(value || 'source').trim();
|
|
22
|
-
return source.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'source';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function extensionForField(fieldName) {
|
|
26
|
-
if (fieldName === 'code') return '.vue';
|
|
27
|
-
return '.js';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function writeSourceArtifact({ tableName, id, fieldName, source }) {
|
|
31
|
-
const hash = sha256(source);
|
|
32
|
-
const dir = join(tmpdir(), 'enfyra-mcp-sources');
|
|
33
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
34
|
-
const fileName = [
|
|
35
|
-
safePart(tableName),
|
|
36
|
-
safePart(id),
|
|
37
|
-
safePart(fieldName),
|
|
38
|
-
hash.slice(0, 12),
|
|
39
|
-
].join('-') + extensionForField(fieldName);
|
|
40
|
-
const path = join(dir, fileName);
|
|
41
|
-
writeFileSync(path, source, { mode: 0o600 });
|
|
42
|
-
return {
|
|
43
|
-
tmpFile: path,
|
|
44
|
-
length: source.length,
|
|
45
|
-
sha256: hash,
|
|
46
|
-
preview: source.length > DEFAULT_PREVIEW_CHARS
|
|
47
|
-
? `${source.slice(0, DEFAULT_PREVIEW_CHARS)}...`
|
|
48
|
-
: source,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function compactSourceField({ tableName, id, fieldName, source, alwaysWrite = false }) {
|
|
53
|
-
if (typeof source !== 'string') return source;
|
|
54
|
-
if (!alwaysWrite && source.length <= DEFAULT_INLINE_LIMIT) return source;
|
|
55
|
-
return writeSourceArtifact({ tableName, id, fieldName, source });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function compactSourceFields(value, { tableName, idField = 'id', alwaysWrite = false } = {}) {
|
|
59
|
-
if (Array.isArray(value)) {
|
|
60
|
-
return value.map((item) => compactSourceFields(item, { tableName, idField, alwaysWrite }));
|
|
61
|
-
}
|
|
62
|
-
if (!value || typeof value !== 'object') return value;
|
|
63
|
-
|
|
64
|
-
const recordId = value[idField] ?? value._id ?? value.id ?? 'record';
|
|
65
|
-
const out = {};
|
|
66
|
-
for (const [key, fieldValue] of Object.entries(value)) {
|
|
67
|
-
if (SOURCE_FIELD_NAMES.has(key) && typeof fieldValue === 'string') {
|
|
68
|
-
out[key] = compactSourceField({
|
|
69
|
-
tableName,
|
|
70
|
-
id: recordId,
|
|
71
|
-
fieldName: key,
|
|
72
|
-
source: fieldValue,
|
|
73
|
-
alwaysWrite,
|
|
74
|
-
});
|
|
75
|
-
} else if (fieldValue && typeof fieldValue === 'object') {
|
|
76
|
-
out[key] = compactSourceFields(fieldValue, { tableName, idField, alwaysWrite });
|
|
77
|
-
} else {
|
|
78
|
-
out[key] = fieldValue;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|