@enfyra/mcp-server 0.0.1 → 0.0.2

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 CHANGED
@@ -1,22 +1,12 @@
1
1
  # Enfyra MCP Server
2
2
 
3
- MCP (Model Context Protocol) server for managing Enfyra instances via Claude Code.
3
+ MCP server for managing Enfyra instances from **Claude Code** (and other MCP clients). All operations go through Enfyra's REST API.
4
4
 
5
- ## Features
5
+ **LLM rules (REST, GraphQL, auth, URL, mutation `create_{tableName}`, etc.):** not in this README — see **`src/lib/mcp-instructions.js`** (content sent via MCP `instructions`) and tool descriptions in **`src/index.mjs`**. This README only covers **MCP installation and configuration** for users/devs.
6
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
7
+ ## Install in Claude Code
14
8
 
15
- ## Add to Claude Code
16
-
17
- ### Method 1: Global Configuration (Recommended)
18
-
19
- Edit `~/.claude.json` and add to the `mcpServers` section:
9
+ Edit `~/.claude.json` and add to `mcpServers`:
20
10
 
21
11
  ```json
22
12
  {
@@ -34,82 +24,21 @@ Edit `~/.claude.json` and add to the `mcpServers` section:
34
24
  }
35
25
  ```
36
26
 
37
- **Important**:
38
- - The `-y` flag automatically confirms installation
39
- - After updating config, restart Claude Code for changes to take effect
27
+ - `-y`: auto-confirm package install without prompting.
28
+ - **Restart Claude Code** after updating config.
29
+
30
+ **Local dev (running this repo):** use `node` with path to `src/index.mjs`; see `.mcp.json` in the project.
40
31
 
41
32
  | Variable | Description | Default |
42
33
  |----------|-------------|---------|
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
34
+ | `ENFYRA_API_URL` | API base URL (usually includes `/api`) | `http://localhost:3000/api` |
35
+ | `ENFYRA_EMAIL` | Admin email | |
36
+ | `ENFYRA_PASSWORD` | Admin password | |
75
37
 
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
38
+ ## Tools (summary)
82
39
 
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
- ```
40
+ Metadata, 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/index.mjs`.
109
41
 
110
- ## Security Notes
42
+ ## Security
111
43
 
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
44
+ API calls use JWT (MCP auto-refreshes). Permissions are enforced by Enfyra.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.mjs CHANGED
@@ -21,16 +21,22 @@ const ENFYRA_PASSWORD = process.env.ENFYRA_PASSWORD || '';
21
21
  // Import modules
22
22
  import { login, refreshAccessToken, getValidToken, resetTokens, getTokenExpiry, initAuth } from './lib/auth.js';
23
23
  import { fetchAPI, validateFilter, validateTableName } from './lib/fetch.js';
24
+ import { buildMcpServerInstructions, buildGraphqlUrls } from './lib/mcp-instructions.js';
24
25
  import { registerTableTools } from './lib/table-tools.js';
25
26
 
26
27
  // Initialize auth module
27
28
  initAuth(ENFYRA_API_URL, ENFYRA_EMAIL, ENFYRA_PASSWORD);
28
29
 
29
- // Create MCP server
30
- const server = new McpServer({
31
- name: 'enfyra-mcp',
32
- version: '1.0.0',
33
- });
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
+ );
34
40
 
35
41
  // ============================================================================
36
42
  // METADATA TOOLS
@@ -52,6 +58,40 @@ server.tool('get_table_metadata', 'Get metadata for a specific table by name', {
52
58
  // QUERY TOOLS
53
59
  // ============================================================================
54
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
+
55
95
  server.tool('query_table', 'Query any table in Enfyra with filters, sorting, and pagination', {
56
96
  tableName: z.string().describe('Table name to query'),
57
97
  filter: z.string().optional().describe('Filter object as JSON string. Examples: \'{"status": {"_eq": "active"}}\''),
@@ -75,19 +115,35 @@ server.tool('query_table', 'Query any table in Enfyra with filters, sorting, and
75
115
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
76
116
  });
77
117
 
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
- });
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
+ );
91
147
 
92
148
  // ============================================================================
93
149
  // CRUD TOOLS
@@ -0,0 +1,73 @@
1
+ /**
2
+ * MCP server `instructions` — surfaced to the host (e.g. Claude Code) for the LLM.
3
+ * Single source of truth for API/REST/GraphQL/auth/mutation naming; README does NOT feed the model.
4
+ * Maintain all assistant-facing rules here (and tool descriptions in index.mjs).
5
+ */
6
+
7
+ /** GraphQL shares the same URL prefix as REST: `{ENFYRA_API_URL}/graphql` and `{ENFYRA_API_URL}/graphql-schema`. */
8
+ export function buildGraphqlUrls(apiBaseUrl) {
9
+ const base = String(apiBaseUrl || '').replace(/\/$/, '');
10
+ return {
11
+ graphqlHttpUrl: `${base}/graphql`,
12
+ graphqlSchemaUrl: `${base}/graphql-schema`,
13
+ };
14
+ }
15
+
16
+ export function buildMcpServerInstructions(apiBaseUrl) {
17
+ const base = String(apiBaseUrl || '').replace(/\/$/, '');
18
+ const { graphqlHttpUrl, graphqlSchemaUrl } = buildGraphqlUrls(apiBaseUrl);
19
+ const getList = `${base}/<table_name>`;
20
+ const getOneById = `${base}/<table_name>?filter={"id":{"_eq":"<id>"}}&limit=1`;
21
+ const patchOne = `${base}/<table_name>/<id>`;
22
+ const delOne = `${base}/<table_name>/<id>`;
23
+ const examplePost = `${base}/post`;
24
+
25
+ return [
26
+ '## Enfyra API endpoints (answer user questions with these rules)',
27
+ '',
28
+ `**API base for this session:** \`${base}\` (from env ENFYRA_API_URL, no trailing slash).`,
29
+ `**Full URL:** base + path segment. Example for table \`post\`: \`${examplePost}\`.`,
30
+ '',
31
+ '### After a new table is created',
32
+ '- Enfyra creates a route at `/{table_name}` using the table **name** from `create_table` (not the alias).',
33
+ '- **Four REST HTTP operations** on that resource:',
34
+ ` - **GET** \`${getList}\` — list / filter (query: filter, sort, page, limit, fields, meta).`,
35
+ ` - **POST** \`${getList}\` — create (JSON body).`,
36
+ ` - **PATCH** \`${patchOne}\` — update one row.`,
37
+ ` - **DELETE** \`${delOne}\` — delete one row.`,
38
+ `- **No** **GET** \`${base}/<table_name>/<id>\`. For one row by id use **GET** \`${getOneById}\` or MCP \`query_table\` / \`find_one_record\`.`,
39
+ '',
40
+ '### Auth and publishedMethods (Enfyra server)',
41
+ '- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
42
+ '- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
43
+ '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
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
+ '',
46
+ '### Resolving the real REST path',
47
+ '- Do **not** assume `route_definition.path` always equals `table_definition.name`. Paths are data-driven (custom prefixes, renames, multiple routes per table).',
48
+ '- 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.',
49
+ '',
50
+ '### MongoDB vs SQL primary key',
51
+ '- On **SQL**, filters often use **`id`**. On **MongoDB**, documents may use **`_id`** — a filter for one row might be `{"_id":{"_eq":"..."}}` instead of `id`, depending on metadata.',
52
+ '',
53
+ '### GraphQL (same prefix as REST / ENFYRA_API_URL)',
54
+ `- **POST** \`${graphqlHttpUrl}\` — GraphQL endpoint (body: GraphQL query). Example with default base: \`http://localhost:3000/api/graphql\`.`,
55
+ `- **GET** \`${graphqlSchemaUrl}\` — current schema SDL (text), e.g. \`.../api/graphql-schema\`.`,
56
+ '- A table appears in the schema only if its route has **both** `GQL_QUERY` and `GQL_MUTATION` in `availableMethods`, `path` = `/<table_name>`, and `mainTable` set.',
57
+ '- **Query** field = same string as `table_definition.name`. **Mutations** are literal concat: `create_`+tableName, `update_`+tableName, `delete_`+tableName (e.g. tableName `post` → `create_post`, input type `postInput`). See `generate-type-defs.ts`. No mutations if no non-PK columns for input.',
58
+ '- **Auth:** `publishedMethods` may include `GQL_QUERY` and/or `GQL_MUTATION` **separately** — each controls anonymous access for queries vs mutations. Otherwise Bearer JWT + `routePermissions` must list the same method key (`GQL_QUERY` / `GQL_MUTATION`).',
59
+ '- MCP does not wrap GraphQL; use REST tools or tell users the URLs above.',
60
+ '',
61
+ '### MCP tool → HTTP',
62
+ `- \`get_all_metadata\` → GET \`${base}/metadata\``,
63
+ `- \`get_table_metadata\` → GET \`${base}/metadata/<tableName>\``,
64
+ `- \`query_table\` → GET \`${base}/<tableName>?…\` (query string from tool args)`,
65
+ `- \`find_one_record\` (by id) → GET \`${base}/<tableName>?filter=…&limit=1\``,
66
+ `- \`create_record\` → POST \`${base}/<tableName>\``,
67
+ `- \`update_record\` → PATCH \`${base}/<tableName>/<id>\``,
68
+ `- \`delete_record\` → DELETE \`${base}/<tableName>/<id>\``,
69
+ `- Other admin paths include \`${base}/route_definition\`, \`${base}/admin/reload\`, etc.`,
70
+ '',
71
+ '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.',
72
+ ].join('\n');
73
+ }
@@ -8,6 +8,7 @@ import { fetchAPI, validateTableName } from './fetch.js';
8
8
  * Register table tools with MCP server
9
9
  */
10
10
  export function registerTableTools(server, ENFYRA_API_URL) {
11
+ const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
11
12
  server.tool(
12
13
  'get_all_tables',
13
14
  'Get all table definitions in the system',
@@ -22,7 +23,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
22
23
 
23
24
  server.tool(
24
25
  'create_table',
25
- 'Create a new table definition. After creating a table, use create_column to add columns.',
26
+ [
27
+ 'Create a new table definition. After creating a table, use create_column to add columns.',
28
+ 'Enfyra auto-creates a REST route at path `/<table_name>` (same segment as `name`, not alias).',
29
+ '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
+ '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.',
31
+ `Full URLs: ${apiBase}/<table_name> (example table post: ${apiBase}/post).`,
32
+ 'GraphQL (GQL_QUERY / GQL_MUTATION) may also be enabled on the route; that is separate from REST paths above.',
33
+ ].join(' '),
26
34
  {
27
35
  name: z.string().describe('Table name (e.g., "user_definition", "my_custom_table"). Must be unique, lowercase with underscores.'),
28
36
  alias: z.string().optional().describe('Table alias for API. If not provided, the table name will be used.'),
@@ -34,8 +42,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
34
42
  method: 'POST',
35
43
  body: JSON.stringify({ name, alias, description, isEnabled }),
36
44
  });
45
+ const base = ENFYRA_API_URL.replace(/\/$/, '');
46
+ const routePath = `/${name}`;
47
+ const restHint = [
48
+ `Auto route path: ${routePath} → full base for REST: ${base}${routePath}`,
49
+ `REST: GET+POST on ${routePath}; PATCH+DELETE on ${routePath}/:id only. No GET ${routePath}/:id.`,
50
+ ].join('\n');
37
51
  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)}` }],
52
+ content: [{ type: 'text', text: `Table created successfully with ID: ${result.id}. Next step: use create_column to add columns (tableId: ${result.id}).\n${restHint}\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
39
53
  };
40
54
  }
41
55
  );