@enfyra/mcp-server 0.0.3 → 0.0.5
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/index.mjs +24 -13
- package/src/lib/mcp-instructions.js +40 -1
- package/src/lib/table-tools.js +2 -1
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -369,19 +369,30 @@ server.tool('create_menu', 'Create a menu item in the navigation', {
|
|
|
369
369
|
return { content: [{ type: 'text', text: `Menu created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
-
server.tool(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
372
|
+
server.tool(
|
|
373
|
+
'create_extension',
|
|
374
|
+
[
|
|
375
|
+
'Create an extension (Vue SFC page or widget). Code must be Vue SFC: <template>...</template> + <script setup>...</script> — NO imports, use globals (ref, useToast, useApi, UButton, etc).',
|
|
376
|
+
'For type=page: create menu first (create_menu), get id, then pass menuId. For type=widget no menu needed. Server auto-compiles; tell user to refresh (F5) after create. See extension rules in MCP instructions.',
|
|
377
|
+
].join(' '),
|
|
378
|
+
{
|
|
379
|
+
name: z.string().describe('Extension name (unique)'),
|
|
380
|
+
type: z.enum(['page', 'widget']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component'),
|
|
381
|
+
code: z.string().describe('Vue SFC string — <template> + <script setup>, NO import statements'),
|
|
382
|
+
menuId: z.string().optional().describe('Required for type=page — menu_definition id from create_menu. Omit for widget'),
|
|
383
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
|
|
384
|
+
description: z.string().optional().describe('Extension description'),
|
|
385
|
+
},
|
|
386
|
+
async (data) => {
|
|
387
|
+
const body = { ...data };
|
|
388
|
+
if (body.menuId) {
|
|
389
|
+
body.menu = { id: body.menuId };
|
|
390
|
+
delete body.menuId;
|
|
391
|
+
}
|
|
392
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(body) });
|
|
393
|
+
return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}). Tell user to refresh (F5) to see it.\n${JSON.stringify(result, null, 2)}` }] };
|
|
394
|
+
},
|
|
395
|
+
);
|
|
385
396
|
|
|
386
397
|
// ============================================================================
|
|
387
398
|
// MAIN
|
|
@@ -43,6 +43,32 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
43
43
|
'- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
|
|
44
44
|
'- 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.',
|
|
45
45
|
'',
|
|
46
|
+
'### OAuth login (browser / frontend — not the MCP `login` tool)',
|
|
47
|
+
'- **MCP `login`** uses **email + password** → `POST {base}/auth/login`. It cannot complete OAuth (no browser redirect).',
|
|
48
|
+
'- **Supported providers (server):** `google`, `facebook`, `github` only.',
|
|
49
|
+
'',
|
|
50
|
+
'**Redirect URI must match everywhere (critical):**',
|
|
51
|
+
'- Enfyra exposes OAuth callback at **`{ENFYRA_API_URL}/auth/{provider}/callback`**. Example when `ENFYRA_API_URL` is `http://localhost:3000/api`: **Google** callback URL is **`http://localhost:3000/api/auth/google/callback`** — i.e. `{ENFYRA_API_URL}/auth/google/callback` (same pattern for Facebook/GitHub: `.../auth/facebook/callback`, `.../auth/github/callback`).',
|
|
52
|
+
'- **Google Cloud Console** → OAuth client → **Authorized redirect URIs**: register **exactly** that URL (scheme + host + path, no typo, no extra slash).',
|
|
53
|
+
'- **Enfyra** (`oauth_config_definition` / OAuth settings): field **`redirectUri`** must be the **same string** as in Google Console — byte-for-byte. If they differ, Google or the server will reject the flow.',
|
|
54
|
+
'- **`appCallbackUrl`** (Enfyra OAuth config) is **different**: it is the **frontend app** URL where Enfyra redirects **after** a successful OAuth (server attaches `accessToken`, `refreshToken`, etc. in query). That is your SPA route (e.g. `https://myapp.com/oauth/callback`), **not** the Google redirect URI.',
|
|
55
|
+
'',
|
|
56
|
+
'**Server flow (for answering users or designing FE):**',
|
|
57
|
+
'1. **Start login (redirect user in browser):** `GET {base}/auth/{provider}?redirect=<URL_ENCODED>` — **`redirect` is required** (where to send the user after the whole flow; encoded in `state`). Example: `?redirect=https%3A%2F%2Fmyapp.com%2Foauth-done`',
|
|
58
|
+
'2. Server **302** to Google/Facebook/GitHub authorization page.',
|
|
59
|
+
'3. Provider calls back: `GET {base}/auth/{provider}/callback?code=...&state=...` (server exchanges code, creates/links user, issues JWT).',
|
|
60
|
+
'4. Server **302** to **`appCallbackUrl`** from OAuth config, with query params: **`accessToken`**, **`refreshToken`**, **`expTime`**, **`loginProvider`**, **`redirect`** (the original `redirect` from step 1). On failure, redirects to `redirect` with **`?error=...`**.',
|
|
61
|
+
'',
|
|
62
|
+
'**Frontend build checklist:**',
|
|
63
|
+
'- Register **`appCallbackUrl`** in Enfyra (SPA route). Implement that page: on load read `accessToken`, `refreshToken`, `expTime` from query (then **strip from URL**), store tokens, use `Authorization: Bearer` for API.',
|
|
64
|
+
'- **“Login with Google” button:** `location.href = `${ENFYRA_API_URL}/auth/google?redirect=${encodeURIComponent(appCallbackUrlOrReturn)}`` — `ENFYRA_API_URL` is the API base (e.g. ends with `/api`). `redirect` is where the user should go after tokens are delivered (often matches or relates to `appCallbackUrl`).',
|
|
65
|
+
'- **Error handling:** If redirected with `?error=`, show message to user.',
|
|
66
|
+
'- **Do not confuse:** Google’s **Authorized redirect URI** = Enfyra **`redirectUri`** = `{ENFYRA_API_URL}/auth/google/callback`. **`appCallbackUrl`** = your SPA only (Enfyra redirects there *after* processing Google’s callback).',
|
|
67
|
+
'',
|
|
68
|
+
'### Schema / table migration (sequential only)',
|
|
69
|
+
'- When creating, updating, or deleting tables (or columns), run operations **one at a time**. The migration process locks the DB per operation.',
|
|
70
|
+
'- Do NOT batch multiple schema changes (e.g. create 3 tables in parallel, or create table + add columns simultaneously). Execute each `create_table`, `create_column`, sync, or drop sequentially; wait for completion before the next.',
|
|
71
|
+
'',
|
|
46
72
|
'### Resolving the real REST path',
|
|
47
73
|
'- Do **not** assume `route_definition.path` always equals `table_definition.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
|
|
48
74
|
'- When unsure of the URL path, use MCP **`get_all_routes`** (or **`get_all_metadata`**) to read each route’s **path** and **mainTable** before stating a full URL.',
|
|
@@ -67,6 +93,18 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
67
93
|
'- **Client**: `io("ORIGIN/namespace", {auth: {token: JWT}})` — e.g. `io("http://localhost:3000/chat", {auth: {token: "…"}})`. WebSocket origin usually matches HTTP host (drop `/api` for WS path). `path` in gateway = namespace.',
|
|
68
94
|
'- **Workflow**: Create gateway → `create_record` on `websocket_definition`. Create event → `create_record` on `websocket_event_definition` with `gateway: {id}`. Changes auto-reload; test handlers before saving.',
|
|
69
95
|
'',
|
|
96
|
+
'### Extension (Vue SFC only — NOT React)',
|
|
97
|
+
'- **CRITICAL:** MUST call `create_record` or `update_record` on `extension_definition` — outputting Vue code in chat does NOT save it. User will NOT see it.',
|
|
98
|
+
'- **Code format:** Vue SFC only. Structure: `<template>...</template>` + `<script setup>...</script>`. Server auto-compiles; if compile fails, fix and retry.',
|
|
99
|
+
'- **NO import statements.** All Vue API and composables are injected globally: `ref`, `reactive`, `computed`, `watch`, `onMounted`; `useToast`, `useApi`, `useEnfyraAuth`, `useRouter`, `useRoute`, `navigateTo`; components `UButton`, `UCard`, `UInput`, `UTable`, `UBadge`, `DataTable`, `FormEditor`, `EmptyState`, `Widget`, etc.',
|
|
100
|
+
'- **useApi:** Does NOT auto-run. Must call `execute()` — e.g. `const { data, execute } = useApi(\'/user_definition\', { query: { limit: 10 } }); onMounted(() => execute());`',
|
|
101
|
+
'- **useHeaderActionRegistry:** Pass array directly: `useHeaderActionRegistry([{ id: \'refresh\', label: \'Refresh\', onClick: fn, color: \'primary\' }]);`',
|
|
102
|
+
'- **type "page":** 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`.',
|
|
103
|
+
'- **type "widget":** No menu. Embed via `<Widget :id="extensionId" />`.',
|
|
104
|
+
'- **NPM packages:** Before using (e.g. dayjs, chart.js) — check `package_definition` (filter `type: App`, `name`, `isEnabled`). If not found, tell user to install via Settings → Packages. Use `getPackages([\'dayjs\'])` in code; call in `onMounted` or async handler.',
|
|
105
|
+
'- **After create/update:** Tell user to refresh (F5). Changes may not appear until reload.',
|
|
106
|
+
'- **Minimal example:** `<template><div class="p-6"><h1 class="text-2xl font-bold">{{ title }}</h1><UButton @click="handleClick">Click</UButton></div></template><script setup>const title = ref(\'My Extension\'); const toast = useToast(); const handleClick = () => toast.add({ title: \'Clicked\', color: \'green\' });</script>`',
|
|
107
|
+
'',
|
|
70
108
|
'### MCP tool → HTTP',
|
|
71
109
|
`- \`get_all_metadata\` → GET \`${base}/metadata\``,
|
|
72
110
|
`- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
|
|
@@ -75,7 +113,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
75
113
|
`- \`create_record\` → POST \`${base}/<tableName>\``,
|
|
76
114
|
`- \`update_record\` → PATCH \`${base}/<tableName>/<id>\``,
|
|
77
115
|
`- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\``,
|
|
78
|
-
`-
|
|
116
|
+
`- \`create_extension\` → POST \`${base}/extension_definition\` (Vue SFC only; for page pass menuId). \`update_record\` on extension_definition to change code.`,
|
|
117
|
+
`- Other: \`${base}/menu_definition\`, \`${base}/websocket_definition\`, \`${base}/admin/reload\`, etc.`,
|
|
79
118
|
'',
|
|
80
119
|
'When asked which endpoint the API calls, respond with **HTTP method + full URL** using this base. Call `get_enfyra_api_context` to confirm the resolved base if needed.',
|
|
81
120
|
].join('\n');
|
package/src/lib/table-tools.js
CHANGED
|
@@ -25,6 +25,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
25
25
|
'create_table',
|
|
26
26
|
[
|
|
27
27
|
'Create a new table definition. After creating a table, use create_column to add columns.',
|
|
28
|
+
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
28
29
|
'Enfyra auto-creates a REST route at path `/<table_name>` (same segment as `name`, not alias).',
|
|
29
30
|
'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).',
|
|
30
31
|
'There is NO `GET /<table>/:id`. To fetch one row by id, use GET `/<table>?filter={"id":{"_eq":"<id>"}}&limit=1` or tool query_table / find_one_record.',
|
|
@@ -56,7 +57,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
56
57
|
|
|
57
58
|
server.tool(
|
|
58
59
|
'create_column',
|
|
59
|
-
'Create a column for an existing table. Columns cascade through table_definition.',
|
|
60
|
+
'Create a column for an existing table. Columns cascade through table_definition. Run schema changes sequentially — migration locks DB per operation.',
|
|
60
61
|
{
|
|
61
62
|
tableId: z.string().describe('Table definition ID (from get_all_tables or create_table).'),
|
|
62
63
|
name: z.string().describe('Column name (e.g., "title", "user_id"). Lowercase with underscores.'),
|