@enfyra/mcp-server 0.0.109 → 0.0.110
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
package/src/lib/auth.js
CHANGED
|
@@ -8,13 +8,25 @@ let accessToken = null;
|
|
|
8
8
|
let refreshToken = null;
|
|
9
9
|
let tokenExpiry = null; // expTime từ server (milliseconds)
|
|
10
10
|
let isRefreshing = false;
|
|
11
|
+
let exchangePromise = null;
|
|
11
12
|
|
|
12
13
|
// Config
|
|
13
14
|
let API_URL = 'http://localhost:3000/api';
|
|
14
15
|
let API_TOKEN = '';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const TOKEN_REFRESH_BUFFER = 20000;
|
|
18
|
+
|
|
19
|
+
function normalizeExpiry(expTime) {
|
|
20
|
+
if (expTime == null) return Infinity;
|
|
21
|
+
if (typeof expTime === 'number') return expTime < 1_000_000_000_000 ? expTime * 1000 : expTime;
|
|
22
|
+
if (typeof expTime === 'string' && expTime.trim()) {
|
|
23
|
+
const numeric = Number(expTime);
|
|
24
|
+
if (Number.isFinite(numeric)) return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
|
|
25
|
+
const parsed = Date.parse(expTime);
|
|
26
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
27
|
+
}
|
|
28
|
+
return Infinity;
|
|
29
|
+
}
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
32
|
* Initialize auth module with config
|
|
@@ -25,7 +37,7 @@ export function initAuth(apiUrl, apiToken = '') {
|
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/**
|
|
28
|
-
* Check if token needs refresh (expires within
|
|
40
|
+
* Check if token needs refresh (expires within the refresh buffer)
|
|
29
41
|
*/
|
|
30
42
|
export function needsRefresh() {
|
|
31
43
|
if (tokenExpiry === Infinity) return false;
|
|
@@ -60,27 +72,37 @@ export async function exchangeApiToken(url, apiToken) {
|
|
|
60
72
|
throw new Error('API token required');
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
const response = await fetch(`${apiUrl}/auth/token/exchange`, {
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: { 'Content-Type': 'application/json' },
|
|
67
|
-
body: JSON.stringify({ apiToken: token }),
|
|
68
|
-
});
|
|
75
|
+
if (exchangePromise) return exchangePromise;
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
exchangePromise = (async () => {
|
|
78
|
+
console.error('[Auth] Exchanging API token...');
|
|
79
|
+
const response = await fetch(`${apiUrl}/auth/token/exchange`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ apiToken: token }),
|
|
83
|
+
});
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tokenExpiry = data.expTime == null ? Infinity : data.expTime;
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`API token exchange failed: ${await response.text()}`);
|
|
87
|
+
}
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
accessToken = data.accessToken || data.access_token;
|
|
91
|
+
refreshToken = null;
|
|
92
|
+
tokenExpiry = normalizeExpiry(data.expTime ?? data.exp_time ?? data.expiresAt ?? data.expires_at);
|
|
93
|
+
|
|
94
|
+
const expiryLabel = tokenExpiry === Infinity
|
|
95
|
+
? 'no expiration'
|
|
96
|
+
: new Date(tokenExpiry).toISOString();
|
|
97
|
+
console.error(`[Auth] API token exchanged, access token expires at ${expiryLabel}`);
|
|
98
|
+
return accessToken;
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await exchangePromise;
|
|
103
|
+
} finally {
|
|
104
|
+
exchangePromise = null;
|
|
105
|
+
}
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
@@ -119,7 +141,7 @@ export async function refreshAccessToken(url) {
|
|
|
119
141
|
const data = await response.json();
|
|
120
142
|
accessToken = data.accessToken || data.access_token;
|
|
121
143
|
refreshToken = data.refreshToken || data.refresh_token;
|
|
122
|
-
tokenExpiry = data.expTime;
|
|
144
|
+
tokenExpiry = normalizeExpiry(data.expTime ?? data.exp_time ?? data.expiresAt ?? data.expires_at);
|
|
123
145
|
|
|
124
146
|
console.error(`[Auth] Token refreshed, expires at ${new Date(tokenExpiry).toISOString()}`);
|
|
125
147
|
return accessToken;
|
package/src/lib/mcp-examples.js
CHANGED
|
@@ -1539,12 +1539,15 @@ ensure_page_extension({
|
|
|
1539
1539
|
'Use enfyra_menu.label, not title.',
|
|
1540
1540
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1541
1541
|
'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
|
|
1542
|
+
'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI.',
|
|
1542
1543
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
1543
1544
|
'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
|
|
1544
1545
|
'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
|
|
1545
1546
|
'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
|
|
1546
1547
|
'Page extensions should be full-bleed by default and responsive from the first version.',
|
|
1547
1548
|
'The extension root is already inside Enfyra admin page main; do not add root-level page padding.',
|
|
1549
|
+
'Use theme tokens/classes for panels, rows, badges, borders, and text. Pair border/divide utilities with border-default or divide-[var(--border-default)] so light and dark themes stay consistent.',
|
|
1550
|
+
'Keep list selection local and fetch detail rows only; do not refetch the whole list after a row click unless the list data changed.',
|
|
1548
1551
|
'Page extension paths are admin app UI routes. Do not verify them with test_rest_endpoint against ENFYRA_API_URL unless inspect_route shows an API route with the same path.',
|
|
1549
1552
|
'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
|
|
1550
1553
|
],
|
|
@@ -27,7 +27,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
27
27
|
'- 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.',
|
|
28
28
|
'- 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.',
|
|
29
29
|
'- Inspect narrowly. Use `inspect_table`, `inspect_route`, and `inspect_feature` for the table/route/feature being changed instead of loading broad metadata.',
|
|
30
|
-
'- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category.',
|
|
30
|
+
'- Load examples only when needed. Before generating schemas, app connection code, OAuth, Socket.IO, handlers/hooks, flows, files, guards, permissions, or extensions, call `get_enfyra_examples` with the matching category. Before extension UI work, call `get_extension_theme_contract` and follow the app-shell/theme/security contract.',
|
|
31
31
|
'- For server scripts, call `discover_script_contexts` before writing or reviewing handler/hook/flow/websocket/GraphQL logic.',
|
|
32
32
|
'- 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/*`.',
|
|
33
33
|
'- Prefer the most specific business operation tool over raw metadata CRUD: `api_endpoint_workflow` for step-by-step endpoint work; `create_api_endpoint` only when a one-shot endpoint operation is clearly safe; route tools such as `enable_route`, `disable_route`, `delete_route`, `add_route_methods`, `public_route_methods`, and `private_route_methods`; `set_table_graphql`; `ensure_guard`; permission/rule tools; websocket tools; flow tools such as `ensure_manual_flow`, `ensure_scheduled_flow`, `choose_flow_step_tool`, and fixed-type flow step tools; and extension tools such as `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension`.',
|
|
@@ -286,6 +286,40 @@ function jsonText(payload) {
|
|
|
286
286
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function getExtensionThemeContract() {
|
|
290
|
+
return {
|
|
291
|
+
action: 'extension_theme_contract',
|
|
292
|
+
useBefore: [
|
|
293
|
+
'Call this before writing or reviewing Enfyra admin page, widget, or global extension UI.',
|
|
294
|
+
'Then call validate_extension_code or an ensure_*_extension tool before saving.',
|
|
295
|
+
],
|
|
296
|
+
layout: [
|
|
297
|
+
'The extension is already mounted inside the Enfyra app shell. Do not add a duplicate page header, centered page wrapper, or root-level page padding.',
|
|
298
|
+
'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
|
|
299
|
+
'Use usePageHeaderRegistry for the shell title and useHeaderActionRegistry/useSubHeaderActionRegistry for page actions.',
|
|
300
|
+
],
|
|
301
|
+
theme: [
|
|
302
|
+
'Use eApp theme tokens/classes, not hardcoded light or dark colors.',
|
|
303
|
+
'Use bg-default, bg-muted, text-highlighted, text-muted, border-default when available.',
|
|
304
|
+
'When using arbitrary CSS vars, prefer var(--surface-default), var(--surface-muted), var(--border-default), var(--text-primary), var(--text-secondary), and var(--text-tertiary).',
|
|
305
|
+
'Never use bare border/divide-y for panels or rows: pair them with border-default or divide-[var(--border-default)]. Avoid border-black, black, slate-only, gray-only, and dark-only palettes.',
|
|
306
|
+
'Status colors must remain readable in both themes; warning badges need high contrast text and a visible but not harsh border/background.',
|
|
307
|
+
],
|
|
308
|
+
interaction: [
|
|
309
|
+
'Every mutating button needs pending/disabled state, success/error feedback, and must close or update its modal when the operation completes.',
|
|
310
|
+
'Do not refetch broad lists after selecting one row. Keep local selection state and fetch only the detail or mutation result needed.',
|
|
311
|
+
'Use bounded pagination for operational lists. Do not replace pagination with arbitrary fixed caps such as 30 or 50.',
|
|
312
|
+
'Customer-facing toasts must describe the operation. Do not surface raw job ids, flow ids, or worker ids.',
|
|
313
|
+
],
|
|
314
|
+
security: [
|
|
315
|
+
'Decide route permission, owner scope, and field exposure before writing UI or backend logic.',
|
|
316
|
+
'UI checks are only guidance; handlers/hooks must independently enforce owner/root-admin authorization.',
|
|
317
|
+
'Use the most specific business route or MCP tool. Do not write directly to raw tables when a domain route exists.',
|
|
318
|
+
],
|
|
319
|
+
compactExample: '<template><section class="min-h-full w-full space-y-4"><div class="rounded-lg border border-default bg-default"><div class="border-b border-default px-4 py-3"><h2 class="text-base font-semibold text-highlighted">Title</h2><p class="text-sm text-muted">Short operational context.</p></div><div class="divide-y divide-[var(--border-default)]"><button class="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-muted/60"><span class="text-sm font-medium text-highlighted">Row</span><span class="text-sm text-muted">Open</span></button></div></div></section></template>',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
289
323
|
function parseJsonObjectArg(name, value, fallback = {}) {
|
|
290
324
|
if (value === undefined || value === null || value === '') return fallback;
|
|
291
325
|
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
@@ -979,6 +1013,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
979
1013
|
[
|
|
980
1014
|
'Validate Enfyra admin extension code before saving it to enfyra_extension.',
|
|
981
1015
|
'Use this for Vue SFC page/widget/global extension code. It calls /enfyra_extension/preview and does not save anything.',
|
|
1016
|
+
'Call get_extension_theme_contract first when generating or reviewing UI.',
|
|
982
1017
|
].join(' '),
|
|
983
1018
|
{
|
|
984
1019
|
code: z.string().describe('Vue SFC or compiled extension bundle code.'),
|
|
@@ -990,6 +1025,13 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
990
1025
|
}),
|
|
991
1026
|
);
|
|
992
1027
|
|
|
1028
|
+
server.tool(
|
|
1029
|
+
'get_extension_theme_contract',
|
|
1030
|
+
'Return the concise Enfyra admin extension UI/theme/security contract. Call before writing or reviewing extension UI.',
|
|
1031
|
+
{},
|
|
1032
|
+
async () => jsonText(getExtensionThemeContract()),
|
|
1033
|
+
);
|
|
1034
|
+
|
|
993
1035
|
server.tool(
|
|
994
1036
|
'set_table_graphql',
|
|
995
1037
|
'Business operation: enable or disable GraphQL for one table through enfyra_graphql, then reload GraphQL. REST route methods do not control GraphQL.',
|
|
@@ -1824,7 +1866,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1824
1866
|
|
|
1825
1867
|
server.tool(
|
|
1826
1868
|
'ensure_page_extension',
|
|
1827
|
-
'Business operation: create or update one page extension attached to an existing menu. Validates extension code before save.',
|
|
1869
|
+
'Business operation: create or update one page extension attached to an existing menu. Validates extension code before save. Call get_extension_theme_contract first for UI work.',
|
|
1828
1870
|
{
|
|
1829
1871
|
name: z.string().describe('Extension unique name.'),
|
|
1830
1872
|
code: z.string().describe('Vue SFC extension code.'),
|
|
@@ -1841,7 +1883,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1841
1883
|
|
|
1842
1884
|
server.tool(
|
|
1843
1885
|
'ensure_global_extension',
|
|
1844
|
-
'Business operation: create or update one global shell extension. Validates extension code before save and rejects menu coupling.',
|
|
1886
|
+
'Business operation: create or update one global shell extension. Validates extension code before save and rejects menu coupling. Call get_extension_theme_contract first for UI work.',
|
|
1845
1887
|
{
|
|
1846
1888
|
name: z.string().describe('Extension unique name.'),
|
|
1847
1889
|
code: z.string().describe('Vue SFC extension code.'),
|
|
@@ -1857,7 +1899,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
1857
1899
|
|
|
1858
1900
|
server.tool(
|
|
1859
1901
|
'ensure_widget_extension',
|
|
1860
|
-
'Business operation: create or update one widget extension. Validates extension code before save and rejects menu coupling.',
|
|
1902
|
+
'Business operation: create or update one widget extension. Validates extension code before save and rejects menu coupling. Call get_extension_theme_contract first for UI work.',
|
|
1861
1903
|
{
|
|
1862
1904
|
name: z.string().describe('Extension unique name.'),
|
|
1863
1905
|
code: z.string().describe('Vue SFC extension code.'),
|