@enfyra/mcp-server 0.0.20 → 0.0.22
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 +1 -1
- package/src/lib/mcp-instructions.js +14 -0
- package/src/lib/table-tools.js +18 -3
- package/src/mcp-server-entry.mjs +10 -1
package/package.json
CHANGED
|
@@ -65,6 +65,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
65
65
|
'### After a new table is created',
|
|
66
66
|
'- MCP **`create_table` supports creating columns and relations in the same call**: pass `columns` and `relations` as JSON arrays. Use `create_relation` only when adding a relation to an existing table later.',
|
|
67
67
|
'- In `create_table.relations`, each relation uses `targetTable` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
|
|
68
|
+
'- **Never ask for or provide physical FK column names** when creating/updating relations. Do not include `fkCol`, `fkColumn`, `foreignKeyColumn`, `sourceColumn`, `targetColumn`, `junctionSourceColumn`, or `junctionTargetColumn` in create/update payloads unless you are only displaying existing metadata. Enfyra relation cascade derives physical FK/junction names from `propertyName` and table metadata, then hides FK columns from app form/schema definition.',
|
|
69
|
+
'- For relation CRUD payloads, the public interface is the relation `propertyName`: example create body uses `"author": {"id": 1}`, not `"authorId"` or a physical FK column. Query/deep/filter keys also use relation `propertyName`.',
|
|
68
70
|
'- Enfyra creates a **default** route at `/{table_name}` using the table **name** from `create_table` (not the alias). Prefer **`create_route`** for additional or custom paths instead of new tables.',
|
|
69
71
|
'- **Four REST HTTP operations** on that resource:',
|
|
70
72
|
` - **GET** \`${getList}\` — list / filter (query: filter, sort, page, limit, fields, meta).`,
|
|
@@ -82,6 +84,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
82
84
|
'- **Right:** `"publishedMethods": [{"id": 1}]` (publishes GET). Multiple: `[{"id": 1}, {"id": 2}]` (publishes GET + POST).',
|
|
83
85
|
'- **To unset:** pass empty array `"publishedMethods": []`.',
|
|
84
86
|
'',
|
|
87
|
+
'### Dynamic script `$repos` mutation return shape',
|
|
88
|
+
'- In handler/hook/flow/websocket scripts, `$ctx.$repos.<table>.create({ data })` and `$ctx.$repos.<table>.update({ id, data })` return the same collection-shaped result as `find`: `{ data: [...], count? }`.',
|
|
89
|
+
'- The `data` field is always an array for `create`/`update`, even when exactly one record was created or updated. If the script needs the record object, it must read `result.data[0]` or `result.data?.[0] ?? null`.',
|
|
90
|
+
'- Wrong: `const id = result.data.id`, `return result.data`, or assuming `create`/`update` returns the bare row object when the script needs one record.',
|
|
91
|
+
'- Right: `const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;`.',
|
|
92
|
+
'',
|
|
85
93
|
'### Auth and publishedMethods (Enfyra server)',
|
|
86
94
|
'- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
|
|
87
95
|
'- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
|
|
@@ -92,6 +100,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
92
100
|
'- **post-hooks always run** after the handler, including when the handler or a pre-hook throws — then `@ERROR` / `$ctx.$error` is set and `@DATA` is null.',
|
|
93
101
|
'- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
|
|
94
102
|
'',
|
|
103
|
+
'### Dynamic script syntax preference',
|
|
104
|
+
'- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, and `@THROW400`–`@THROW503`.',
|
|
105
|
+
'- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
|
|
106
|
+
'- Preferred: `const result = await @REPOS.main.create({ data: @BODY });`.',
|
|
107
|
+
'- Avoid: `const result = await $ctx.$repos.main.create({ data: $ctx.$body });` unless the script truly needs an unmapped `$ctx` property.',
|
|
108
|
+
'',
|
|
95
109
|
'### OAuth login (browser / frontend — not the MCP `login` tool)',
|
|
96
110
|
'- **MCP `login`** uses **email + password** → `POST {base}/auth/login`. It cannot complete OAuth (no browser redirect).',
|
|
97
111
|
'- **Supported providers (server):** `google`, `facebook`, `github` only.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -43,7 +43,20 @@ function parseJsonArrayParam(name, value) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function normalizeRelationForTablePatch(relation) {
|
|
46
|
-
const {
|
|
46
|
+
const {
|
|
47
|
+
sourceTable,
|
|
48
|
+
targetTable,
|
|
49
|
+
targetTableId,
|
|
50
|
+
mappedBy,
|
|
51
|
+
fkCol,
|
|
52
|
+
fkColumn,
|
|
53
|
+
foreignKeyColumn,
|
|
54
|
+
sourceColumn,
|
|
55
|
+
targetColumn,
|
|
56
|
+
junctionSourceColumn,
|
|
57
|
+
junctionTargetColumn,
|
|
58
|
+
...rest
|
|
59
|
+
} = relation;
|
|
47
60
|
const normalized = { ...rest };
|
|
48
61
|
const resolvedTargetTable =
|
|
49
62
|
targetTableId ??
|
|
@@ -90,6 +103,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
90
103
|
'**Not** for adding a custom API path or handler only — for that use **`create_route`** with an existing `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
91
104
|
'PREFERRED: pass `columns` and `relations` params as JSON arrays to create a table WITH columns and relations in one call (cascade). Only use create_column/create_relation separately when adding to an existing table later.',
|
|
92
105
|
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id or {id}.',
|
|
106
|
+
'Do NOT provide physical FK/junction columns. Never include fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn. Enfyra derives and hides those physical columns from relation propertyName/table metadata.',
|
|
93
107
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
94
108
|
'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
95
109
|
'REST surface for that route (matches server route engine): 4 HTTP operations — GET `/<table>` (list/filter), POST `/<table>` (create), PATCH `/<table>/:id` (update), DELETE `/<table>/:id` (delete).',
|
|
@@ -102,7 +116,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
102
116
|
alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
|
|
103
117
|
description: z.string().optional().describe('Description of what this table stores.'),
|
|
104
118
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, defaultValue?, description?, options? }. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"status","type":"enum","options":["draft","published"]}]'),
|
|
105
|
-
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id or {"id": <id>}. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
119
|
+
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id or {"id": <id>}. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
106
120
|
},
|
|
107
121
|
async ({ name, alias, description, columns: columnsJson, relations: relationsJson }) => {
|
|
108
122
|
const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
|
|
@@ -316,7 +330,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
316
330
|
'create_relation',
|
|
317
331
|
[
|
|
318
332
|
'Create a relation between two tables (many-to-one, one-to-many, one-to-one, many-to-many).',
|
|
319
|
-
'For many-to-one: a FK column is created on the source table. For one-to-many: the FK is on the target (inverse relation).',
|
|
333
|
+
'For many-to-one: a physical FK column is created on the source table. For one-to-many: the FK is on the target (inverse relation). This physical FK is derived by Enfyra and hidden from app schema/forms.',
|
|
334
|
+
'Never ask the user for physical FK column names and never send fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. The public API uses relation propertyName only.',
|
|
320
335
|
'Run sequentially — DB migration locks per operation.',
|
|
321
336
|
].join(' '),
|
|
322
337
|
{
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -355,6 +355,7 @@ server.tool(
|
|
|
355
355
|
relations: routeTables.has('relation_definition')
|
|
356
356
|
? 'relation_definition has a REST route for reads/metadata, but canonical schema migration is create_relation/delete_relation or table_definition PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.'
|
|
357
357
|
: 'Use create_relation/delete_relation or table_definition PATCH with the full relations array. Relation onDelete accepts CASCADE, SET NULL, or RESTRICT.',
|
|
358
|
+
relationCascadeFkContract: 'Do not ask for or send physical FK/junction column names in relation create/update payloads. Enfyra derives fk/junction columns from relation propertyName/table metadata and hides FK columns from app schema/forms. Use targetTable, type, propertyName, inversePropertyName or mappedBy, isNullable, onDelete.',
|
|
358
359
|
tableDefinitionRelations: (tableDefinition?.relations || []).map((rel) => rel.propertyName),
|
|
359
360
|
relationDefinitionRelations: (relationTable?.relations || []).map((rel) => rel.propertyName),
|
|
360
361
|
},
|
|
@@ -511,6 +512,7 @@ server.tool(
|
|
|
511
512
|
backendNotes: {
|
|
512
513
|
primaryKey: 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
|
|
513
514
|
relationNames: 'API relation operations use relation propertyName, not physical FK column names.',
|
|
515
|
+
relationCascadeFkContract: 'When creating relations through create_table/create_relation/table_definition PATCH, never provide fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. These are physical implementation details derived by Enfyra and hidden from app schema/forms.',
|
|
514
516
|
graphql: 'GraphQL query args also accept filter/sort/page/limit, but GraphQL requires Bearer auth and table enablement via gql_definition.',
|
|
515
517
|
},
|
|
516
518
|
table: tableName
|
|
@@ -550,6 +552,7 @@ server.tool(
|
|
|
550
552
|
const payload = {
|
|
551
553
|
transformer: {
|
|
552
554
|
rule: 'Dynamic server scripts are transformed before sandbox execution. Macros expand to $ctx paths; comments are not transformed.',
|
|
555
|
+
preferredSyntax: 'Prefer template macros in generated Enfyra scripts. Use @BODY/@QUERY/@PARAMS/@USER/@REPOS/@HELPERS/@SOCKET/@TRIGGER/@DATA/@ERROR/@THROW* instead of raw $ctx access whenever a macro exists. Use raw $ctx only for fields without a macro.',
|
|
553
556
|
coreMacros: {
|
|
554
557
|
'@BODY': '$ctx.$body',
|
|
555
558
|
'@QUERY': '$ctx.$query',
|
|
@@ -618,7 +621,12 @@ server.tool(
|
|
|
618
621
|
},
|
|
619
622
|
},
|
|
620
623
|
helpers: {
|
|
621
|
-
repos:
|
|
624
|
+
repos: {
|
|
625
|
+
scopes: '$repos.main enforces route main table behavior; $repos.secure.<table> enforces field permissions; $repos.<table> is trusted/internal.',
|
|
626
|
+
mutationReturnShape: '$repos.<table>.create({ data }) and $repos.<table>.update({ id, data }) return a collection-shaped result: { data: [...], count? }. data is always an array for create/update, even for one created/updated record. If a script needs the single record object, it must read result.data[0] or result.data?.[0] ?? null.',
|
|
627
|
+
preferredExample: 'const result = await @REPOS.main.create({ data: @BODY }); const record = result.data?.[0] ?? null; return record;',
|
|
628
|
+
wrongSingleRecordAccess: 'Do not use result.data.id, do not return result.data when one object is expected, and do not assume create/update returns the bare row object.',
|
|
629
|
+
},
|
|
622
630
|
socketInHttpOrFlow: 'HTTP/flow context can emitToUser/emitToRoom/emitToGateway/broadcast, but cannot reply/join/leave/disconnect because there is no bound socket.',
|
|
623
631
|
packages: 'Server packages installed through install_package are exposed as $ctx.$pkgs.packageName in server scripts.',
|
|
624
632
|
files: 'Upload helpers are on $helpers; raw create_record on file_definition is not equivalent to multipart upload/storage rollback.',
|
|
@@ -997,6 +1005,7 @@ server.tool(
|
|
|
997
1005
|
fields: 'Use column names and relation propertyName values.',
|
|
998
1006
|
filter: 'Use query DSL operators on column names or nested relation propertyName objects.',
|
|
999
1007
|
deep: 'Deep fetch keys are relation propertyName values.',
|
|
1008
|
+
relationMutation: 'For relation schema creation/update use targetTable/type/propertyName/inversePropertyName|mappedBy/isNullable/onDelete only. Do not provide physical FK/junction columns; Enfyra derives and hides them.',
|
|
1000
1009
|
},
|
|
1001
1010
|
};
|
|
1002
1011
|
|