@enfyra/mcp-server 0.1.52 → 0.1.53

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
@@ -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":"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"}
@@ -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 = [
@@ -1764,19 +1765,6 @@ async function patchExtensionCode(apiUrl, { id, name, search, replace, searchMod
1764
1765
  validation: result.validation,
1765
1766
  };
1766
1767
  }
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
1768
  function resolveColumn(table, columnName) {
1781
1769
  const column = (table.columns || []).find((item) => item?.name === columnName || sameId(getId(item), columnName));
1782
1770
  if (!column)
@@ -3079,7 +3067,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3079
3067
  globalRulesAckKey: globalRulesAckParam(z),
3080
3068
  }, async ({ tableName, isEnabled, globalRulesAckKey }) => {
3081
3069
  assertGlobalRulesAck(globalRulesAckKey);
3082
- const table = resolveTable(await getMetadataTables(ENFYRA_API_URL), tableName);
3070
+ const catalog = await fetchTableCatalog(ENFYRA_API_URL);
3071
+ const table = resolveTableCatalogEntry(catalog, tableName);
3072
+ if (!table)
3073
+ throw new Error(`Table not found: ${tableName}`);
3083
3074
  const existing = await findRecord(ENFYRA_API_URL, 'enfyra_graphql', { table: { id: { _eq: getId(table) } } }, 'id,_id,table.id,isEnabled');
3084
3075
  const operation = await createOrPatch(ENFYRA_API_URL, 'enfyra_graphql', existing, {
3085
3076
  table: { id: getId(table) },
@@ -3367,7 +3358,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3367
3358
  globalRulesAckKey: globalRulesAckParam(z),
3368
3359
  }, async ({ tableName, columnName, ruleType, value, message, description, isEnabled, globalRulesAckKey }) => {
3369
3360
  assertGlobalRulesAck(globalRulesAckKey);
3370
- const table = resolveTable(await getMetadataTables(ENFYRA_API_URL), tableName);
3361
+ const table = await fetchTableMetadataByRef(ENFYRA_API_URL, tableName);
3371
3362
  const column = resolveColumn(table, columnName);
3372
3363
  const existing = await findRecord(ENFYRA_API_URL, 'enfyra_column_rule', {
3373
3364
  column: { id: { _eq: getId(column) } },
@@ -3407,11 +3398,10 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
3407
3398
  if (!!columnName === !!relationName)
3408
3399
  throw new Error('Provide exactly one of columnName or relationName.');
3409
3400
  assertOneScope({ roleId, roleName, allowedUserIds });
3410
- const [tables, role] = await Promise.all([
3411
- getMetadataTables(ENFYRA_API_URL),
3401
+ const [table, role] = await Promise.all([
3402
+ fetchTableMetadataByRef(ENFYRA_API_URL, tableName),
3412
3403
  resolveRole(ENFYRA_API_URL, { roleId, roleName }),
3413
3404
  ]);
3414
- const table = resolveTable(tables, tableName);
3415
3405
  const field = columnName ? resolveColumn(table, columnName) : resolveRelation(table, relationName);
3416
3406
  const filter = {
3417
3407
  action: { _eq: action },