@enfyra/mcp-server 0.0.8 → 0.0.9
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 +1 -1
- package/src/lib/table-tools.js +228 -16
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
70
70
|
'- **`column_definition` has NO route** — do NOT call `query_table("column_definition", …)`. It will always 404.',
|
|
71
71
|
'- To check which tables are accessible via MCP tools, call `get_all_routes` and look for the route whose `mainTable.id` matches the table you need, or `get_all_metadata` to see all table names.',
|
|
72
72
|
'- **Tables confirmed to have REST routes (system):** `table_definition`, `route_definition`, `user_definition`, `setting_definition`, `ai_config_definition`, `role_definition`, `menu_definition`, `extension_definition`, `folder_definition`, `file_definition`, `file_permission_definition`, `package_definition`, `bootstrap_script_definition`, `storage_config_definition`, `ai_conversation_definition`, `ai_message_definition`, `websocket_definition`, `websocket_event_definition`, `oauth_config_definition`, `oauth_account_definition`, `method_definition`, `pre_hook_definition`, `post_hook_definition`, `route_handler_definition`, `route_permission_definition`.',
|
|
73
|
-
'- **Tables without REST routes (internal/system only):** `column_definition`, `relation_definition` — these are managed indirectly via `
|
|
73
|
+
'- **Tables without REST routes (internal/system only):** `column_definition`, `relation_definition` — these are managed indirectly via cascade on `table_definition` (PATCH /table_definition/{id} with columns/relations array). The `create_column` MCP tool handles this automatically.',
|
|
74
74
|
'',
|
|
75
75
|
'### Schema / table migration (sequential only)',
|
|
76
76
|
'- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -4,11 +4,23 @@
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { fetchAPI } from './fetch.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Helper: fetch table with columns and relations
|
|
9
|
+
*/
|
|
10
|
+
async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
|
|
11
|
+
const filter = encodeURIComponent(JSON.stringify({ id: { _eq: tableId } }));
|
|
12
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition?filter=${filter}&limit=1&fields=*,columns.*,relations.*`);
|
|
13
|
+
return result?.data?.[0] || result?.[0] || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
/**
|
|
8
17
|
* Register table tools with MCP server
|
|
9
18
|
*/
|
|
10
19
|
export function registerTableTools(server, ENFYRA_API_URL) {
|
|
11
20
|
const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
|
|
21
|
+
|
|
22
|
+
// ─── READ ───
|
|
23
|
+
|
|
12
24
|
server.tool(
|
|
13
25
|
'get_all_tables',
|
|
14
26
|
'Get all table definitions in the system',
|
|
@@ -21,11 +33,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
21
33
|
}
|
|
22
34
|
);
|
|
23
35
|
|
|
36
|
+
// ─── CREATE TABLE ───
|
|
37
|
+
|
|
24
38
|
server.tool(
|
|
25
39
|
'create_table',
|
|
26
40
|
[
|
|
27
41
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
28
|
-
'Use create_column to add more columns after creation.',
|
|
42
|
+
'Use create_column to add more columns after creation (columns are managed via cascade PATCH on table_definition, NOT via /column_definition).',
|
|
29
43
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
30
44
|
'Enfyra auto-creates a REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
31
45
|
'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).',
|
|
@@ -57,6 +71,194 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
57
71
|
}
|
|
58
72
|
);
|
|
59
73
|
|
|
74
|
+
// ─── UPDATE TABLE ───
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
'update_table',
|
|
78
|
+
[
|
|
79
|
+
'Update table properties: name (rename), alias, description, isSingleRecord, uniques, indexes.',
|
|
80
|
+
'Does NOT modify columns or relations — use create_column, update_column, delete_column, create_relation for those.',
|
|
81
|
+
'Run schema changes sequentially — migration locks DB per operation.',
|
|
82
|
+
].join(' '),
|
|
83
|
+
{
|
|
84
|
+
tableId: z.string().describe('Table definition ID.'),
|
|
85
|
+
name: z.string().optional().describe('New table name (rename). Lowercase with underscores.'),
|
|
86
|
+
alias: z.string().optional().describe('New table alias.'),
|
|
87
|
+
description: z.string().optional().describe('New description.'),
|
|
88
|
+
isSingleRecord: z.boolean().optional().describe('Set to true for single-record table (e.g., settings/config).'),
|
|
89
|
+
},
|
|
90
|
+
async ({ tableId, name, alias, description, isSingleRecord }) => {
|
|
91
|
+
const body = {};
|
|
92
|
+
if (name !== undefined) body.name = name;
|
|
93
|
+
if (alias !== undefined) body.alias = alias;
|
|
94
|
+
if (description !== undefined) body.description = description;
|
|
95
|
+
if (isSingleRecord !== undefined) body.isSingleRecord = isSingleRecord;
|
|
96
|
+
|
|
97
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
98
|
+
method: 'PATCH',
|
|
99
|
+
body: JSON.stringify(body),
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text: `Table ${tableId} updated.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ─── DELETE TABLE ───
|
|
108
|
+
|
|
109
|
+
server.tool(
|
|
110
|
+
'delete_table',
|
|
111
|
+
[
|
|
112
|
+
'Delete a table and ALL associated data. This is DESTRUCTIVE and IRREVERSIBLE.',
|
|
113
|
+
'Deletes: table metadata, all columns, all relations (source + target), all routes, junction tables, FK columns from other tables, and the PHYSICAL DATABASE TABLE with ALL DATA.',
|
|
114
|
+
'Always confirm with the user before calling this tool.',
|
|
115
|
+
].join(' '),
|
|
116
|
+
{
|
|
117
|
+
tableId: z.string().describe('Table definition ID to delete.'),
|
|
118
|
+
},
|
|
119
|
+
async ({ tableId }) => {
|
|
120
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: `Table ${tableId} deleted.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// ─── CREATE COLUMN ───
|
|
130
|
+
|
|
131
|
+
server.tool(
|
|
132
|
+
'create_column',
|
|
133
|
+
[
|
|
134
|
+
'Add a column to an existing table via PATCH /table_definition/{tableId}.',
|
|
135
|
+
'Columns are managed through cascade with table_definition — there is NO direct /column_definition endpoint.',
|
|
136
|
+
'This tool fetches existing columns, appends the new one, and PATCHes the table.',
|
|
137
|
+
'Run schema changes sequentially — migration locks DB per operation.',
|
|
138
|
+
].join(' '),
|
|
139
|
+
{
|
|
140
|
+
tableId: z.string().describe('Table definition ID (from get_all_tables or create_table).'),
|
|
141
|
+
name: z.string().describe('Column name (e.g., "title", "user_id"). Lowercase with underscores.'),
|
|
142
|
+
type: z.string().describe('Column type: varchar, int, text, boolean, datetime, json, decimal, timestamp, uuid, bigint, float, longtext, richtext, simple-json, code, enum, array-select, date.'),
|
|
143
|
+
isNullable: z.boolean().optional().default(true).describe('Set to false if column cannot be null.'),
|
|
144
|
+
isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
|
|
145
|
+
defaultValue: z.string().optional().describe('Default value as JSON string.'),
|
|
146
|
+
description: z.string().optional().describe('Column description.'),
|
|
147
|
+
options: z.string().optional().describe('Column options as JSON string (e.g., enum values).'),
|
|
148
|
+
},
|
|
149
|
+
async ({ tableId, name, type, isNullable, isUnique, defaultValue, description, options }) => {
|
|
150
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
151
|
+
if (!tableData) {
|
|
152
|
+
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existingColumns = (tableData.columns || []).map(col => ({ id: col.id }));
|
|
156
|
+
const newCol = { name, type, isNullable: isNullable ?? true };
|
|
157
|
+
if (isUnique) newCol.isUnique = true;
|
|
158
|
+
if (defaultValue !== undefined) newCol.defaultValue = defaultValue;
|
|
159
|
+
if (description) newCol.description = description;
|
|
160
|
+
if (options) newCol.options = JSON.parse(options);
|
|
161
|
+
|
|
162
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
163
|
+
method: 'PATCH',
|
|
164
|
+
body: JSON.stringify({ columns: [...existingColumns, newCol] }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: 'text', text: `Column "${name}" added to table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ─── UPDATE COLUMN ───
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
'update_column',
|
|
177
|
+
[
|
|
178
|
+
'Update an existing column on a table via PATCH /table_definition/{tableId}.',
|
|
179
|
+
'Fetches all columns, modifies the target column, and PATCHes the table.',
|
|
180
|
+
'Run schema changes sequentially — migration locks DB per operation.',
|
|
181
|
+
].join(' '),
|
|
182
|
+
{
|
|
183
|
+
tableId: z.string().describe('Table definition ID.'),
|
|
184
|
+
columnId: z.string().describe('Column definition ID to update.'),
|
|
185
|
+
name: z.string().optional().describe('New column name.'),
|
|
186
|
+
type: z.string().optional().describe('New column type.'),
|
|
187
|
+
isNullable: z.boolean().optional().describe('Set nullable.'),
|
|
188
|
+
isHidden: z.boolean().optional().describe('Hide column from API responses.'),
|
|
189
|
+
defaultValue: z.string().optional().describe('New default value as JSON string.'),
|
|
190
|
+
description: z.string().optional().describe('New description.'),
|
|
191
|
+
options: z.string().optional().describe('New options as JSON string.'),
|
|
192
|
+
},
|
|
193
|
+
async ({ tableId, columnId, name, type, isNullable, isHidden, defaultValue, description, options }) => {
|
|
194
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
195
|
+
if (!tableData) {
|
|
196
|
+
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const columns = (tableData.columns || []).map(col => {
|
|
200
|
+
if (String(col.id) === String(columnId)) {
|
|
201
|
+
const updated = { id: col.id };
|
|
202
|
+
if (name !== undefined) updated.name = name;
|
|
203
|
+
if (type !== undefined) updated.type = type;
|
|
204
|
+
if (isNullable !== undefined) updated.isNullable = isNullable;
|
|
205
|
+
if (isHidden !== undefined) updated.isHidden = isHidden;
|
|
206
|
+
if (defaultValue !== undefined) updated.defaultValue = defaultValue;
|
|
207
|
+
if (description !== undefined) updated.description = description;
|
|
208
|
+
if (options !== undefined) updated.options = JSON.parse(options);
|
|
209
|
+
return updated;
|
|
210
|
+
}
|
|
211
|
+
return { id: col.id };
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
215
|
+
method: 'PATCH',
|
|
216
|
+
body: JSON.stringify({ columns }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text: `Column ${columnId} updated on table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ─── DELETE COLUMN ───
|
|
226
|
+
|
|
227
|
+
server.tool(
|
|
228
|
+
'delete_column',
|
|
229
|
+
[
|
|
230
|
+
'Delete a column from a table via PATCH /table_definition/{tableId}.',
|
|
231
|
+
'Fetches all columns, removes the target, and PATCHes the table.',
|
|
232
|
+
'The physical column is dropped from the database. System columns (id, createdAt, updatedAt) cannot be deleted.',
|
|
233
|
+
'Run schema changes sequentially — migration locks DB per operation.',
|
|
234
|
+
].join(' '),
|
|
235
|
+
{
|
|
236
|
+
tableId: z.string().describe('Table definition ID.'),
|
|
237
|
+
columnId: z.string().describe('Column definition ID to delete.'),
|
|
238
|
+
},
|
|
239
|
+
async ({ tableId, columnId }) => {
|
|
240
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
241
|
+
if (!tableData) {
|
|
242
|
+
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const columns = (tableData.columns || [])
|
|
246
|
+
.filter(col => String(col.id) !== String(columnId))
|
|
247
|
+
.map(col => ({ id: col.id }));
|
|
248
|
+
|
|
249
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
250
|
+
method: 'PATCH',
|
|
251
|
+
body: JSON.stringify({ columns }),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: 'text', text: `Column ${columnId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// ─── CREATE RELATION ───
|
|
261
|
+
|
|
60
262
|
server.tool(
|
|
61
263
|
'create_relation',
|
|
62
264
|
[
|
|
@@ -85,26 +287,36 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
85
287
|
}
|
|
86
288
|
);
|
|
87
289
|
|
|
290
|
+
// ─── DELETE RELATION ───
|
|
291
|
+
|
|
88
292
|
server.tool(
|
|
89
|
-
'
|
|
90
|
-
|
|
293
|
+
'delete_relation',
|
|
294
|
+
[
|
|
295
|
+
'Delete a relation from a table via PATCH /table_definition/{tableId}.',
|
|
296
|
+
'Fetches all relations, removes the target, and PATCHes the table.',
|
|
297
|
+
'Drops FK columns and junction tables (for many-to-many).',
|
|
298
|
+
].join(' '),
|
|
91
299
|
{
|
|
92
|
-
tableId: z.string().describe('Table definition ID (
|
|
93
|
-
|
|
94
|
-
type: z.string().describe('Column type: varchar, int, text, boolean, datetime, json, decimal, etc.'),
|
|
95
|
-
length: z.number().optional().describe('Length for varchar types (e.g., 255).'),
|
|
96
|
-
isRequired: z.boolean().optional().default(false).describe('Set to true if column cannot be null.'),
|
|
97
|
-
isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
|
|
98
|
-
defaultValue: z.string().optional().describe('Default value as JSON string.'),
|
|
99
|
-
description: z.string().optional().describe('Column description.'),
|
|
300
|
+
tableId: z.string().describe('Table definition ID (source table of the relation).'),
|
|
301
|
+
relationId: z.string().describe('Relation definition ID to delete.'),
|
|
100
302
|
},
|
|
101
|
-
async ({ tableId,
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
303
|
+
async ({ tableId, relationId }) => {
|
|
304
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
|
|
305
|
+
if (!tableData) {
|
|
306
|
+
return { content: [{ type: 'text', text: `Error: Table with ID ${tableId} not found.` }] };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const relations = (tableData.relations || [])
|
|
310
|
+
.filter(rel => String(rel.id) !== String(relationId))
|
|
311
|
+
.map(rel => ({ id: rel.id }));
|
|
312
|
+
|
|
313
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/table_definition/${tableId}`, {
|
|
314
|
+
method: 'PATCH',
|
|
315
|
+
body: JSON.stringify({ relations }),
|
|
105
316
|
});
|
|
317
|
+
|
|
106
318
|
return {
|
|
107
|
-
content: [{ type: 'text', text: `
|
|
319
|
+
content: [{ type: 'text', text: `Relation ${relationId} deleted from table ${tableId}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
108
320
|
};
|
|
109
321
|
}
|
|
110
322
|
);
|