@enfyra/mcp-server 0.0.1
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 +115 -0
- package/package.json +24 -0
- package/src/index.mjs +348 -0
- package/src/lib/auth.js +154 -0
- package/src/lib/fetch.js +103 -0
- package/src/lib/table-tools.js +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Enfyra MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for managing Enfyra instances via Claude Code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto Token Refresh**: Automatically obtains and refreshes JWT tokens
|
|
8
|
+
- **Metadata Management**: Query tables, columns, relations, routes, hooks
|
|
9
|
+
- **CRUD Operations**: Create, read, update, delete records in any table
|
|
10
|
+
- **Route & Handler Management**: Create routes, handlers, pre/post hooks
|
|
11
|
+
- **Table Management**: Create tables and columns
|
|
12
|
+
- **Cache Control**: Reload metadata, routes, Swagger, GraphQL
|
|
13
|
+
- **Log Access**: View and tail log files
|
|
14
|
+
|
|
15
|
+
## Add to Claude Code
|
|
16
|
+
|
|
17
|
+
### Method 1: Global Configuration (Recommended)
|
|
18
|
+
|
|
19
|
+
Edit `~/.claude.json` and add to the `mcpServers` section:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"enfyra": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "@enfyra/mcp-server"],
|
|
27
|
+
"env": {
|
|
28
|
+
"ENFYRA_API_URL": "http://localhost:3000/api",
|
|
29
|
+
"ENFYRA_EMAIL": "your-email@example.com",
|
|
30
|
+
"ENFYRA_PASSWORD": "your-password"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Important**:
|
|
38
|
+
- The `-y` flag automatically confirms installation
|
|
39
|
+
- After updating config, restart Claude Code for changes to take effect
|
|
40
|
+
|
|
41
|
+
| Variable | Description | Default |
|
|
42
|
+
|----------|-------------|---------|
|
|
43
|
+
| `ENFYRA_API_URL` | Base URL of Enfyra API | `http://localhost:3000/api` |
|
|
44
|
+
| `ENFYRA_EMAIL` | Admin email for authentication | - |
|
|
45
|
+
| `ENFYRA_PASSWORD` | Admin password for authentication | - |
|
|
46
|
+
|
|
47
|
+
## Available Tools
|
|
48
|
+
|
|
49
|
+
### Authentication
|
|
50
|
+
- `login` - Login and get new access token
|
|
51
|
+
|
|
52
|
+
### Metadata
|
|
53
|
+
- `get_all_metadata` - Get all system metadata
|
|
54
|
+
- `get_table_metadata` - Get metadata for a specific table
|
|
55
|
+
- `query_table` - Query any table with filters
|
|
56
|
+
- `find_one_record` - Find a single record
|
|
57
|
+
|
|
58
|
+
### CRUD
|
|
59
|
+
- `create_record` - Create a new record
|
|
60
|
+
- `update_record` - Update a record
|
|
61
|
+
- `delete_record` - Delete a record
|
|
62
|
+
|
|
63
|
+
### Routes & Handlers
|
|
64
|
+
- `get_all_routes` - Get all route definitions
|
|
65
|
+
- `create_route` - Create a new route
|
|
66
|
+
- `create_handler` - Create a handler for a route
|
|
67
|
+
- `create_pre_hook` - Create a pre-hook
|
|
68
|
+
- `create_post_hook` - Create a post-hook
|
|
69
|
+
|
|
70
|
+
### Tables
|
|
71
|
+
- `get_all_tables` - Get all table definitions
|
|
72
|
+
- `create_table` - Create a new table
|
|
73
|
+
- `create_column` - Create a column
|
|
74
|
+
- `sync_table_schema` - Sync DB schema with metadata
|
|
75
|
+
|
|
76
|
+
### System
|
|
77
|
+
- `reload_all` - Reload all caches
|
|
78
|
+
- `reload_metadata` - Reload metadata only
|
|
79
|
+
- `reload_routes` - Reload routes only
|
|
80
|
+
- `reload_swagger` - Reload Swagger spec
|
|
81
|
+
- `reload_graphql` - Reload GraphQL schema
|
|
82
|
+
|
|
83
|
+
### Logs
|
|
84
|
+
- `get_log_files` - List available log files
|
|
85
|
+
- `get_log_content` - Get log file content
|
|
86
|
+
- `tail_log` - Get last N lines from log
|
|
87
|
+
|
|
88
|
+
### Auth
|
|
89
|
+
- `get_current_user` - Get current user info
|
|
90
|
+
- `get_all_roles` - Get all roles
|
|
91
|
+
|
|
92
|
+
## Usage Examples
|
|
93
|
+
|
|
94
|
+
After configuring in Claude Code:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
"Query all users"
|
|
98
|
+
→ Uses query_table with tableName="user_definition"
|
|
99
|
+
|
|
100
|
+
"Create a new API route for /api/tasks"
|
|
101
|
+
→ Uses create_route, then create_handler
|
|
102
|
+
|
|
103
|
+
"Debug the error logs"
|
|
104
|
+
→ Uses tail_log with filename="error.log"
|
|
105
|
+
|
|
106
|
+
"Deploy my metadata changes"
|
|
107
|
+
→ Uses reload_all
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Security Notes
|
|
111
|
+
|
|
112
|
+
- All operations go through Enfyra's REST API
|
|
113
|
+
- Authentication is required (JWT token with auto-refresh)
|
|
114
|
+
- Permissions are enforced by Enfyra's auth system
|
|
115
|
+
- Pre/Post hooks and RLS are applied to all queries
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enfyra/mcp-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/index.mjs",
|
|
8
|
+
"bin": "src/index.mjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node src/index.mjs",
|
|
14
|
+
"dev": "node --watch src/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
+
"dotenv": "^17.3.1",
|
|
19
|
+
"zod": "^3.24.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { config } from 'dotenv';
|
|
10
|
+
config();
|
|
11
|
+
|
|
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 { registerTableTools } from './lib/table-tools.js';
|
|
25
|
+
|
|
26
|
+
// Initialize auth module
|
|
27
|
+
initAuth(ENFYRA_API_URL, ENFYRA_EMAIL, ENFYRA_PASSWORD);
|
|
28
|
+
|
|
29
|
+
// Create MCP server
|
|
30
|
+
const server = new McpServer({
|
|
31
|
+
name: 'enfyra-mcp',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// METADATA TOOLS
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
server.tool('get_all_metadata', 'Get all metadata (tables, columns, relations, routes, hooks, etc.) from Enfyra', {}, async () => {
|
|
40
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
41
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
server.tool('get_table_metadata', 'Get metadata for a specific table by name', {
|
|
45
|
+
tableName: z.string().describe('Table name (e.g., "user_definition", "route_definition")'),
|
|
46
|
+
}, async ({ tableName }) => {
|
|
47
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/metadata/${tableName}`);
|
|
48
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// QUERY TOOLS
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
server.tool('query_table', 'Query any table in Enfyra with filters, sorting, and pagination', {
|
|
56
|
+
tableName: z.string().describe('Table name to query'),
|
|
57
|
+
filter: z.string().optional().describe('Filter object as JSON string. Examples: \'{"status": {"_eq": "active"}}\''),
|
|
58
|
+
sort: z.string().optional().describe('Sort field. Prefix with - for descending (e.g., "createdAt", "-id")'),
|
|
59
|
+
page: z.number().optional().describe('Page number (default: 1)'),
|
|
60
|
+
limit: z.number().optional().describe('Items per page (default: 50, max: 500)'),
|
|
61
|
+
fields: z.array(z.string()).optional().describe('Fields to select'),
|
|
62
|
+
}, async ({ tableName, filter, sort, page, limit, fields }) => {
|
|
63
|
+
validateTableName(tableName);
|
|
64
|
+
validateFilter(filter);
|
|
65
|
+
|
|
66
|
+
const queryParams = new URLSearchParams();
|
|
67
|
+
if (filter) queryParams.set('filter', filter);
|
|
68
|
+
if (sort) queryParams.set('sort', sort);
|
|
69
|
+
if (page) queryParams.set('page', String(page));
|
|
70
|
+
if (limit) queryParams.set('limit', String(limit));
|
|
71
|
+
if (fields) queryParams.set('fields', fields.join(','));
|
|
72
|
+
|
|
73
|
+
const query = queryParams.toString();
|
|
74
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}${query ? `?${query}` : ''}`);
|
|
75
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.tool('find_one_record', 'Find a single record by ID or filter', {
|
|
79
|
+
tableName: z.string().describe('Table name'),
|
|
80
|
+
id: z.string().optional().describe('Record ID'),
|
|
81
|
+
filter: z.string().optional().describe('Filter as JSON string to find by'),
|
|
82
|
+
}, async ({ tableName, id, filter }) => {
|
|
83
|
+
validateTableName(tableName);
|
|
84
|
+
if (id) {
|
|
85
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`);
|
|
86
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
87
|
+
}
|
|
88
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}?filter=${filter}&limit=1`);
|
|
89
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.data?.[0] || null, null, 2) }] };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// CRUD TOOLS
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
server.tool('create_record', 'Create a new record in any table', {
|
|
97
|
+
tableName: z.string().describe('Table name to insert into'),
|
|
98
|
+
data: z.string().describe('Record data as JSON string'),
|
|
99
|
+
}, async ({ tableName, data }) => {
|
|
100
|
+
validateTableName(tableName);
|
|
101
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}`, { method: 'POST', body: data });
|
|
102
|
+
return { content: [{ type: 'text', text: `Record created:\n${JSON.stringify(result, null, 2)}` }] };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
server.tool('update_record', 'Update an existing record by ID using PATCH', {
|
|
106
|
+
tableName: z.string().describe('Table name'),
|
|
107
|
+
id: z.string().describe('Record ID to update'),
|
|
108
|
+
data: z.string().describe('Fields to update as JSON string'),
|
|
109
|
+
}, async ({ tableName, id, data }) => {
|
|
110
|
+
validateTableName(tableName);
|
|
111
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'PATCH', body: data });
|
|
112
|
+
return { content: [{ type: 'text', text: `Record updated:\n${JSON.stringify(result, null, 2)}` }] };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
server.tool('delete_record', 'Delete a record by ID', {
|
|
116
|
+
tableName: z.string().describe('Table name'),
|
|
117
|
+
id: z.string().describe('Record ID to delete'),
|
|
118
|
+
}, async ({ tableName, id }) => {
|
|
119
|
+
validateTableName(tableName);
|
|
120
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/${tableName}/${id}`, { method: 'DELETE' });
|
|
121
|
+
return { content: [{ type: 'text', text: `Record deleted:\n${JSON.stringify(result, null, 2)}` }] };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// ROUTE & HANDLER TOOLS
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
server.tool('get_all_routes', 'Get all route definitions with handlers, hooks, and permissions', {
|
|
129
|
+
includeDisabled: z.boolean().optional().default(false).describe('Include disabled routes'),
|
|
130
|
+
}, async ({ includeDisabled }) => {
|
|
131
|
+
const filter = includeDisabled ? {} : { isEnabled: { _eq: true } };
|
|
132
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/route_definition?filter=${encodeURIComponent(JSON.stringify(filter))}&limit=500`);
|
|
133
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
server.tool('create_route', 'Create a new route definition', {
|
|
137
|
+
path: z.string().describe('Route path (e.g., "/api/users")'),
|
|
138
|
+
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'REST', 'GQL_QUERY', 'GQL_MUTATION']).describe('HTTP method'),
|
|
139
|
+
tableId: z.string().describe('Main table ID for this route'),
|
|
140
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable route'),
|
|
141
|
+
description: z.string().optional().describe('Route description'),
|
|
142
|
+
}, async ({ path, method, tableId, isEnabled, description }) => {
|
|
143
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/route_definition', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify({ path, method, tableId, isEnabled, description }),
|
|
146
|
+
});
|
|
147
|
+
return { content: [{ type: 'text', text: `Route created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
server.tool('create_handler', 'Create a handler for a route. Use template syntax: @BODY, @USER, #table_name, @THROW404', {
|
|
151
|
+
routeId: z.string().describe('Route definition ID'),
|
|
152
|
+
logic: z.string().describe('Handler logic (JavaScript code)'),
|
|
153
|
+
timeout: z.number().optional().describe('Handler timeout in ms (default: 30000)'),
|
|
154
|
+
}, async ({ routeId, logic, timeout }) => {
|
|
155
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/route_handler_definition', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
body: JSON.stringify({ routeId, logic, timeout: timeout || 30000 }),
|
|
158
|
+
});
|
|
159
|
+
return { content: [{ type: 'text', text: `Handler created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.tool('create_pre_hook', 'Create a pre-hook for a route. Use template syntax: @BODY, @QUERY, @USER', {
|
|
163
|
+
routeId: z.string().describe('Route definition ID'),
|
|
164
|
+
code: z.string().describe('Hook code (JavaScript)'),
|
|
165
|
+
methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])).optional().describe('Methods this hook applies to'),
|
|
166
|
+
order: z.number().optional().default(0).describe('Hook execution order'),
|
|
167
|
+
}, async ({ routeId, code, methods, order }) => {
|
|
168
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/pre_hook_definition', {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: JSON.stringify({ routeId, code, methods: methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], order }),
|
|
171
|
+
});
|
|
172
|
+
return { content: [{ type: 'text', text: `Pre-hook created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
server.tool('create_post_hook', 'Create a post-hook for a route. Use template syntax: @DATA, @STATUS', {
|
|
176
|
+
routeId: z.string().describe('Route definition ID'),
|
|
177
|
+
code: z.string().describe('Hook code (JavaScript)'),
|
|
178
|
+
methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])).optional().describe('Methods this hook applies to'),
|
|
179
|
+
order: z.number().optional().default(0).describe('Hook execution order'),
|
|
180
|
+
}, async ({ routeId, code, methods, order }) => {
|
|
181
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/post_hook_definition', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: JSON.stringify({ routeId, code, methods: methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], order }),
|
|
184
|
+
});
|
|
185
|
+
return { content: [{ type: 'text', text: `Post-hook created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Register table tools
|
|
189
|
+
registerTableTools(server, ENFYRA_API_URL);
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// CACHE & SYSTEM TOOLS
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
server.tool('reload_all', 'Reload all caches (metadata, routes, swagger, GraphQL)', {}, async () => {
|
|
196
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload', { method: 'POST' });
|
|
197
|
+
return { content: [{ type: 'text', text: `System reloaded:\n${JSON.stringify(result, null, 2)}` }] };
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
server.tool('reload_metadata', 'Reload metadata cache only', {}, async () => {
|
|
201
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/metadata', { method: 'POST' });
|
|
202
|
+
return { content: [{ type: 'text', text: `Metadata reloaded:\n${JSON.stringify(result, null, 2)}` }] };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
server.tool('reload_routes', 'Reload routes cache only', {}, async () => {
|
|
206
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/routes', { method: 'POST' });
|
|
207
|
+
return { content: [{ type: 'text', text: `Routes reloaded:\n${JSON.stringify(result, null, 2)}` }] };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
server.tool('reload_swagger', 'Reload Swagger/OpenAPI spec', {}, async () => {
|
|
211
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/swagger', { method: 'POST' });
|
|
212
|
+
return { content: [{ type: 'text', text: `Swagger reloaded:\n${JSON.stringify(result, null, 2)}` }] };
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
server.tool('reload_graphql', 'Reload GraphQL schema', {}, async () => {
|
|
216
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/admin/reload/graphql', { method: 'POST' });
|
|
217
|
+
return { content: [{ type: 'text', text: `GraphQL reloaded:\n${JSON.stringify(result, null, 2)}` }] };
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// LOGS TOOLS
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
server.tool('get_log_files', 'List available log files and stats', {}, async () => {
|
|
225
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/logs');
|
|
226
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
server.tool('get_log_content', 'Get content of a specific log file', {
|
|
230
|
+
filename: z.string().describe('Log file name'),
|
|
231
|
+
page: z.number().optional().default(1).describe('Page number'),
|
|
232
|
+
pageSize: z.number().optional().default(100).describe('Lines per page'),
|
|
233
|
+
filter: z.string().optional().describe('Text filter'),
|
|
234
|
+
level: z.string().optional().describe('Log level filter (INFO, WARN, ERROR)'),
|
|
235
|
+
}, async ({ filename, page, pageSize, filter, level }) => {
|
|
236
|
+
const queryParams = new URLSearchParams();
|
|
237
|
+
if (page) queryParams.set('page', String(page));
|
|
238
|
+
if (pageSize) queryParams.set('pageSize', String(pageSize));
|
|
239
|
+
if (filter) queryParams.set('filter', filter);
|
|
240
|
+
if (level) queryParams.set('level', level);
|
|
241
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/logs/${filename}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`);
|
|
242
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
server.tool('tail_log', 'Get last N lines from a log file', {
|
|
246
|
+
filename: z.string().describe('Log file name'),
|
|
247
|
+
lines: z.number().optional().default(50).describe('Number of lines to retrieve'),
|
|
248
|
+
}, async ({ filename, lines }) => {
|
|
249
|
+
const result = await fetchAPI(ENFYRA_API_URL, `/logs/${filename}/tail?lines=${lines}`);
|
|
250
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
server.tool('search_logs', 'Search for ERROR or WARN logs across recent log files', {
|
|
254
|
+
level: z.enum(['ERROR', 'WARN', 'INFO']).optional().default('ERROR').describe('Log level'),
|
|
255
|
+
keyword: z.string().optional().describe('Keyword to filter logs'),
|
|
256
|
+
limit: z.number().optional().default(50).describe('Max results per level'),
|
|
257
|
+
}, async ({ level, keyword, limit }) => {
|
|
258
|
+
const logFilesResult = await fetchAPI(ENFYRA_API_URL, '/logs');
|
|
259
|
+
const logFiles = logFilesResult.files || [];
|
|
260
|
+
const recentFiles = logFiles.filter(f => f.name.includes('app-') || f.name.includes('error-'));
|
|
261
|
+
const results = [];
|
|
262
|
+
for (const file of recentFiles.slice(0, 3)) {
|
|
263
|
+
try {
|
|
264
|
+
const contentResult = await fetchAPI(ENFYRA_API_URL, `/logs/${file.name}?level=${level}&pageSize=${limit}`);
|
|
265
|
+
const lines = contentResult.lines || contentResult.data || [];
|
|
266
|
+
const filteredLines = keyword ? lines.filter(l => JSON.stringify(l).toLowerCase().includes(keyword.toLowerCase())) : lines;
|
|
267
|
+
if (filteredLines.length > 0) results.push({ file: file.name, level, logs: filteredLines });
|
|
268
|
+
} catch (e) { /* skip */ }
|
|
269
|
+
}
|
|
270
|
+
return { content: [{ type: 'text', text: `Found ${results.length} files:\n${JSON.stringify(results, null, 2)}` }] };
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// AUTH & USER TOOLS
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
server.tool('get_current_user', 'Get current authenticated user info', {}, async () => {
|
|
278
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/me');
|
|
279
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
server.tool('get_all_roles', 'Get all role definitions', {}, async () => {
|
|
283
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/role_definition?limit=100');
|
|
284
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
server.tool('login', 'Force login to Enfyra and get new tokens', {
|
|
288
|
+
email: z.string().email().optional().describe('Admin email'),
|
|
289
|
+
password: z.string().optional().describe('Password'),
|
|
290
|
+
}, async ({ email, password }) => {
|
|
291
|
+
const loginEmail = email || ENFYRA_EMAIL;
|
|
292
|
+
const loginPassword = password || ENFYRA_PASSWORD;
|
|
293
|
+
if (!loginEmail || !loginPassword) throw new Error('Email and password required');
|
|
294
|
+
await login(ENFYRA_API_URL, loginEmail, loginPassword);
|
|
295
|
+
return { content: [{ type: 'text', text: `Logged in successfully!\nToken expires: ${new Date(getTokenExpiry()).toISOString()}` }] };
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// MENU & EXTENSION TOOLS
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
server.tool('create_menu', 'Create a menu item in the navigation', {
|
|
303
|
+
label: z.string().describe('Menu label'),
|
|
304
|
+
type: z.enum(['separator', 'link', 'route', 'dropdown', 'widget', 'extension']).describe('Menu type'),
|
|
305
|
+
icon: z.string().optional().describe('Lucide icon name'),
|
|
306
|
+
path: z.string().optional().describe('Route path for type=route'),
|
|
307
|
+
externalUrl: z.string().optional().describe('External URL for type=link'),
|
|
308
|
+
order: z.number().optional().default(0).describe('Display order'),
|
|
309
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable menu'),
|
|
310
|
+
description: z.string().optional().describe('Menu description'),
|
|
311
|
+
}, async (data) => {
|
|
312
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/menu_definition', { method: 'POST', body: JSON.stringify(data) });
|
|
313
|
+
return { content: [{ type: 'text', text: `Menu created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
server.tool('create_extension', 'Create a code extension (custom UI page or widget)', {
|
|
317
|
+
name: z.string().describe('Extension name (unique)'),
|
|
318
|
+
type: z.enum(['page', 'widget']).describe('Extension type'),
|
|
319
|
+
code: z.string().describe('Component code as string (React/Vue)'),
|
|
320
|
+
routePath: z.string().optional().describe('Route path for page type'),
|
|
321
|
+
menuLabel: z.string().optional().describe('Menu label (auto-creates menu)'),
|
|
322
|
+
menuIcon: z.string().optional().describe('Menu icon'),
|
|
323
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable extension'),
|
|
324
|
+
description: z.string().optional().describe('Extension description'),
|
|
325
|
+
}, async (data) => {
|
|
326
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/extension_definition', { method: 'POST', body: JSON.stringify(data) });
|
|
327
|
+
return { content: [{ type: 'text', text: `Extension created (ID: ${result.id}):\n${JSON.stringify(result, null, 2)}` }] };
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// MAIN
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
async function main() {
|
|
335
|
+
console.error('Starting Enfyra MCP Server...');
|
|
336
|
+
console.error(`API URL: ${ENFYRA_API_URL}`);
|
|
337
|
+
console.error(`Auth: ${ENFYRA_EMAIL ? `Configured (${ENFYRA_EMAIL})` : 'Not configured'}`);
|
|
338
|
+
|
|
339
|
+
const transport = new StdioServerTransport();
|
|
340
|
+
await server.connect(transport);
|
|
341
|
+
|
|
342
|
+
console.error('Enfyra MCP Server running on stdio');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
main().catch((error) => {
|
|
346
|
+
console.error('Fatal error:', error);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication module for Enfyra MCP Server
|
|
3
|
+
* Handles login, token refresh, and token validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Token state
|
|
7
|
+
let accessToken = null;
|
|
8
|
+
let refreshToken = null;
|
|
9
|
+
let tokenExpiry = null; // expTime từ server (milliseconds)
|
|
10
|
+
let isRefreshing = false;
|
|
11
|
+
|
|
12
|
+
// Config
|
|
13
|
+
let API_URL = 'http://localhost:3000/api';
|
|
14
|
+
let EMAIL = '';
|
|
15
|
+
let PASSWORD = '';
|
|
16
|
+
|
|
17
|
+
// Refresh buffer: refresh token 1 minute before expiry
|
|
18
|
+
const TOKEN_REFRESH_BUFFER = 60000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize auth module with config
|
|
22
|
+
*/
|
|
23
|
+
export function initAuth(apiUrl, email, password) {
|
|
24
|
+
API_URL = apiUrl;
|
|
25
|
+
EMAIL = email;
|
|
26
|
+
PASSWORD = password;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if token needs refresh (expires within 1 minute)
|
|
31
|
+
*/
|
|
32
|
+
export function needsRefresh() {
|
|
33
|
+
if (!tokenExpiry) return true;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
return now + TOKEN_REFRESH_BUFFER >= tokenExpiry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get current access token (does not refresh)
|
|
40
|
+
*/
|
|
41
|
+
export function getAccessToken() {
|
|
42
|
+
return accessToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get token expiry time
|
|
47
|
+
*/
|
|
48
|
+
export function getTokenExpiry() {
|
|
49
|
+
return tokenExpiry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Login and get access + refresh tokens
|
|
54
|
+
*/
|
|
55
|
+
export async function login(url, email, password) {
|
|
56
|
+
const apiUrl = url || API_URL;
|
|
57
|
+
const authEmail = email || EMAIL;
|
|
58
|
+
const authPassword = password || PASSWORD;
|
|
59
|
+
|
|
60
|
+
if (!authEmail || !authPassword) {
|
|
61
|
+
throw new Error('Email and password required');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error('[Auth] Logging in...');
|
|
65
|
+
const response = await fetch(`${apiUrl}/auth/login`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ email: authEmail, password: authPassword }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Login failed: ${await response.text()}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
accessToken = data.accessToken || data.access_token;
|
|
77
|
+
refreshToken = data.refreshToken || data.refresh_token;
|
|
78
|
+
tokenExpiry = data.expTime;
|
|
79
|
+
|
|
80
|
+
console.error(`[Auth] Logged in as ${authEmail}, token expires at ${new Date(tokenExpiry).toISOString()}`);
|
|
81
|
+
return accessToken;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Refresh access token using refresh token
|
|
86
|
+
*/
|
|
87
|
+
export async function refreshAccessToken(url, email, password) {
|
|
88
|
+
const apiUrl = url || API_URL;
|
|
89
|
+
const authEmail = email || EMAIL;
|
|
90
|
+
const authPassword = password || PASSWORD;
|
|
91
|
+
|
|
92
|
+
if (isRefreshing) {
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
94
|
+
return accessToken;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!refreshToken) {
|
|
98
|
+
console.error('[Auth] No refresh token, performing fresh login');
|
|
99
|
+
return await login(apiUrl, authEmail, authPassword);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isRefreshing = true;
|
|
103
|
+
try {
|
|
104
|
+
console.error('[Auth] Refreshing token...');
|
|
105
|
+
const response = await fetch(`${apiUrl}/auth/refresh-token`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({ refreshToken }),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
console.error('[Auth] Refresh failed, logging in fresh');
|
|
113
|
+
refreshToken = null;
|
|
114
|
+
return await login(apiUrl, authEmail, authPassword);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
accessToken = data.accessToken || data.access_token;
|
|
119
|
+
refreshToken = data.refreshToken || data.refresh_token;
|
|
120
|
+
tokenExpiry = data.expTime;
|
|
121
|
+
|
|
122
|
+
console.error(`[Auth] Token refreshed, expires at ${new Date(tokenExpiry).toISOString()}`);
|
|
123
|
+
return accessToken;
|
|
124
|
+
} finally {
|
|
125
|
+
isRefreshing = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get valid access token, refreshing if needed
|
|
131
|
+
*/
|
|
132
|
+
export async function getValidToken(url, email, password) {
|
|
133
|
+
const apiUrl = url || API_URL;
|
|
134
|
+
const authEmail = email || EMAIL;
|
|
135
|
+
const authPassword = password || PASSWORD;
|
|
136
|
+
|
|
137
|
+
if (!accessToken || needsRefresh()) {
|
|
138
|
+
if (refreshToken) {
|
|
139
|
+
return await refreshAccessToken(apiUrl, authEmail, authPassword);
|
|
140
|
+
}
|
|
141
|
+
return await login(apiUrl, authEmail, authPassword);
|
|
142
|
+
}
|
|
143
|
+
return accessToken;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reset token state (for logout)
|
|
148
|
+
*/
|
|
149
|
+
export function resetTokens() {
|
|
150
|
+
accessToken = null;
|
|
151
|
+
refreshToken = null;
|
|
152
|
+
tokenExpiry = null;
|
|
153
|
+
isRefreshing = false;
|
|
154
|
+
}
|
package/src/lib/fetch.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client module for Enfyra MCP Server
|
|
3
|
+
* Handles API requests with auth, timeout, and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getValidToken } from './auth.js';
|
|
7
|
+
|
|
8
|
+
// Timeout configuration
|
|
9
|
+
const FETCH_TIMEOUT = 30000; // 30 seconds
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Make HTTP request to Enfyra API
|
|
13
|
+
* @param {string} apiUrl - Base API URL
|
|
14
|
+
* @param {string} path - Request path
|
|
15
|
+
* @param {object} options - Fetch options
|
|
16
|
+
* @returns {Promise<any>} Response data
|
|
17
|
+
*/
|
|
18
|
+
export async function fetchAPI(apiUrl, path, options = {}) {
|
|
19
|
+
const url = `${apiUrl}${path}`;
|
|
20
|
+
const token = await getValidToken();
|
|
21
|
+
|
|
22
|
+
const headersList = [
|
|
23
|
+
['Content-Type', 'application/json'],
|
|
24
|
+
['Authorization', `Bearer ${token}`],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (options.headers) {
|
|
28
|
+
const optHeaders = options.headers;
|
|
29
|
+
for (const key of Object.keys(optHeaders)) {
|
|
30
|
+
const existingIdx = headersList.findIndex(h => h[0] === key);
|
|
31
|
+
if (existingIdx >= 0) {
|
|
32
|
+
headersList[existingIdx] = [key, optHeaders[key]];
|
|
33
|
+
} else {
|
|
34
|
+
headersList.push([key, optHeaders[key]]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, {
|
|
44
|
+
...options,
|
|
45
|
+
headers: headersList,
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
});
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const error = await res.text().catch(() => res.statusText);
|
|
52
|
+
throw new Error(`API error (${res.status}): ${error}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return res.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (error.name === 'AbortError') {
|
|
59
|
+
throw new Error(`Request timeout after ${FETCH_TIMEOUT}ms`);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate filter JSON and check for injection patterns
|
|
67
|
+
* @param {string} filterStr - Filter JSON string
|
|
68
|
+
* @returns {object|null} Parsed filter object
|
|
69
|
+
*/
|
|
70
|
+
export function validateFilter(filterStr) {
|
|
71
|
+
if (!filterStr) return null;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(filterStr);
|
|
74
|
+
// Check depth limit (max 10 levels)
|
|
75
|
+
function checkDepth(obj, depth = 0) {
|
|
76
|
+
if (depth > 10) {
|
|
77
|
+
throw new Error('Filter depth exceeds maximum of 10 levels');
|
|
78
|
+
}
|
|
79
|
+
if (obj && typeof obj === 'object') {
|
|
80
|
+
for (const key of Object.keys(obj)) {
|
|
81
|
+
checkDepth(obj[key], depth + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
checkDepth(parsed);
|
|
86
|
+
return parsed;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e.message.includes('depth')) throw e;
|
|
89
|
+
throw new Error(`Invalid filter JSON: ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate table name (alphanumeric, underscores only)
|
|
95
|
+
* @param {string} tableName - Table name to validate
|
|
96
|
+
* @returns {string} Validated table name
|
|
97
|
+
*/
|
|
98
|
+
export function validateTableName(tableName) {
|
|
99
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
100
|
+
throw new Error(`Invalid table name: ${tableName}. Must start with letter/underscore and contain only alphanumeric characters and underscores.`);
|
|
101
|
+
}
|
|
102
|
+
return tableName;
|
|
103
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table & Column tools for Enfyra MCP Server
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { fetchAPI, validateTableName } from './fetch.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register table tools with MCP server
|
|
9
|
+
*/
|
|
10
|
+
export function registerTableTools(server, ENFYRA_API_URL) {
|
|
11
|
+
server.tool(
|
|
12
|
+
'get_all_tables',
|
|
13
|
+
'Get all table definitions in the system',
|
|
14
|
+
{},
|
|
15
|
+
async () => {
|
|
16
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/table_definition?limit=500');
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
server.tool(
|
|
24
|
+
'create_table',
|
|
25
|
+
'Create a new table definition. After creating a table, use create_column to add columns.',
|
|
26
|
+
{
|
|
27
|
+
name: z.string().describe('Table name (e.g., "user_definition", "my_custom_table"). Must be unique, lowercase with underscores.'),
|
|
28
|
+
alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
|
|
29
|
+
description: z.string().optional().describe('Description of what this table stores.'),
|
|
30
|
+
isEnabled: z.boolean().optional().default(true).describe('Enable table. Set to false to disable.'),
|
|
31
|
+
},
|
|
32
|
+
async ({ name, alias, description, isEnabled }) => {
|
|
33
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/table_definition', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify({ name, alias, description, isEnabled }),
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: 'text', text: `Table created successfully with ID: ${result.id}. Next step: use create_column to add columns (tableId: ${result.id}).\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
server.tool(
|
|
44
|
+
'create_column',
|
|
45
|
+
'Create a column for an existing table. Columns cascade through table_definition.',
|
|
46
|
+
{
|
|
47
|
+
tableId: z.string().describe('Table definition ID (from get_all_tables or create_table).'),
|
|
48
|
+
name: z.string().describe('Column name (e.g., "title", "user_id"). Lowercase with underscores.'),
|
|
49
|
+
type: z.string().describe('Column type: varchar, int, text, boolean, datetime, json, decimal, etc.'),
|
|
50
|
+
length: z.number().optional().describe('Length for varchar types (e.g., 255).'),
|
|
51
|
+
isRequired: z.boolean().optional().default(false).describe('Set to true if column cannot be null.'),
|
|
52
|
+
isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
|
|
53
|
+
defaultValue: z.string().optional().describe('Default value as JSON string.'),
|
|
54
|
+
description: z.string().optional().describe('Column description.'),
|
|
55
|
+
},
|
|
56
|
+
async ({ tableId, name, type, length, isRequired, isUnique, defaultValue, description }) => {
|
|
57
|
+
const result = await fetchAPI(ENFYRA_API_URL, '/column_definition', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: JSON.stringify({ tableId, name, type, length, isRequired, isUnique, defaultValue, description }),
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: 'text', text: `Column created with ID: ${result.id}.\n\n${JSON.stringify(result, null, 2)}` }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|