@enfyra/mcp-server 0.1.52 → 0.1.54
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 +12 -0
- package/dist/lib/mcp-instructions.js +1 -0
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/metadata-client.d.ts +8 -0
- package/dist/lib/metadata-client.js +69 -0
- package/dist/lib/metadata-client.js.map +1 -0
- package/dist/lib/platform-operation-tools.d.ts +1 -0
- package/dist/lib/platform-operation-tools.js +119 -21
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +5 -2
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/runtime-cache-socket.d.ts +2 -2
- package/dist/lib/runtime-cache-socket.js +67 -21
- package/dist/lib/runtime-cache-socket.js.map +1 -1
- package/dist/lib/runtime-zone-tools.d.ts +1 -1
- package/dist/lib/runtime-zone-tools.js +17 -24
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/table-tools.d.ts +4 -3
- package/dist/lib/table-tools.js +59 -43
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/toolset-filter.js +1 -0
- package/dist/lib/toolset-filter.js.map +1 -1
- package/dist/lib/types.d.ts +12 -0
- package/dist/mcp-server-entry.js +91 -108
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -246,6 +246,18 @@ Without a target flag, interactive mode asks which client to configure. Non-inte
|
|
|
246
246
|
|
|
247
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.
|
|
248
248
|
|
|
249
|
+
## Metadata Contract
|
|
250
|
+
|
|
251
|
+
The MCP server uses Enfyra metadata lazily:
|
|
252
|
+
|
|
253
|
+
- `GET /metadata` returns only `dbType` and `enfyraVersion`.
|
|
254
|
+
- `GET /metadata/{tableName}` returns one permission-projected table schema.
|
|
255
|
+
- `get_all_tables` and normal discovery use a lightweight `enfyra_table` catalog.
|
|
256
|
+
- `get_table_metadata`, `inspect_table`, mutation validation, and schema operations fetch only the table schemas they need.
|
|
257
|
+
- Explicit broad schema searches fan out to per-table metadata with bounded concurrency and reuse the MCP runtime cache.
|
|
258
|
+
|
|
259
|
+
Use `get_all_tables` for table names, then `get_table_metadata` or `inspect_table` for columns and relations. `get_all_metadata(includeFull=true)` is an explicit broad operation and should not be used as startup discovery.
|
|
260
|
+
|
|
249
261
|
## Common Examples
|
|
250
262
|
|
|
251
263
|
Use `get_enfyra_examples` from the MCP tool list when asking an LLM to generate implementation patterns. It returns focused examples for:
|
|
@@ -21,6 +21,7 @@ export function buildMcpServerInstructions(apiBaseUrl, options = {}) {
|
|
|
21
21
|
'- Primary success criterion: one-shot done. A capable baseline model (GLM-5-Turbo or stronger) should finish a fully specified, non-destructive task in one task turn, using the smallest necessary sequence of MCP calls and without asking the user to relay intermediate plans or retry avoidable failures.',
|
|
22
22
|
'- Primary efficiency criterion: lazy-load by domain. Startup instructions are only a router; load required knowledge, examples, live metadata, script contexts, and theme contracts only when that exact task needs them. Never load broad discovery, full knowledge, or a full reference merely as a precaution.',
|
|
23
23
|
'- For target sanity checks, call `get_enfyra_api_context`; do not load broad metadata only to confirm the API base.',
|
|
24
|
+
'- Metadata is lazy: `GET /metadata` is runtime context only, while table schema comes from `GET /metadata/:name`. Use `get_all_tables` for a lightweight catalog and `get_table_metadata` or `inspect_table` for one schema; do not preload all table schemas.',
|
|
24
25
|
'- When the goal or tool path is ambiguous, call `discover_enfyra_workflows` with intent/risk/surface; follow its `primaryPath` and `avoidTools`. Skip it when the exact focused operation is already clear.',
|
|
25
26
|
'- Inspect only the target: use `inspect_table`, `inspect_route`, `inspect_feature`, `search_admin_extensions`, or `search_runtime_zone`. Do not run broad discovery after the target is known.',
|
|
26
27
|
'- Load examples with `get_enfyra_examples` only for an unfamiliar pattern. For extension UI, load `get_extension_theme_contract`; call build_extension_ui only when the requested UI needs those contracts.',
|
|
@@ -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;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,qBAAqB;QACrB,gTAAgT;QAChT,mTAAmT;QACnT,qHAAqH;QACrH,6MAA6M;QAC7M,gMAAgM;QAChM,6MAA6M;QAC7M,4EAA4E;QAC5E,2NAA2N;QAC3N,kTAAkT;QAClT,6LAA6L;QAC7L,yLAAyL;QACzL,mPAAmP;KACpP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,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,qBAAqB;QACrB,gTAAgT;QAChT,mTAAmT;QACnT,qHAAqH;QACrH,gQAAgQ;QAChQ,6MAA6M;QAC7M,gMAAgM;QAChM,6MAA6M;QAC7M,4EAA4E;QAC5E,2NAA2N;QAC3N,kTAAkT;QAClT,6LAA6L;QAC7L,yLAAyL;QACzL,mPAAmP;KACpP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MetadataContext, MetadataTableCatalogEntry, UnknownRecord } from "./types.js";
|
|
2
|
+
export declare function unwrapTableMetadata(result: unknown): UnknownRecord | null;
|
|
3
|
+
export declare function fetchMetadataContext(apiUrl: string): Promise<MetadataContext>;
|
|
4
|
+
export declare function fetchTableMetadata(apiUrl: string, tableName: string): Promise<UnknownRecord>;
|
|
5
|
+
export declare function fetchTableCatalog(apiUrl: string): Promise<MetadataTableCatalogEntry[]>;
|
|
6
|
+
export declare function fetchTableMetadataByRef(apiUrl: string, tableRef: unknown): Promise<UnknownRecord>;
|
|
7
|
+
export declare function resolveTableCatalogEntry(tables: MetadataTableCatalogEntry[], tableRef: unknown): MetadataTableCatalogEntry | null;
|
|
8
|
+
export declare function fetchMetadataTables(apiUrl: string, catalog: MetadataTableCatalogEntry[], concurrency?: number): Promise<UnknownRecord[]>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fetchAPI } from "./fetch.js";
|
|
2
|
+
const TABLE_CATALOG_FIELDS = "id,name,alias,description,isSingleRecord";
|
|
3
|
+
const DEFAULT_METADATA_CONCURRENCY = 4;
|
|
4
|
+
function unwrapData(result) {
|
|
5
|
+
if (Array.isArray(result))
|
|
6
|
+
return result;
|
|
7
|
+
if (result && typeof result === "object" && Array.isArray(result.data)) {
|
|
8
|
+
return result.data;
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
export function unwrapTableMetadata(result) {
|
|
13
|
+
if (!result || typeof result !== "object")
|
|
14
|
+
return null;
|
|
15
|
+
const record = result;
|
|
16
|
+
const data = record.data;
|
|
17
|
+
if (data && typeof data === "object" && !Array.isArray(data))
|
|
18
|
+
return data;
|
|
19
|
+
const table = record.table;
|
|
20
|
+
if (table && typeof table === "object" && !Array.isArray(table))
|
|
21
|
+
return table;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export async function fetchMetadataContext(apiUrl) {
|
|
25
|
+
const result = await fetchAPI(apiUrl, "/metadata");
|
|
26
|
+
return {
|
|
27
|
+
dbType: result?.dbType ?? null,
|
|
28
|
+
enfyraVersion: result?.enfyraVersion ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function fetchTableMetadata(apiUrl, tableName) {
|
|
32
|
+
const result = await fetchAPI(apiUrl, `/metadata/${encodeURIComponent(tableName)}`);
|
|
33
|
+
const table = unwrapTableMetadata(result);
|
|
34
|
+
if (!table)
|
|
35
|
+
throw new Error(`Metadata for table "${tableName}" did not contain a table object.`);
|
|
36
|
+
return table;
|
|
37
|
+
}
|
|
38
|
+
export async function fetchTableCatalog(apiUrl) {
|
|
39
|
+
const result = await fetchAPI(apiUrl, `/enfyra_table?fields=${encodeURIComponent(TABLE_CATALOG_FIELDS)}&limit=0&sort=name`);
|
|
40
|
+
return unwrapData(result)
|
|
41
|
+
.filter((item) => Boolean(item && typeof item === "object" && typeof item.name === "string"));
|
|
42
|
+
}
|
|
43
|
+
export async function fetchTableMetadataByRef(apiUrl, tableRef) {
|
|
44
|
+
const catalog = await fetchTableCatalog(apiUrl);
|
|
45
|
+
const entry = resolveTableCatalogEntry(catalog, tableRef);
|
|
46
|
+
if (!entry)
|
|
47
|
+
throw new Error(`Table not found: ${String(tableRef)}`);
|
|
48
|
+
return fetchTableMetadata(apiUrl, entry.name);
|
|
49
|
+
}
|
|
50
|
+
export function resolveTableCatalogEntry(tables, tableRef) {
|
|
51
|
+
const normalizedRef = String(tableRef ?? "");
|
|
52
|
+
return tables.find((table) => (String(table.id ?? table._id ?? "") === normalizedRef
|
|
53
|
+
|| table.name === normalizedRef
|
|
54
|
+
|| table.alias === normalizedRef)) ?? null;
|
|
55
|
+
}
|
|
56
|
+
export async function fetchMetadataTables(apiUrl, catalog, concurrency = DEFAULT_METADATA_CONCURRENCY) {
|
|
57
|
+
const tables = new Array(catalog.length);
|
|
58
|
+
let nextIndex = 0;
|
|
59
|
+
const workerCount = Math.min(Math.max(1, concurrency), catalog.length);
|
|
60
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
61
|
+
while (nextIndex < catalog.length) {
|
|
62
|
+
const index = nextIndex;
|
|
63
|
+
nextIndex += 1;
|
|
64
|
+
tables[index] = await fetchTableMetadata(apiUrl, catalog[index].name);
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
return tables;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=metadata-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-client.js","sourceRoot":"","sources":["../../src/lib/metadata-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,oBAAoB,GAAG,0CAA0C,CAAC;AACxE,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAEvC,SAAS,UAAU,CAAC,MAAe;IACjC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACzC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAE,MAAwB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1F,OAAQ,MAAwB,CAAC,IAAiB,CAAC;IACrD,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAe;IACjD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACvD,MAAM,MAAM,GAAG,MAAuB,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAqB,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAsB,CAAC;IAC/F,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAc;IACvD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACnD,OAAO;QACL,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI;QAC9B,aAAa,EAAE,MAAM,EAAE,aAAa,IAAI,IAAI;KAC7C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,SAAiB;IACxE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,aAAa,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,SAAS,mCAAmC,CAAC,CAAC;IACjG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAc;IACpD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAC3B,MAAM,EACN,wBAAwB,kBAAkB,CAAC,oBAAoB,CAAC,oBAAoB,CACrF,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,CAAC;SACtB,MAAM,CAAC,CAAC,IAAI,EAAqC,EAAE,CAAC,OAAO,CAC1D,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAQ,IAAkC,CAAC,IAAI,KAAK,QAAQ,CACjG,CAAC,CAAC;AACP,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,QAAiB;IAC7E,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,wBAAwB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACpE,OAAO,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAAmC,EACnC,QAAiB;IAEjB,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC7C,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC5B,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,aAAa;WAClD,KAAK,CAAC,IAAI,KAAK,aAAa;WAC5B,KAAK,CAAC,KAAK,KAAK,aAAa,CACjC,CAAC,IAAI,IAAI,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAc,EACd,OAAoC,EACpC,WAAW,GAAG,4BAA4B;IAE1C,MAAM,MAAM,GAAoB,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,KAAK,IAAI,EAAE;QAC/D,OAAO,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QACxE,CAAC;IACH,CAAC,CAAC,CAAC,CAAC;IACJ,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { fetchAPI } from './fetch.js';
|
|
4
|
+
import { fetchTableCatalog, fetchTableMetadataByRef, resolveTableCatalogEntry } from './metadata-client.js';
|
|
4
5
|
import { validatePortableScriptSource, validateScriptSourceIfPresent } from './mutation-guards.js';
|
|
5
6
|
import { assertDynamicCodeKnowledgeAck, assertDynamicCodeKnowledgeAckIf, assertExtensionKnowledgeAck, assertGlobalRulesAck, dynamicCodeKnowledgeAckParam, extensionKnowledgeAckParam, globalRulesAckParam, } from './required-knowledge.js';
|
|
6
7
|
const AUTO_INJECTED_EXTENSION_COMPONENT_TAGS = [
|
|
@@ -849,12 +850,34 @@ function toPascalIdentifier(value, fallback = 'Items') {
|
|
|
849
850
|
export function buildExtensionApiUsageSnippet(input = {}) {
|
|
850
851
|
const resource = String(input.resource || input.name || 'items');
|
|
851
852
|
const pascal = toPascalIdentifier(resource, 'Items');
|
|
852
|
-
const
|
|
853
|
-
const
|
|
853
|
+
const operation = String(input.operation || input.mode || input.intent || '').toLowerCase() || String(input.method || 'GET').toLowerCase();
|
|
854
|
+
const normalizedOperation = {
|
|
855
|
+
get: 'list',
|
|
856
|
+
read: 'list',
|
|
857
|
+
load: 'list',
|
|
858
|
+
post: 'create',
|
|
859
|
+
patch: 'update',
|
|
860
|
+
put: 'update',
|
|
861
|
+
del: 'delete',
|
|
862
|
+
remove: 'delete',
|
|
863
|
+
destroy: 'delete',
|
|
864
|
+
}[operation] || operation;
|
|
865
|
+
const defaultMethodByOperation = {
|
|
866
|
+
list: 'GET',
|
|
867
|
+
find_one: 'GET',
|
|
868
|
+
create: 'POST',
|
|
869
|
+
update: 'PATCH',
|
|
870
|
+
delete: 'DELETE',
|
|
871
|
+
batch_update: 'PATCH',
|
|
872
|
+
batch_delete: 'DELETE',
|
|
873
|
+
};
|
|
874
|
+
const method = String(input.method || defaultMethodByOperation[normalizedOperation] || 'GET').toUpperCase();
|
|
875
|
+
const rawPath = String(input.path || `/${resource}`);
|
|
876
|
+
const path = rawPath.replace(/\/:id\/?$/, '');
|
|
854
877
|
const responseName = input.responseName || `${resource}Response`;
|
|
855
878
|
const pendingName = input.pendingName || `${resource}Pending`;
|
|
856
879
|
const errorName = input.errorName || `${resource}Error`;
|
|
857
|
-
const executeName = input.executeName || (method === 'GET' ? `load${pascal}` : `${
|
|
880
|
+
const executeName = input.executeName || (method === 'GET' ? `load${pascal}` : `${normalizedOperation.replace(/(^|_)([a-z])/g, (_m, _p, ch) => ch.toUpperCase()).replace(/^./, (ch) => ch.toLowerCase())}${pascal}Api`);
|
|
858
881
|
const refreshName = input.refreshName || `refresh${pascal}`;
|
|
859
882
|
const options = [];
|
|
860
883
|
if (method !== 'GET')
|
|
@@ -878,6 +901,60 @@ export function buildExtensionApiUsageSnippet(input = {}) {
|
|
|
878
901
|
lines.push(`onMounted(() => { ${executeName}(); });`);
|
|
879
902
|
}
|
|
880
903
|
}
|
|
904
|
+
else if (normalizedOperation === 'create') {
|
|
905
|
+
const handlerName = input.handlerName || `create${pascal.replace(/s$/, '')}`;
|
|
906
|
+
const payloadName = input.payloadName || 'payload';
|
|
907
|
+
lines.push(...[
|
|
908
|
+
'',
|
|
909
|
+
`async function ${handlerName}(${payloadName}) {`,
|
|
910
|
+
` const response = await ${executeName}({ body: ${payloadName} });`,
|
|
911
|
+
' if (!response) return null;',
|
|
912
|
+
' return response;',
|
|
913
|
+
'}',
|
|
914
|
+
]);
|
|
915
|
+
}
|
|
916
|
+
else if (normalizedOperation === 'update') {
|
|
917
|
+
const handlerName = input.handlerName || `update${pascal.replace(/s$/, '')}`;
|
|
918
|
+
const recordName = input.recordName || 'record';
|
|
919
|
+
const bodyName = input.bodyName || 'body';
|
|
920
|
+
const idExpression = input.idExpression || `${recordName}.id`;
|
|
921
|
+
const bodyArg = bodyName === 'body' ? 'body' : `body: ${bodyName}`;
|
|
922
|
+
lines.push(...[
|
|
923
|
+
'',
|
|
924
|
+
`async function ${handlerName}(${recordName}, ${bodyName}) {`,
|
|
925
|
+
` const response = await ${executeName}({ id: ${idExpression}, ${bodyArg} });`,
|
|
926
|
+
' if (!response) return null;',
|
|
927
|
+
' return response;',
|
|
928
|
+
'}',
|
|
929
|
+
]);
|
|
930
|
+
}
|
|
931
|
+
else if (normalizedOperation === 'delete') {
|
|
932
|
+
const handlerName = input.handlerName || `delete${pascal.replace(/s$/, '')}`;
|
|
933
|
+
const recordName = input.recordName || 'record';
|
|
934
|
+
const idExpression = input.idExpression || `${recordName}.id`;
|
|
935
|
+
lines.push(...[
|
|
936
|
+
'',
|
|
937
|
+
`async function ${handlerName}(${recordName}) {`,
|
|
938
|
+
` const response = await ${executeName}({ id: ${idExpression} });`,
|
|
939
|
+
' if (!response) return null;',
|
|
940
|
+
' return response;',
|
|
941
|
+
'}',
|
|
942
|
+
]);
|
|
943
|
+
}
|
|
944
|
+
else if (normalizedOperation === 'batch_update' || normalizedOperation === 'batch_delete') {
|
|
945
|
+
const handlerName = input.handlerName || `${normalizedOperation === 'batch_update' ? 'update' : 'delete'}${pascal}Batch`;
|
|
946
|
+
const idsName = input.idsName || 'ids';
|
|
947
|
+
const bodyName = input.bodyName || 'body';
|
|
948
|
+
const args = normalizedOperation === 'batch_update' ? `{ ids: ${idsName}, body: ${bodyName} }` : `{ ids: ${idsName} }`;
|
|
949
|
+
lines.push(...[
|
|
950
|
+
'',
|
|
951
|
+
`async function ${handlerName}(${normalizedOperation === 'batch_update' ? `${idsName}, ${bodyName}` : idsName}) {`,
|
|
952
|
+
` const response = await ${executeName}(${args});`,
|
|
953
|
+
' if (!response) return null;',
|
|
954
|
+
' return response;',
|
|
955
|
+
'}',
|
|
956
|
+
]);
|
|
957
|
+
}
|
|
881
958
|
else {
|
|
882
959
|
const handlerName = input.handlerName || `${method.toLowerCase()}${pascal}Record`;
|
|
883
960
|
lines.push(...[
|
|
@@ -891,9 +968,11 @@ export function buildExtensionApiUsageSnippet(input = {}) {
|
|
|
891
968
|
}
|
|
892
969
|
return {
|
|
893
970
|
action: 'extension_api_usage_built',
|
|
971
|
+
operation: normalizedOperation,
|
|
894
972
|
snippet: lines.join('\n'),
|
|
895
973
|
contract: [
|
|
896
974
|
'useApi returns refs plus execute/refresh; it does not auto-run.',
|
|
975
|
+
'The useApi path is the base route string or a () => string getter; do not pass computed refs and do not put :id placeholders in the path.',
|
|
897
976
|
'Pass query/body as objects or computed objects, not JSON.stringify strings.',
|
|
898
977
|
'Read normal list rows from data.value?.data or from the direct execute() response.',
|
|
899
978
|
'For mutations, call execute({ body }), execute({ id, body }), execute({ id }), or execute({ ids }) from a user action.',
|
|
@@ -1764,19 +1843,6 @@ async function patchExtensionCode(apiUrl, { id, name, search, replace, searchMod
|
|
|
1764
1843
|
validation: result.validation,
|
|
1765
1844
|
};
|
|
1766
1845
|
}
|
|
1767
|
-
function normalizeMetadataTables(metadata) {
|
|
1768
|
-
const tables = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
|
|
1769
|
-
return Array.isArray(tables) ? tables : Object.values(tables || {});
|
|
1770
|
-
}
|
|
1771
|
-
async function getMetadataTables(apiUrl) {
|
|
1772
|
-
return normalizeMetadataTables(await fetchAPI(apiUrl, '/metadata'));
|
|
1773
|
-
}
|
|
1774
|
-
function resolveTable(tables, tableName) {
|
|
1775
|
-
const table = tables.find((item) => item?.name === tableName || item?.alias === tableName || sameId(getId(item), tableName));
|
|
1776
|
-
if (!table)
|
|
1777
|
-
throw new Error(`Table not found: ${tableName}`);
|
|
1778
|
-
return table;
|
|
1779
|
-
}
|
|
1780
1846
|
function resolveColumn(table, columnName) {
|
|
1781
1847
|
const column = (table.columns || []).find((item) => item?.name === columnName || sameId(getId(item), columnName));
|
|
1782
1848
|
if (!column)
|
|
@@ -2860,6 +2926,36 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2860
2926
|
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|
|
2861
2927
|
return jsonText(buildExtensionUiSnippet(kind, input));
|
|
2862
2928
|
});
|
|
2929
|
+
server.tool('build_extension_api_usage', [
|
|
2930
|
+
'Generate a contract-safe useApi snippet for Enfyra admin extensions.',
|
|
2931
|
+
'Use this instead of writing useApi calls from memory so route paths, execute({ id, body }), query/body objects, and mutation handlers follow the app composable contract.',
|
|
2932
|
+
'The tool returns code only; apply it with patch_extension_code or update_extension_code and then validate/save normally.',
|
|
2933
|
+
].join(' '), {
|
|
2934
|
+
operation: z.enum(['list', 'find_one', 'create', 'update', 'delete', 'batch_update', 'batch_delete']).default('list').describe('API usage pattern to generate. Reads use the base route with query objects; mutations append ids through execute options.'),
|
|
2935
|
+
resource: z.string().default('items').describe('Resource variable base name, e.g. notes, projects, messages.'),
|
|
2936
|
+
path: z.string().optional().describe('Base API route path such as /notes. Do not include /:id; the builder strips a trailing /:id if provided.'),
|
|
2937
|
+
queryExpression: z.string().optional().describe('Raw Vue expression for query object/computed. Do not JSON.stringify.'),
|
|
2938
|
+
bodyExpression: z.string().optional().describe('Raw Vue expression for default body object/computed when useful. Do not JSON.stringify.'),
|
|
2939
|
+
errorContext: z.string().optional().describe('Safe error context label for useApi error reporting.'),
|
|
2940
|
+
responseName: z.string().optional().describe('Optional data ref variable name.'),
|
|
2941
|
+
pendingName: z.string().optional().describe('Optional pending ref variable name.'),
|
|
2942
|
+
errorName: z.string().optional().describe('Optional error ref variable name.'),
|
|
2943
|
+
executeName: z.string().optional().describe('Optional execute alias name.'),
|
|
2944
|
+
refreshName: z.string().optional().describe('Optional refresh alias name.'),
|
|
2945
|
+
rowsName: z.string().optional().describe('Optional computed rows variable for list/find_one operations.'),
|
|
2946
|
+
handlerName: z.string().optional().describe('Optional generated handler function name for mutations.'),
|
|
2947
|
+
recordName: z.string().optional().describe('Record parameter name for update/delete handlers.'),
|
|
2948
|
+
payloadName: z.string().optional().describe('Payload parameter name for create handlers.'),
|
|
2949
|
+
bodyName: z.string().optional().describe('Body parameter name for update/batch_update handlers.'),
|
|
2950
|
+
idsName: z.string().optional().describe('Ids parameter name for batch handlers.'),
|
|
2951
|
+
idExpression: z.string().optional().describe('Raw id expression for update/delete handlers. Defaults to record.id.'),
|
|
2952
|
+
autoLoad: z.boolean().optional().default(true).describe('For reads, generate onMounted(() => execute()).'),
|
|
2953
|
+
onErrorExpression: z.string().optional().describe('Raw onError handler expression when custom handling is needed.'),
|
|
2954
|
+
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2955
|
+
}, async ({ extensionKnowledgeAckKey, ...input }) => {
|
|
2956
|
+
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|
|
2957
|
+
return jsonText(buildExtensionApiUsageSnippet(input));
|
|
2958
|
+
});
|
|
2863
2959
|
server.tool('build_extension_drawer', [
|
|
2864
2960
|
'Generate a contract-safe CommonDrawer Vue snippet for Enfyra admin extensions.',
|
|
2865
2961
|
'Use this before writing or patching drawer/editing workflows so the model does not have to remember CommonDrawer slots, footer action props, full-width fields, or button type rules.',
|
|
@@ -3079,7 +3175,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3079
3175
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
3080
3176
|
}, async ({ tableName, isEnabled, globalRulesAckKey }) => {
|
|
3081
3177
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
3082
|
-
const
|
|
3178
|
+
const catalog = await fetchTableCatalog(ENFYRA_API_URL);
|
|
3179
|
+
const table = resolveTableCatalogEntry(catalog, tableName);
|
|
3180
|
+
if (!table)
|
|
3181
|
+
throw new Error(`Table not found: ${tableName}`);
|
|
3083
3182
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_graphql', { table: { id: { _eq: getId(table) } } }, 'id,_id,table.id,isEnabled');
|
|
3084
3183
|
const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_graphql', existing, {
|
|
3085
3184
|
table: { id: getId(table) },
|
|
@@ -3367,7 +3466,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3367
3466
|
globalRulesAckKey: globalRulesAckParam(z),
|
|
3368
3467
|
}, async ({ tableName, columnName, ruleType, value, message, description, isEnabled, globalRulesAckKey }) => {
|
|
3369
3468
|
assertGlobalRulesAck(globalRulesAckKey);
|
|
3370
|
-
const table =
|
|
3469
|
+
const table = await fetchTableMetadataByRef(ENFYRA_API_URL, tableName);
|
|
3371
3470
|
const column = resolveColumn(table, columnName);
|
|
3372
3471
|
const existing = await findRecord(ENFYRA_API_URL, 'enfyra_column_rule', {
|
|
3373
3472
|
column: { id: { _eq: getId(column) } },
|
|
@@ -3407,11 +3506,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
3407
3506
|
if (!!columnName === !!relationName)
|
|
3408
3507
|
throw new Error('Provide exactly one of columnName or relationName.');
|
|
3409
3508
|
assertOneScope({ roleId, roleName, allowedUserIds });
|
|
3410
|
-
const [
|
|
3411
|
-
|
|
3509
|
+
const [table, role] = await Promise.all([
|
|
3510
|
+
fetchTableMetadataByRef(ENFYRA_API_URL, tableName),
|
|
3412
3511
|
resolveRole(ENFYRA_API_URL, { roleId, roleName }),
|
|
3413
3512
|
]);
|
|
3414
|
-
const table = resolveTable(tables, tableName);
|
|
3415
3513
|
const field = columnName ? resolveColumn(table, columnName) : resolveRelation(table, relationName);
|
|
3416
3514
|
const filter = {
|
|
3417
3515
|
action: { _eq: action },
|