@enfyra/mcp-server 0.0.11 → 0.0.14

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/src/index.mjs CHANGED
@@ -1,518 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Enfyra MCP Server - Main Entry Point
4
- *
5
- * Provides tools to manage Enfyra instance via Claude Code.
6
- * All operations go through Enfyra's REST API.
3
+ * Enfyra MCP entry: `config` subcommand (local project files) or stdio MCP server.
7
4
  */
8
5
 
9
- import { config } from 'dotenv';
10
- config();
6
+ import { config as loadEnv } from 'dotenv';
11
7
 
12
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
- import { z } from 'zod';
15
-
16
- // Configuration
17
- const ENFYRA_API_URL = process.env.ENFYRA_API_URL || 'http://localhost:3000/api';
18
- const ENFYRA_EMAIL = process.env.ENFYRA_EMAIL || '';
19
- const ENFYRA_PASSWORD = process.env.ENFYRA_PASSWORD || '';
20
-
21
- // Import modules
22
- import { login, refreshAccessToken, getValidToken, resetTokens, getTokenExpiry, initAuth } from './lib/auth.js';
23
- import { fetchAPI, validateFilter, validateTableName } from './lib/fetch.js';
24
- import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instructions.js';
25
- import { registerTableTools } from './lib/table-tools.js';
26
-
27
- // Initialize auth module
28
- initAuth(ENFYRA_API_URL, ENFYRA_EMAIL, ENFYRA_PASSWORD);
29
-
30
- // Create MCP server — `instructions` is sent to the host (e.g. Claude Code) for the LLM; not README
31
- const server = new McpServer(
32
- {
33
- name: 'enfyra-mcp',
34
- version: '1.0.0',
35
- },
36
- {
37
- instructions: buildMcpServerInstructions(ENFYRA_API_URL),
38
- },
39
- );
40
-
41
- // ============================================================================
42
- // METADATA TOOLS
43
- // ============================================================================
44
-
45
- server.tool('get_all_metadata', 'Get all metadata (tables, columns, relations, routes, hooks, etc.) from Enfyra', {}, async () => {
46
- const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
47
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
48
- });
49
-
50
- server.tool('get_table_metadata', 'Get metadata for a specific table by name', {
51
- tableName: z.string().describe('Table name (e.g., "user_definition", "route_definition")'),
52
- }, async ({ tableName }) => {
53
- const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
54
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
55
- });
56
-
57
- // ============================================================================
58
- // QUERY TOOLS
59
- // ============================================================================
60
-
61
- server.tool(
62
- 'get_enfyra_api_context',
63
- [
64
- 'Returns the resolved API base URL for this MCP session (env ENFYRA_API_URL).',
65
- 'Use when the user asks which HTTP endpoint or full URL applies: combine enfyraApiUrl with paths from server instructions (GET/POST /{table}, PATCH/DELETE /{table}/{id}, no GET /{table}/{id}).',
66
- 'Auth: publishedMethods on a route can allow a method without Bearer; otherwise JWT + routePermissions — see server instructions.',
67
- 'If path might differ from table name, use get_all_routes before asserting a URL.',
68
- 'Same mapping as MCP tool → HTTP: query_table=GET /table?..., create_record=POST /table, update_record=PATCH /table/id, delete_record=DELETE /table/id.',
69
- 'GraphQL: see graphqlHttpUrl / graphqlSchemaUrl in response; GQL_QUERY vs GQL_MUTATION in publishedMethods — server instructions.',
70
- ].join(' '),
71
- {},
72
- async () => {
73
- const base = ENFYRA_API_URL.replace(/\/$/, '');
74
- const gql = buildGraphqlUrls(ENFYRA_API_URL);
75
- const payload = {
76
- enfyraApiUrl: base,
77
- graphqlHttpUrl: gql.graphqlHttpUrl,
78
- graphqlSchemaUrl: gql.graphqlSchemaUrl,
79
- examples: {
80
- listOrCreate: `${base}/<table_name>`,
81
- updateOrDelete: `${base}/<table_name>/<id>`,
82
- oneRowById: `${base}/<table_name>?filter={"id":{"_eq":"<id>"}}&limit=1`,
83
- },
84
- auth: {
85
- publishedMethods: 'If the HTTP method is published for that route, no Bearer required; else Bearer JWT and routePermissions apply.',
86
- mcp: 'This server uses admin credentials from env for tools (fetchAPI).',
87
- },
88
- pathResolution: 'Confirm route path with get_all_routes or metadata — path may not equal table name.',
89
- note: 'Full tool→HTTP mapping is in MCP server instructions (shown to the model at connect).',
90
- };
91
- return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
92
- },
93
- );
94
-
95
- server.tool('query_table', 'Query any table in Enfyra with filters, sorting, and pagination', {
96
- tableName: z.string().describe('Table name to query'),
97
- filter: z.string().optional().describe('Filter object as JSON string. Examples: \'{"status": {"_eq": "active"}}\''),
98
- sort: z.string().optional().describe('Sort field. Prefix with - for descending (e.g., "createdAt", "-id")'),
99
- page: z.number().optional().describe('Page number (default: 1)'),
100
- limit: z.number().optional().describe('Items per page (default: 50, max: 500)'),
101
- fields: z.array(z.string()).optional().describe('Fields to select'),
102
- }, async ({ tableName, filter, sort, page, limit, fields }) => {
103
- validateTableName(tableName);
104
- validateFilter(filter);
105
-
106
- const queryParams = new URLSearchParams();
107
- if (filter) queryParams.set('filter', filter);
108
- if (sort) queryParams.set('sort', sort);
109
- if (page) queryParams.set('page', String(page));
110
- if (limit) queryParams.set('limit', String(limit));
111
- if (fields) queryParams.set('fields', fields.join(','));
112
-
113
- const query = queryParams.toString();
114
- const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}${query ? `?${query}` : ''}`);
115
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
116
- });
117
-
118
- server.tool(
119
- 'find_one_record',
120
- 'Find a single record by ID or filter. By ID uses GET with filter (Enfyra has no GET /table/:id route).',
121
- {
122
- tableName: z.string().describe('Table name'),
123
- id: z.string().optional().describe('Record ID'),
124
- filter: z.string().optional().describe('Filter as JSON string to find by'),
125
- },
126
- async ({ tableName, id, filter }) => {
127
- validateTableName(tableName);
128
- if (id) {
129
- // Enfyra route engine does not register GET /<table>/:id (only PATCH/DELETE use /:id). Use list + filter.
130
- const filterObj = JSON.stringify({ id: { _eq: id } });
131
- const result = await fetchAPI(
132
- ENFYRA_API_URL,
133
- `/${tableName}?filter=${encodeURIComponent(filterObj)}&limit=1`,
134
- );
135
- const one = result.data?.[0] ?? null;
136
- return { content: [{ type: 'text', text: JSON.stringify(one, null, 2) }] };
137
- }
138
- if (!filter) throw new Error('Provide id or filter');
139
- validateFilter(filter);
140
- const result = await fetchAPI(
141
- ENFYRA_API_URL,
142
- `/${tableName}?filter=${encodeURIComponent(filter)}&limit=1`,
143
- );
144
- return { content: [{ type: 'text', text: JSON.stringify(result.data?.[0] || null, null, 2) }] };
145
- },
146
- );
147
-
148
- // ============================================================================
149
- // CRUD TOOLS
150
- // ============================================================================
151
-
152
- server.tool('create_record', 'Create a new record in any table', {
153
- tableName: z.string().describe('Table name to insert into'),
154
- data: z.string().describe('Record data as JSON string'),
155
- }, async ({ tableName, data }) => {
156
- validateTableName(tableName);
157
- const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}`, { method: 'POST', body: data });
158
- return { content: [{ type: 'text', text: `Record created:\n${JSON.stringify(result, null, 2)}` }] };
159
- });
160
-
161
- server.tool('update_record', 'Update an existing record by ID using PATCH', {
162
- tableName: z.string().describe('Table name'),
163
- id: z.string().describe('Record ID to update'),
164
- data: z.string().describe('Fields to update as JSON string'),
165
- }, async ({ tableName, id, data }) => {
166
- validateTableName(tableName);
167
- const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'PATCH', body: data });
168
- return { content: [{ type: 'text', text: `Record updated:\n${JSON.stringify(result, null, 2)}` }] };
169
- });
170
-
171
- server.tool('delete_record', 'Delete a record by ID', {
172
- tableName: z.string().describe('Table name'),
173
- id: z.string().describe('Record ID to delete'),
174
- }, async ({ tableName, id }) => {
175
- validateTableName(tableName);
176
- const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'DELETE' });
177
- return { content: [{ type: 'text', text: `Record deleted:\n${JSON.stringify(result, null, 2)}` }] };
178
- });
179
-
180
- // ============================================================================
181
- // ROUTE & HANDLER TOOLS
182
- // ============================================================================
183
-
184
- server.tool('get_all_routes', 'Get all route definitions with handlers, hooks, and permissions', {
185
- includeDisabled: z.boolean().optional().default(false).describe('Include disabled routes'),
186
- }, async ({ includeDisabled }) => {
187
- const filter = includeDisabled ? {} : { isEnabled: { _eq: true } };
188
- const result = await fetchAPI(ENFYRA_API_URL, `/route_definition?filter=${encodeURIComponent(JSON.stringify(filter))}&limit=500`);
189
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
190
- });
191
-
192
- server.tool('create_route', 'Create a new route definition', {
193
- path: z.string().describe('Route path (e.g., "/api/users")'),
194
- method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'REST', 'GQL_QUERY', 'GQL_MUTATION']).describe('HTTP method'),
195
- tableId: z.string().describe('Main table ID for this route'),
196
- isEnabled: z.boolean().optional().default(true).describe('Enable route'),
197
- description: z.string().optional().describe('Route description'),
198
- }, async ({ path, method, tableId, isEnabled, description }) => {
199
- const result = await fetchAPI(ENFYRA_API_URL, '/route_definition', {
200
- method: 'POST',
201
- body: JSON.stringify({ path, method, tableId, isEnabled, description }),
202
- });
203
- return { content: [{ type: 'text', text: `Route created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
204
- });
205
-
206
- server.tool('create_handler', 'Create a handler for a route. Use template syntax: @BODY, @USER, #table_name, @THROW404', {
207
- routeId: z.string().describe('Route definition ID'),
208
- logic: z.string().describe('Handler logic (JavaScript code)'),
209
- timeout: z.number().optional().describe('Handler timeout in ms (default: 30000)'),
210
- }, async ({ routeId, logic, timeout }) => {
211
- const result = await fetchAPI(ENFYRA_API_URL, '/route_handler_definition', {
212
- method: 'POST',
213
- body: JSON.stringify({ routeId, logic, timeout: timeout || 30000 }),
214
- });
215
- return { content: [{ type: 'text', text: `Handler created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
216
- });
217
-
218
- server.tool('create_pre_hook', 'Create a pre-hook for a route. Use template syntax: @BODY, @QUERY, @USER', {
219
- routeId: z.string().describe('Route definition ID'),
220
- code: z.string().describe('Hook code (JavaScript)'),
221
- methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])).optional().describe('Methods this hook applies to'),
222
- order: z.number().optional().default(0).describe('Hook execution order'),
223
- }, async ({ routeId, code, methods, order }) => {
224
- const result = await fetchAPI(ENFYRA_API_URL, '/pre_hook_definition', {
225
- method: 'POST',
226
- body: JSON.stringify({ routeId, code, methods: methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], order }),
227
- });
228
- return { content: [{ type: 'text', text: `Pre-hook created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
229
- });
230
-
231
- server.tool('create_post_hook', 'Create a post-hook for a route. Use template syntax: @DATA, @STATUS', {
232
- routeId: z.string().describe('Route definition ID'),
233
- code: z.string().describe('Hook code (JavaScript)'),
234
- methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])).optional().describe('Methods this hook applies to'),
235
- order: z.number().optional().default(0).describe('Hook execution order'),
236
- }, async ({ routeId, code, methods, order }) => {
237
- const result = await fetchAPI(ENFYRA_API_URL, '/post_hook_definition', {
238
- method: 'POST',
239
- body: JSON.stringify({ routeId, code, methods: methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], order }),
240
- });
241
- return { content: [{ type: 'text', text: `Post-hook created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
242
- });
243
-
244
- // Register table tools
245
- registerTableTools(server, ENFYRA_API_URL);
246
-
247
- // ============================================================================
248
- // CACHE & SYSTEM TOOLS
249
- // ============================================================================
250
-
251
- server.tool('reload_all', 'Reload all caches (metadata, routes, swagger, GraphQL)', {}, async () => {
252
- const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
253
- return { content: [{ type: 'text', text: `System reloaded:\n${JSON.stringify(result, null, 2)}` }] };
254
- });
255
-
256
- server.tool('reload_metadata', 'Reload metadata cache only', {}, async () => {
257
- const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
258
- return { content: [{ type: 'text', text: `Metadata reloaded:\n${JSON.stringify(result, null, 2)}` }] };
259
- });
260
-
261
- server.tool('reload_routes', 'Reload routes cache only', {}, async () => {
262
- const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
263
- return { content: [{ type: 'text', text: `Routes reloaded:\n${JSON.stringify(result, null, 2)}` }] };
264
- });
265
-
266
- server.tool('reload_swagger', 'Reload Swagger/OpenAPI spec', {}, async () => {
267
- const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/swagger', { method: 'POST' });
268
- return { content: [{ type: 'text', text: `Swagger reloaded:\n${JSON.stringify(result, null, 2)}` }] };
269
- });
270
-
271
- server.tool('reload_graphql', 'Reload GraphQL schema', {}, async () => {
272
- const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
273
- return { content: [{ type: 'text', text: `GraphQL reloaded:\n${JSON.stringify(result, null, 2)}` }] };
274
- });
275
-
276
- // ============================================================================
277
- // LOGS TOOLS
278
- // ============================================================================
279
-
280
- server.tool('get_log_files', 'List available log files and stats', {}, async () => {
281
- const result = await fetchAPI(ENFYRA_API_URL, '/logs');
282
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
283
- });
284
-
285
- server.tool('get_log_content', 'Get content of a specific log file', {
286
- filename: z.string().describe('Log file name'),
287
- page: z.number().optional().default(1).describe('Page number'),
288
- pageSize: z.number().optional().default(100).describe('Lines per page'),
289
- filter: z.string().optional().describe('Text filter'),
290
- level: z.string().optional().describe('Log level filter (INFO, WARN, ERROR)'),
291
- }, async ({ filename, page, pageSize, filter, level }) => {
292
- const queryParams = new URLSearchParams();
293
- if (page) queryParams.set('page', String(page));
294
- if (pageSize) queryParams.set('pageSize', String(pageSize));
295
- if (filter) queryParams.set('filter', filter);
296
- if (level) queryParams.set('level', level);
297
- const result = await fetchAPI(ENFYRA_API_URL, `/logs/${filename}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`);
298
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
299
- });
300
-
301
- server.tool('tail_log', 'Get last N lines from a log file', {
302
- filename: z.string().describe('Log file name'),
303
- lines: z.number().optional().default(50).describe('Number of lines to retrieve'),
304
- }, async ({ filename, lines }) => {
305
- const result = await fetchAPI(ENFYRA_API_URL, `/logs/${filename}/tail?lines=${lines}`);
306
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
307
- });
308
-
309
- server.tool('search_logs', 'Search for ERROR or WARN logs across recent log files', {
310
- level: z.enum(['ERROR', 'WARN', 'INFO']).optional().default('ERROR').describe('Log level'),
311
- keyword: z.string().optional().describe('Keyword to filter logs'),
312
- limit: z.number().optional().default(50).describe('Max results per level'),
313
- }, async ({ level, keyword, limit }) => {
314
- const logFilesResult = await fetchAPI(ENFYRA_API_URL, '/logs');
315
- const logFiles = logFilesResult.files || [];
316
- const recentFiles = logFiles.filter(f => f.name.includes('app-') || f.name.includes('error-'));
317
- const results = [];
318
- for (const file of recentFiles.slice(0, 3)) {
319
- try {
320
- const contentResult = await fetchAPI(ENFYRA_API_URL, `/logs/${file.name}?level=${level}&pageSize=${limit}`);
321
- const lines = contentResult.lines || contentResult.data || [];
322
- const filteredLines = keyword ? lines.filter(l => JSON.stringify(l).toLowerCase().includes(keyword.toLowerCase())) : lines;
323
- if (filteredLines.length > 0) results.push({ file: file.name, level, logs: filteredLines });
324
- } catch (e) { /* skip */ }
325
- }
326
- return { content: [{ type: 'text', text: `Found ${results.length} files:\n${JSON.stringify(results, null, 2)}` }] };
327
- });
328
-
329
- // ============================================================================
330
- // AUTH & USER TOOLS
331
- // ============================================================================
332
-
333
- server.tool('get_current_user', 'Get current authenticated user info', {}, async () => {
334
- const result = await fetchAPI(ENFYRA_API_URL, '/me');
335
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
336
- });
337
-
338
- server.tool('get_all_roles', 'Get all role definitions', {}, async () => {
339
- const result = await fetchAPI(ENFYRA_API_URL, '/role_definition?limit=100');
340
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
341
- });
342
-
343
- server.tool('login', 'Force login to Enfyra and get new tokens', {
344
- email: z.string().email().optional().describe('Admin email'),
345
- password: z.string().optional().describe('Password'),
346
- }, async ({ email, password }) => {
347
- const loginEmail = email || ENFYRA_EMAIL;
348
- const loginPassword = password || ENFYRA_PASSWORD;
349
- if (!loginEmail || !loginPassword) throw new Error('Email and password required');
350
- await login(ENFYRA_API_URL, loginEmail, loginPassword);
351
- return { content: [{ type: 'text', text: `Logged in successfully!\nToken expires: ${new Date(getTokenExpiry()).toISOString()}` }] };
352
- });
353
-
354
- // ============================================================================
355
- // PACKAGE TOOLS
356
- // ============================================================================
357
-
358
- server.tool(
359
- 'search_npm',
360
- 'Search NPM registry for packages. Returns name, version, description for installation.',
361
- {
362
- query: z.string().describe('Package name or search term (e.g., "axios", "node-ssh", "dayjs")'),
363
- limit: z.number().optional().default(5).describe('Max results (default: 5)'),
364
- },
365
- async ({ query, limit }) => {
366
- const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`;
367
- const response = await fetch(url);
368
- if (!response.ok) throw new Error(`NPM search failed: ${response.statusText}`);
369
- const data = await response.json();
370
-
371
- const packages = data.objects.map((obj) => ({
372
- name: obj.package.name,
373
- version: obj.package.version,
374
- description: obj.package.description || '',
375
- }));
376
-
377
- return {
378
- content: [{
379
- type: 'text',
380
- text: JSON.stringify({ packages, total: data.total }, null, 2),
381
- }],
382
- };
383
- },
384
- );
385
-
386
- server.tool(
387
- 'install_package',
388
- [
389
- 'Install an NPM package on Enfyra. Searches NPM registry for exact version, then creates package_definition record.',
390
- 'Enfyra handles the actual yarn add internally based on type.',
391
- 'Type "Server" = available in handlers/hooks as $ctx.$pkgs.packageName.',
392
- 'Type "App" = available in extensions via getPackages().',
393
- ].join(' '),
394
- {
395
- name: z.string().describe('Exact NPM package name (e.g., "node-ssh", "axios")'),
396
- type: z.enum(['Server', 'App']).default('Server').describe('Where to install: Server (handlers/hooks) or App (extensions)'),
397
- version: z.string().optional().describe('Specific version. If omitted, fetches latest from NPM.'),
398
- },
399
- async ({ name, type, version }) => {
400
- // Step 1: Get package info from NPM if version not specified
401
- let pkgVersion = version;
402
- let pkgDescription = '';
403
-
404
- if (!pkgVersion) {
405
- const npmUrl = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(name)}&size=5`;
406
- const npmResponse = await fetch(npmUrl);
407
- if (!npmResponse.ok) throw new Error(`NPM search failed: ${npmResponse.statusText}`);
408
- const npmData = await npmResponse.json();
409
-
410
- const exactMatch = npmData.objects.find((obj) => obj.package.name === name);
411
- if (!exactMatch) throw new Error(`Package "${name}" not found on NPM`);
412
-
413
- pkgVersion = exactMatch.package.version;
414
- pkgDescription = exactMatch.package.description || '';
415
- }
416
-
417
- // Step 2: Check if already installed
418
- const checkFilter = JSON.stringify({ name: { _eq: name } });
419
- const existing = await fetchAPI(ENFYRA_API_URL, `/package_definition?filter=${encodeURIComponent(checkFilter)}&limit=1`);
420
- if (existing.data && existing.data.length > 0) {
421
- return {
422
- content: [{
423
- type: 'text',
424
- text: `Package "${name}" is already installed (version: ${existing.data[0].version}, type: ${existing.data[0].type}).\n${JSON.stringify(existing.data[0], null, 2)}`,
425
- }],
426
- };
427
- }
428
-
429
- // Step 3: Get current user for installedBy
430
- const me = await fetchAPI(ENFYRA_API_URL, '/me');
431
- const userId = me.data?.[0]?.id || me.data?.[0]?._id;
432
- if (!userId) throw new Error('Cannot get current user ID');
433
-
434
- // Step 4: Install via package_definition
435
- const body = {
436
- name,
437
- version: pkgVersion,
438
- description: pkgDescription,
439
- type,
440
- installedBy: { id: userId },
441
- };
442
-
443
- const result = await fetchAPI(ENFYRA_API_URL, '/package_definition', {
444
- method: 'POST',
445
- body: JSON.stringify(body),
446
- });
447
-
448
- return {
449
- content: [{
450
- type: 'text',
451
- text: `Package "${name}@${pkgVersion}" installed successfully (type: ${type}).\n${JSON.stringify(result, null, 2)}`,
452
- }],
453
- };
454
- },
455
- );
456
-
457
- // ============================================================================
458
- // MENU & EXTENSION TOOLS
459
- // ============================================================================
460
-
461
- server.tool('create_menu', 'Create a menu item in the navigation', {
462
- label: z.string().describe('Menu label'),
463
- type: z.enum(['separator', 'link', 'route', 'dropdown', 'widget', 'extension']).describe('Menu type'),
464
- icon: z.string().optional().describe('Lucide icon name'),
465
- path: z.string().optional().describe('Route path for type=route'),
466
- externalUrl: z.string().optional().describe('External URL for type=link'),
467
- order: z.number().optional().default(0).describe('Display order'),
468
- isEnabled: z.boolean().optional().default(true).describe('Enable menu'),
469
- description: z.string().optional().describe('Menu description'),
470
- }, async (data) => {
471
- const result = await fetchAPI(ENFYRA_API_URL, '/menu_definition', { method: 'POST', body: JSON.stringify(data) });
472
- return { content: [{ type: 'text', text: `Menu created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
473
- });
474
-
475
- server.tool(
476
- 'create_extension',
477
- [
478
- '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).',
479
- '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.',
480
- ].join(' '),
481
- {
482
- name: z.string().describe('Extension name (unique)'),
483
- type: z.enum(['page', 'widget']).describe('Extension type: page = full page linked to menu; widget = embed via Widget component'),
484
- code: z.string().describe('Vue SFC string — <template> + <script setup>, NO import statements'),
485
- menuId: z.string().optional().describe('Required for type=page — menu_definition id from create_menu. Omit for widget'),
486
- isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
487
- description: z.string().optional().describe('Extension description'),
488
- },
489
- async (data) => {
490
- const body = { ...data };
491
- if (body.menuId) {
492
- body.menu = { id: body.menuId };
493
- delete body.menuId;
494
- }
495
- const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(body) });
496
- return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}). Tell user to refresh (F5) to see it.\n${JSON.stringify(result, null, 2)}` }] };
497
- },
498
- );
499
-
500
- // ============================================================================
501
- // MAIN
502
- // ============================================================================
503
-
504
- async function main() {
505
- console.error('Starting Enfyra MCP Server...');
506
- console.error(`API URL: ${ENFYRA_API_URL}`);
507
- console.error(`Auth: ${ENFYRA_EMAIL ? `Configured (${ENFYRA_EMAIL})` : 'Not configured'}`);
508
-
509
- const transport = new StdioServerTransport();
510
- await server.connect(transport);
511
-
512
- console.error('Enfyra MCP Server running on stdio');
8
+ const args = process.argv.slice(2);
9
+ if (args[0] === 'config') {
10
+ loadEnv({ quiet: true });
11
+ const { runLocalConfig } = await import('./lib/config-local.mjs');
12
+ await runLocalConfig(args.slice(1));
13
+ process.exit(0);
513
14
  }
514
15
 
515
- main().catch((error) => {
516
- console.error('Fatal error:', error);
517
- process.exit(1);
518
- });
16
+ loadEnv();
17
+ await import('./mcp-server-entry.mjs');