@enfyra/mcp-server 0.0.15 → 0.0.17
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
|
@@ -54,6 +54,10 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
54
54
|
'- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
|
|
55
55
|
'- MCP tools that use `fetchAPI` authenticate with the configured admin credentials; explain to users that **direct HTTP** calls need a token unless the route/method is published.',
|
|
56
56
|
'',
|
|
57
|
+
'### Post-hooks (REST)',
|
|
58
|
+
'- **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.',
|
|
59
|
+
'- You may **mutate** `@DATA` / `$ctx.$data` in place, or **return** a value: a non-`undefined` return replaces `$ctx.$data` as the response body.',
|
|
60
|
+
'',
|
|
57
61
|
'### OAuth login (browser / frontend — not the MCP `login` tool)',
|
|
58
62
|
'- **MCP `login`** uses **email + password** → `POST {base}/auth/login`. It cannot complete OAuth (no browser redirect).',
|
|
59
63
|
'- **Supported providers (server):** `google`, `facebook`, `github` only.',
|
|
@@ -98,7 +102,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
98
102
|
`- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). With Nuxt base: e.g. \`http://localhost:3000/api/graphql\`. With direct Nest: e.g. \`http://localhost:1105/graphql\`.`,
|
|
99
103
|
`- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text); same base pattern as above.`,
|
|
100
104
|
'- A table appears in the schema if some **enabled route** for that table has **both** `GQL_QUERY` and `GQL_MUTATION` in `availableMethods` and a **`mainTable`** pointing at the table. The route **`path` does not need to be** `/<table_name>` (custom paths still qualify).',
|
|
101
|
-
'- **Query** field = same string as `table_definition.name`. **Mutations** are literal concat: `create_`+tableName, `update_`+tableName, `delete_`+tableName (e.g. tableName `post` → `create_post`, input type `postInput`). See `generate-type-defs.ts`.
|
|
105
|
+
'- **Query** field = same string as `table_definition.name`. **Mutations** are literal concat: `create_`+tableName, `update_`+tableName, `delete_`+tableName (e.g. tableName `post` → `create_post`, input type `postInput`). See `generate-type-defs.ts`. If every column is skipped for input (only PK, or only `createdAt`/`updatedAt`, or all unpublished), the schema emits **no** `Query.<tableName>` and **no** create/update/delete mutations for that table (an output `type` may still exist for relation wiring).',
|
|
102
106
|
'- **Auth:** `publishedMethods` may include `GQL_QUERY` and/or `GQL_MUTATION` **separately** — each controls anonymous access for queries vs mutations. Otherwise Bearer JWT + `routePermissions` must list the same method key (`GQL_QUERY` / `GQL_MUTATION`).',
|
|
103
107
|
'- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
|
|
104
108
|
'',
|
|
@@ -130,7 +134,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
130
134
|
'- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@DISPATCH.trigger("flow-name", payload)` instead.',
|
|
131
135
|
'- **Step config examples**: script: `{"code":"return #user_definition.find({limit:10})"}`, condition: `{"code":"return @LAST?.data?.length > 0"}` (uses JS truthy/falsy: `return user` = truthy if exists, `return null` = falsy), query: `{"table":"user_definition","filter":{"status":{"_eq":"active"}},"limit":10}`, http: `{"url":"https://api.example.com","method":"POST","body":{}}` (auto Content-Type: application/json; **http `url` must be public-safe**—see Safety), sleep: `{"ms":5000}`, trigger_flow: `{"flowId":2}`.',
|
|
132
136
|
'- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@LAST`. Input payload via `@PAYLOAD`. Repos via `#table_name`.',
|
|
133
|
-
'- **Template syntax (flows)**: `@PAYLOAD` → `$ctx.$flow.$payload` (input data), `@LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@
|
|
137
|
+
'- **Template syntax (flows)**: `@PAYLOAD` → `$ctx.$flow.$payload` (input data), `@LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@THROW400`–`@THROW503` / `@THROW` → `$ctx.$throw[...]`. Trigger other flows in handlers via `@DISPATCH.trigger(name, payload)` or `$ctx.$dispatch.trigger(name, payload)`.',
|
|
134
138
|
'- **Condition branching**: Condition step uses JavaScript truthy/falsy evaluation (e.g. `return user` → truthy if exists, falsy if null/0/undefined). Children with matching `parent: {id: conditionStepId}` and `branch: "true"/"false"` execute. Root steps (no parent) always execute sequentially.',
|
|
135
139
|
'- **Safety**: Max nesting depth 10 (flow triggering flow). Circular flow detection prevents A→B→A loops. HTTP steps: **SSRF hardening** — only `http`/`https`; blocks `localhost`, private IPs, and hostnames resolving to private IPs (use internet-facing URLs like `https://api.example.com`, not internal services, unless server policy changes). Default HTTP timeout 30s (AbortController). `$dispatch.trigger()` available inside flow steps.',
|
|
136
140
|
'- **Workflow**: Create flow → `create_record` on `flow_definition`. Add steps → `create_record` on `flow_step_definition` with `flow: {id}`. For branch steps, set `parent: {id: conditionStepId}` and `branch: "true"` or `"false"`. Trigger manually via `POST /admin/flow/trigger/{flowId}`.',
|
|
@@ -166,6 +170,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
166
170
|
'- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
|
|
167
171
|
'- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ id, label, onClick, color, icon, order, side, global }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component }`.',
|
|
168
172
|
'- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
|
|
173
|
+
'- **usePageHeaderRegistry:** Page title strip. `{ registerPageHeader, clearPageHeader, pageHeader, hasPageHeader }`. Config: `title`, optional `description`, `stats`, `variant`, `gradient` (`purple`|`blue`|`cyan`|`none` — horizontal strip + leading icon tint), `leadingIcon` (icon name), `hideLeadingIcon`. Call `registerPageHeader` again when title/stats must update (plain object snapshot, not refs inside the config).',
|
|
169
174
|
'- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
|
|
170
175
|
'- **useMenuApi:** Low-level menu API.',
|
|
171
176
|
'- **useFilterQuery:** Filter builder. Returns `{ buildQuery(filter), buildFilterObject(filter), createEmptyFilter(), hasActiveFilters(filter), getFilterSummary(filter, fields), encodeFilterToUrl(filter), parseFilterFromUrl(searchParams) }`.',
|
|
@@ -177,7 +182,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
177
182
|
'#### Injected UI Components (auto-resolved):',
|
|
178
183
|
'- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
|
|
179
184
|
'- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
|
|
180
|
-
'- **Form:** `FormEditor`, `FilterEditor`, `FilterHistory`, `FieldSelector`',
|
|
185
|
+
'- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
|
|
181
186
|
'- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
|
|
182
187
|
'- **Menu:** `MenuRenderer`, `MenuItemEditor`',
|
|
183
188
|
'- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
|
|
@@ -190,7 +195,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
190
195
|
'- `$ctx` — runtime context',
|
|
191
196
|
'',
|
|
192
197
|
'#### Extension types:',
|
|
193
|
-
'- **FormEditor field-map:** Customize fields via `:field-map
|
|
198
|
+
'- **FormEditor field-map:** Customize fields via `:field-map`. Options: `label`, `description`, `hideLabel`, `hideDescription`, `component`, `componentProps`, `type`, `disabled`, `placeholder`, `permission`, `excludedOptions`/`includedOptions`, `fieldProps` (e.g. grid `class: \'md:col-span-2\'` when `layout=\'grid\'`), `booleanWrapperClass`, `fieldWrapperClass`. Optional `:sections` — array of `{ id, title?, hideHeading?, headingClass?, class?, rootClass?, fields: string[] }`; field order follows `fields`; unlisted columns render after. Custom input component: `modelValue` / `update:modelValue`.',
|
|
194
199
|
'- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `menu_definition`), find by path/label, then create extension with `menu: { id: menuId }`. `menu_definition` uses **label** not name — filter by `label` or `path`.',
|
|
195
200
|
'- **type "widget":** Widget extension. No menu required. Embed via `<Widget :id="extensionId" />` in other extensions or pages.',
|
|
196
201
|
'',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -13,6 +13,26 @@ async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
|
13
13
|
return result?.data?.[0] || result?.[0] || null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* PATCH table_definition with auto-confirm for schema changes.
|
|
18
|
+
* First PATCH returns preview + requiredConfirmHash; this helper
|
|
19
|
+
* automatically resends with ?schemaConfirmHash= to apply.
|
|
20
|
+
*/
|
|
21
|
+
async function patchTableAutoConfirm(ENFYRA_API_URL, tableId, body) {
|
|
22
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
23
|
+
method: 'PATCH',
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
});
|
|
26
|
+
const preview = Array.isArray(result?.data) ? result.data[0] : result?.data;
|
|
27
|
+
if (preview?._preview && preview?.requiredConfirmHash) {
|
|
28
|
+
return fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}?schemaConfirmHash=${preview.requiredConfirmHash}`, {
|
|
29
|
+
method: 'PATCH',
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
/**
|
|
17
37
|
* Register table tools with MCP server
|
|
18
38
|
*/
|
|
@@ -40,7 +60,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
40
60
|
[
|
|
41
61
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
42
62
|
'**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).',
|
|
43
|
-
'
|
|
63
|
+
'PREFERRED: pass `columns` param as JSON array to create table WITH columns in one call (cascade). Only use create_column separately if adding to an existing table later.',
|
|
44
64
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
45
65
|
'Enfyra auto-creates a default REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
46
66
|
'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).',
|
|
@@ -53,12 +73,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
53
73
|
alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
|
|
54
74
|
description: z.string().optional().describe('Description of what this table stores.'),
|
|
55
75
|
isEnabled: z.boolean().optional().default(true).describe('Enable table. Set to false to disable.'),
|
|
76
|
+
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"]}]'),
|
|
56
77
|
},
|
|
57
|
-
async ({ name, alias, description, isEnabled }) => {
|
|
78
|
+
async ({ name, alias, description, isEnabled, columns: columnsJson }) => {
|
|
58
79
|
const idColumn = { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
|
|
80
|
+
const userColumns = columnsJson ? JSON.parse(columnsJson) : [];
|
|
59
81
|
const result = await fetchAPI(ENFYRA_API_URL, '/table_definition', {
|
|
60
82
|
method: 'POST',
|
|
61
|
-
body: JSON.stringify({ name, alias, description, isEnabled, columns: [idColumn] }),
|
|
83
|
+
body: JSON.stringify({ name, alias, description, isEnabled, columns: [idColumn, ...userColumns] }),
|
|
62
84
|
});
|
|
63
85
|
const base = ENFYRA_API_URL.replace(/\/$/, '');
|
|
64
86
|
const routePath = `/${name}`;
|
|
@@ -66,8 +88,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
66
88
|
`Auto route path: ${routePath} → full base for REST: ${base}${routePath}`,
|
|
67
89
|
`REST: GET+POST on ${routePath}; PATCH+DELETE on ${routePath}/:id only. No GET ${routePath}/:id.`,
|
|
68
90
|
].join('\n');
|
|
91
|
+
const colHint = userColumns.length
|
|
92
|
+
? `Table created with ${userColumns.length} column(s) + auto id.`
|
|
93
|
+
: `Table created. Use create_column to add columns (tableId: ${result.id}).`;
|
|
69
94
|
return {
|
|
70
|
-
content: [{ type: 'text', text:
|
|
95
|
+
content: [{ type: 'text', text: `${colHint}\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
71
96
|
};
|
|
72
97
|
}
|
|
73
98
|
);
|
|
@@ -153,17 +178,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
153
178
|
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
154
179
|
}
|
|
155
180
|
|
|
156
|
-
const existingColumns = (tableData.columns || []).map(
|
|
181
|
+
const existingColumns = (tableData.columns || []).map(({ table, ...col }) => col);
|
|
157
182
|
const newCol = { name, type, isNullable: isNullable ?? true };
|
|
158
183
|
if (isUnique) newCol.isUnique = true;
|
|
159
184
|
if (defaultValue !== undefined) newCol.defaultValue = defaultValue;
|
|
160
185
|
if (description) newCol.description = description;
|
|
161
186
|
if (options) newCol.options = JSON.parse(options);
|
|
162
187
|
|
|
163
|
-
const result = await
|
|
164
|
-
method: 'PATCH',
|
|
165
|
-
body: JSON.stringify({ columns: [...existingColumns, newCol] }),
|
|
166
|
-
});
|
|
188
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns: [...existingColumns, newCol] });
|
|
167
189
|
|
|
168
190
|
return {
|
|
169
191
|
content: [{ type: 'text', text: `Column "${name}" added to table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -186,36 +208,32 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
186
208
|
name: z.string().optional().describe('New column name.'),
|
|
187
209
|
type: z.string().optional().describe('New column type.'),
|
|
188
210
|
isNullable: z.boolean().optional().describe('Set nullable.'),
|
|
189
|
-
|
|
211
|
+
isPublished: z.boolean().optional().describe('Set column visibility baseline. false = unpublished (omitted from response unless allowed by field permission rules).'),
|
|
190
212
|
defaultValue: z.string().optional().describe('New default value as JSON string.'),
|
|
191
213
|
description: z.string().optional().describe('New description.'),
|
|
192
214
|
options: z.string().optional().describe('New options as JSON string.'),
|
|
193
215
|
},
|
|
194
|
-
async ({ tableId, columnId, name, type, isNullable,
|
|
216
|
+
async ({ tableId, columnId, name, type, isNullable, isPublished, defaultValue, description, options }) => {
|
|
195
217
|
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
196
218
|
if (!tableData) {
|
|
197
219
|
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
const columns = (tableData.columns || []).map(col => {
|
|
223
|
+
const { table, ...rest } = col;
|
|
201
224
|
if (String(col.id) === String(columnId)) {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
205
|
-
if (
|
|
206
|
-
if (
|
|
207
|
-
if (
|
|
208
|
-
if (
|
|
209
|
-
if (options !== undefined) updated.options = JSON.parse(options);
|
|
210
|
-
return updated;
|
|
225
|
+
if (name !== undefined) rest.name = name;
|
|
226
|
+
if (type !== undefined) rest.type = type;
|
|
227
|
+
if (isNullable !== undefined) rest.isNullable = isNullable;
|
|
228
|
+
if (isPublished !== undefined) rest.isPublished = isPublished;
|
|
229
|
+
if (defaultValue !== undefined) rest.defaultValue = defaultValue;
|
|
230
|
+
if (description !== undefined) rest.description = description;
|
|
231
|
+
if (options !== undefined) rest.options = JSON.parse(options);
|
|
211
232
|
}
|
|
212
|
-
return
|
|
233
|
+
return rest;
|
|
213
234
|
});
|
|
214
235
|
|
|
215
|
-
const result = await
|
|
216
|
-
method: 'PATCH',
|
|
217
|
-
body: JSON.stringify({ columns }),
|
|
218
|
-
});
|
|
236
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
|
|
219
237
|
|
|
220
238
|
return {
|
|
221
239
|
content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -245,12 +263,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
245
263
|
|
|
246
264
|
const columns = (tableData.columns || [])
|
|
247
265
|
.filter(col => String(col.id) !== String(columnId))
|
|
248
|
-
.map(
|
|
266
|
+
.map(({ table, ...col }) => col);
|
|
249
267
|
|
|
250
|
-
const result = await
|
|
251
|
-
method: 'PATCH',
|
|
252
|
-
body: JSON.stringify({ columns }),
|
|
253
|
-
});
|
|
268
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
|
|
254
269
|
|
|
255
270
|
return {
|
|
256
271
|
content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
@@ -277,11 +292,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
277
292
|
onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT', 'NO ACTION']).optional().default('SET NULL').describe('On delete behavior.'),
|
|
278
293
|
},
|
|
279
294
|
async ({ sourceTableId, targetTableId, type, propertyName, inversePropertyName, isNullable, onDelete }) => {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
295
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, sourceTableId);
|
|
296
|
+
if (!tableData) {
|
|
297
|
+
return { content: [{ type: 'text', text: `Error: Table with ID ${sourceTableId} not found.` }] };
|
|
298
|
+
}
|
|
299
|
+
const existingRelations = (tableData.relations || []).map(({ sourceTable, targetTable, ...rel }) => rel);
|
|
300
|
+
const newRelation = { targetTableId, type, propertyName, inversePropertyName: inversePropertyName || null, isNullable, onDelete };
|
|
301
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, sourceTableId, { relations: [...existingRelations, newRelation] });
|
|
285
302
|
return {
|
|
286
303
|
content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${sourceTableId} → ${targetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
287
304
|
};
|
|
@@ -309,12 +326,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
309
326
|
|
|
310
327
|
const relations = (tableData.relations || [])
|
|
311
328
|
.filter(rel => String(rel.id) !== String(relationId))
|
|
312
|
-
.map(
|
|
329
|
+
.map(({ sourceTable, targetTable, ...rel }) => rel);
|
|
313
330
|
|
|
314
|
-
const result = await
|
|
315
|
-
method: 'PATCH',
|
|
316
|
-
body: JSON.stringify({ relations }),
|
|
317
|
-
});
|
|
331
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
|
|
318
332
|
|
|
319
333
|
return {
|
|
320
334
|
content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -333,8 +333,8 @@ server.tool(
|
|
|
333
333
|
[
|
|
334
334
|
'Create a post-hook that runs AFTER the handler. Use to transform responses or add metadata.',
|
|
335
335
|
'Use `routeId` from `create_route` or `get_all_routes` — do not create a new table just to get a route id.',
|
|
336
|
-
'Macros: @DATA
|
|
337
|
-
'
|
|
336
|
+
'Macros: @DATA, @STATUS, @ERROR, @BODY, @QUERY, @USER, @SHARE, @API (post-hooks always run; on error path @ERROR is set, @DATA is null).',
|
|
337
|
+
'Mutate @DATA / $ctx.$data in place, or return a value: if the hook returns anything other than undefined, that value replaces $ctx.$data as the response payload.',
|
|
338
338
|
].join(' '),
|
|
339
339
|
{
|
|
340
340
|
routeId: z.union([z.string(), z.number()]).describe('Route definition ID'),
|