@enfyra/mcp-server 0.0.59 → 0.0.60
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 +1 -1
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +22 -0
- package/src/lib/mcp-instructions.js +1 -0
- package/src/mcp-server-entry.mjs +173 -0
package/README.md
CHANGED
|
@@ -215,7 +215,7 @@ When an LLM builds a Nuxt, Next, or other SSR frontend for Enfyra, follow the sa
|
|
|
215
215
|
|
|
216
216
|
## Tools (summary)
|
|
217
217
|
|
|
218
|
-
Metadata, examples, query/CRUD, route/handler/hook, tables/columns, reload cache, logs, user/roles, login, menu/extension, `get_enfyra_api_context`. For full tool list and behavior, see the app after enabling MCP or the source in `src/mcp-server-entry.mjs`.
|
|
218
|
+
Metadata, examples, query/CRUD, method management, route/handler/hook, tables/columns, reload cache, logs, user/roles, login, menu/extension, `get_enfyra_api_context`. For full tool list and behavior, see the app after enabling MCP or the source in `src/mcp-server-entry.mjs`.
|
|
219
219
|
|
|
220
220
|
Use `get_enfyra_examples` when asking an LLM to generate concrete Enfyra implementation patterns. It returns categorized examples for SSR app auth/OAuth/proxy setup, schema/relations, queries/deep, handlers/hooks, permissions/RLS, websocket, flows, files, and extensions.
|
|
221
221
|
|
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -785,6 +785,28 @@ return {
|
|
|
785
785
|
title: 'Dynamic app extensions and menus',
|
|
786
786
|
useWhen: 'Use when adding custom UI pages to the Enfyra app.',
|
|
787
787
|
examples: [
|
|
788
|
+
{
|
|
789
|
+
name: 'Create or update HTTP method colors',
|
|
790
|
+
code: `list_methods()
|
|
791
|
+
|
|
792
|
+
create_method({
|
|
793
|
+
method: "PUT",
|
|
794
|
+
buttonColor: "#e0e7ff",
|
|
795
|
+
textColor: "#4338ca"
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
update_method({
|
|
799
|
+
method: "PATCH",
|
|
800
|
+
buttonColor: "#fef3c7",
|
|
801
|
+
textColor: "#b45309"
|
|
802
|
+
})`,
|
|
803
|
+
notes: [
|
|
804
|
+
'Use dedicated method tools instead of generic CRUD on method_definition.',
|
|
805
|
+
'buttonColor is the badge background and textColor is the badge text color.',
|
|
806
|
+
'The eApp management UI is /settings/methods.',
|
|
807
|
+
'delete_method is preview-first and should only be used for unused custom methods.',
|
|
808
|
+
],
|
|
809
|
+
},
|
|
788
810
|
{
|
|
789
811
|
name: 'Create menu then extension',
|
|
790
812
|
code: `create_menu({
|
|
@@ -306,6 +306,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
306
306
|
'- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, capacity totals, billing totals, or detail metrics there by default; keep those as body cards/tables where the operator can scan them with the page content. Only use PageHeader stats for a deliberately compact overview page where the stats are truly header-level context.',
|
|
307
307
|
'- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Sensitive registry actions must include a `permission` condition, for example `{ id: "create", label: "Create report", permission: { and: [{ route: "/report_definition", methods: ["POST"] }] }, onClick }`.',
|
|
308
308
|
'- **Header action button variants:** choose the button variant by intent. Use `color: "primary", variant: "solid"` for the main page action. Use `color: "neutral", variant: "ghost"` for back/navigation actions and `color: "neutral", variant: "outline"` for visible secondary actions. `variant: "soft"` is only for low-emphasis secondary/chrome actions; do not use soft for critical or primary header actions just because it looks acceptable in dark mode.',
|
|
309
|
+
'- **HTTP method management:** use the dedicated MCP tools `list_methods`, `create_method`, `update_method`, and preview-first `delete_method` for `method_definition`. The eApp UI for the same records is `/settings/methods`. Method color fields are `buttonColor` for badge background and `textColor` for badge text, both full hex colors. Do not use generic `create_record` on `method_definition` unless the dedicated tool is unavailable.',
|
|
309
310
|
'- **Extension navigation:** prefer `NuxtLink` or Nuxt UI components with `:to` for visible navigation links and drill-down cards/buttons. Use `navigateTo(...)` only for imperative navigation after submit, confirm, mutation, or another side effect.',
|
|
310
311
|
'- **Extension runtime scope:** eApp exposes Vue APIs and injected Nuxt/Enfyra composables both to script global scope and Vue app `globalProperties`. Template expressions may call injected helpers directly, for example after a save handler can call `navigateTo("/data/report_definition")`, because Vue compiles template helpers to `_ctx.*`.',
|
|
311
312
|
'- **Extension CSS affects shell utility ordering:** dynamic extension CSS is injected after the app shell CSS. Shell/page-header code must not put conflicting plain Tailwind utilities on the same element, such as `flex-col` plus `flex-row`, `items-start` plus `items-center`, or `text-left` plus `text-center`. Choose one mutually exclusive class per state; otherwise extension CSS can change which utility wins and shift shell layout.',
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -356,6 +356,31 @@ function appendQuery(path, queryParams) {
|
|
|
356
356
|
return `${path}${path.includes('?') ? '&' : '?'}${queryParams}`;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
const METHOD_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
360
|
+
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
361
|
+
|
|
362
|
+
function normalizeMethodNameInput(method) {
|
|
363
|
+
const value = String(method || '').trim().toUpperCase();
|
|
364
|
+
if (!METHOD_NAME_RE.test(value)) {
|
|
365
|
+
throw new Error('Method must start with A-Z and contain only uppercase letters, numbers, or underscore.');
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeHexColorInput(value, fieldName) {
|
|
371
|
+
const color = String(value || '').trim().toLowerCase();
|
|
372
|
+
if (!HEX_COLOR_RE.test(color)) {
|
|
373
|
+
throw new Error(`${fieldName} must be a full hex color such as #1d4ed8.`);
|
|
374
|
+
}
|
|
375
|
+
return color;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function findMethodRecordByName(method) {
|
|
379
|
+
const filter = encodeURIComponent(JSON.stringify({ method: { _eq: method } }));
|
|
380
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,method,buttonColor,textColor,isSystem`);
|
|
381
|
+
return unwrapData(result)[0] || null;
|
|
382
|
+
}
|
|
383
|
+
|
|
359
384
|
// Create MCP server — `instructions` is sent to the host (e.g. Claude Code) for the LLM; not README
|
|
360
385
|
const server = new McpServer(
|
|
361
386
|
{
|
|
@@ -1020,6 +1045,154 @@ server.tool('delete_record', 'Delete a record by ID', {
|
|
|
1020
1045
|
}, null, 2) }] };
|
|
1021
1046
|
});
|
|
1022
1047
|
|
|
1048
|
+
server.tool(
|
|
1049
|
+
'list_methods',
|
|
1050
|
+
'List method_definition records with their UI colors. Use this before creating route methods or method-colored UI.',
|
|
1051
|
+
{},
|
|
1052
|
+
async () => {
|
|
1053
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition?fields=id,_id,method,buttonColor,textColor,isSystem&sort=method&limit=0');
|
|
1054
|
+
const methods = unwrapData(result).map((method) => ({
|
|
1055
|
+
id: getId(method),
|
|
1056
|
+
method: method.method,
|
|
1057
|
+
buttonColor: method.buttonColor,
|
|
1058
|
+
textColor: method.textColor,
|
|
1059
|
+
isSystem: method.isSystem === true,
|
|
1060
|
+
}));
|
|
1061
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1062
|
+
tableName: 'method_definition',
|
|
1063
|
+
methods,
|
|
1064
|
+
appUi: '/settings/methods',
|
|
1065
|
+
}, null, 2) }] };
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
server.tool(
|
|
1070
|
+
'create_method',
|
|
1071
|
+
'Create a method_definition record with app badge colors. Prefer this over generic create_record for method_definition.',
|
|
1072
|
+
{
|
|
1073
|
+
method: z.string().describe('Uppercase method name, e.g. GET, POST, PUT, CUSTOM_METHOD. Must start with A-Z and contain only A-Z, 0-9, or underscore.'),
|
|
1074
|
+
buttonColor: z.string().describe('Badge background color as full hex, e.g. #dbeafe.'),
|
|
1075
|
+
textColor: z.string().describe('Badge text color as full hex, e.g. #1d4ed8.'),
|
|
1076
|
+
isSystem: z.boolean().optional().default(false).describe('Set true only for built-in/runtime-owned methods. Normal app methods should leave this false.'),
|
|
1077
|
+
},
|
|
1078
|
+
async ({ method, buttonColor, textColor, isSystem }) => {
|
|
1079
|
+
const normalizedMethod = normalizeMethodNameInput(method);
|
|
1080
|
+
const existing = await findMethodRecordByName(normalizedMethod);
|
|
1081
|
+
if (existing) {
|
|
1082
|
+
throw new Error(`Method ${normalizedMethod} already exists with id ${getId(existing)}. Use update_method to change colors.`);
|
|
1083
|
+
}
|
|
1084
|
+
const body = {
|
|
1085
|
+
method: normalizedMethod,
|
|
1086
|
+
buttonColor: normalizeHexColorInput(buttonColor, 'buttonColor'),
|
|
1087
|
+
textColor: normalizeHexColorInput(textColor, 'textColor'),
|
|
1088
|
+
isSystem: isSystem === true,
|
|
1089
|
+
};
|
|
1090
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/method_definition', {
|
|
1091
|
+
method: 'POST',
|
|
1092
|
+
body: JSON.stringify(body),
|
|
1093
|
+
});
|
|
1094
|
+
_methodMap = null;
|
|
1095
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1096
|
+
...summarizeMutationResult(result, 'created', 'method_definition'),
|
|
1097
|
+
method: normalizedMethod,
|
|
1098
|
+
appUi: '/settings/methods',
|
|
1099
|
+
}, null, 2) }] };
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
server.tool(
|
|
1104
|
+
'update_method',
|
|
1105
|
+
'Update a method_definition record color pair, and optionally rename non-system methods. Prefer this over generic update_record for method_definition.',
|
|
1106
|
+
{
|
|
1107
|
+
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1108
|
+
method: z.string().optional().describe('Existing method name to find, or new name when id is provided.'),
|
|
1109
|
+
buttonColor: z.string().optional().describe('Badge background color as full hex, e.g. #dbeafe.'),
|
|
1110
|
+
textColor: z.string().optional().describe('Badge text color as full hex, e.g. #1d4ed8.'),
|
|
1111
|
+
},
|
|
1112
|
+
async ({ id, method, buttonColor, textColor }) => {
|
|
1113
|
+
let targetId = id;
|
|
1114
|
+
let existing = null;
|
|
1115
|
+
if (!targetId) {
|
|
1116
|
+
if (!method) throw new Error('Provide id or method.');
|
|
1117
|
+
const normalizedMethod = normalizeMethodNameInput(method);
|
|
1118
|
+
existing = await findMethodRecordByName(normalizedMethod);
|
|
1119
|
+
if (!existing) throw new Error(`Method ${normalizedMethod} was not found.`);
|
|
1120
|
+
targetId = getId(existing);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const body = {};
|
|
1124
|
+
if (buttonColor !== undefined) {
|
|
1125
|
+
body.buttonColor = normalizeHexColorInput(buttonColor, 'buttonColor');
|
|
1126
|
+
}
|
|
1127
|
+
if (textColor !== undefined) {
|
|
1128
|
+
body.textColor = normalizeHexColorInput(textColor, 'textColor');
|
|
1129
|
+
}
|
|
1130
|
+
if (method !== undefined && id) {
|
|
1131
|
+
body.method = normalizeMethodNameInput(method);
|
|
1132
|
+
}
|
|
1133
|
+
if (Object.keys(body).length === 0) {
|
|
1134
|
+
throw new Error('Provide buttonColor, textColor, or a new method name.');
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition/${encodeURIComponent(String(targetId))}`, {
|
|
1138
|
+
method: 'PATCH',
|
|
1139
|
+
body: JSON.stringify(body),
|
|
1140
|
+
});
|
|
1141
|
+
_methodMap = null;
|
|
1142
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1143
|
+
...summarizeMutationResult(result, 'updated', 'method_definition'),
|
|
1144
|
+
id: targetId,
|
|
1145
|
+
appUi: '/settings/methods',
|
|
1146
|
+
}, null, 2) }] };
|
|
1147
|
+
},
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
server.tool(
|
|
1151
|
+
'delete_method',
|
|
1152
|
+
'Preview or delete a method_definition record. Only delete unused custom methods; system/default methods should be kept.',
|
|
1153
|
+
{
|
|
1154
|
+
id: z.string().optional().describe('Method record id. If omitted, method is used to find the record.'),
|
|
1155
|
+
method: z.string().optional().describe('Method name to find when id is omitted.'),
|
|
1156
|
+
confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
|
|
1157
|
+
},
|
|
1158
|
+
async ({ id, method, confirm }) => {
|
|
1159
|
+
let targetId = id;
|
|
1160
|
+
let target = null;
|
|
1161
|
+
if (!targetId) {
|
|
1162
|
+
if (!method) throw new Error('Provide id or method.');
|
|
1163
|
+
target = await findMethodRecordByName(normalizeMethodNameInput(method));
|
|
1164
|
+
if (!target) throw new Error(`Method ${method} was not found.`);
|
|
1165
|
+
targetId = getId(target);
|
|
1166
|
+
}
|
|
1167
|
+
if (!confirm) {
|
|
1168
|
+
if (!target) {
|
|
1169
|
+
const primaryKey = await getPrimaryFieldName('method_definition');
|
|
1170
|
+
const filter = encodeURIComponent(JSON.stringify({ [primaryKey]: { _eq: targetId } }));
|
|
1171
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition?filter=${filter}&limit=1&fields=id,_id,method,buttonColor,textColor,isSystem`);
|
|
1172
|
+
target = unwrapData(result)[0] || null;
|
|
1173
|
+
}
|
|
1174
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1175
|
+
action: 'delete_method_preview',
|
|
1176
|
+
id: targetId,
|
|
1177
|
+
method: target?.method,
|
|
1178
|
+
isSystem: target?.isSystem === true,
|
|
1179
|
+
destructive: true,
|
|
1180
|
+
warning: 'Only delete unused custom methods. Deleting a method can affect route method relations.',
|
|
1181
|
+
next: 'Call delete_method again with confirm=true to delete.',
|
|
1182
|
+
}, null, 2) }] };
|
|
1183
|
+
}
|
|
1184
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/method_definition/${encodeURIComponent(String(targetId))}`, { method: 'DELETE' });
|
|
1185
|
+
_methodMap = null;
|
|
1186
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1187
|
+
action: 'deleted',
|
|
1188
|
+
tableName: 'method_definition',
|
|
1189
|
+
id: targetId,
|
|
1190
|
+
statusCode: result?.statusCode,
|
|
1191
|
+
success: result?.success,
|
|
1192
|
+
}, null, 2) }] };
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1023
1196
|
server.tool(
|
|
1024
1197
|
'run_admin_test',
|
|
1025
1198
|
[
|