icode-mcp-adapter 1.0.4 → 1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icode-mcp-adapter",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Dynamic MCP server adapter — auto-generates CRUD tools from schema configs. Plugs into icode-server via Fastify or runs standalone.",
5
5
  "type": "module",
6
6
  "main": "src/engine/McpEngine.js",
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { registerTableTools } from './ToolBuilder.js';
3
- import { resolveSchemas } from './SchemaAdapter.js';
3
+ import { resolveSchemas as defaultResolveSchemas } from './SchemaAdapter.js';
4
4
 
5
5
  /**
6
6
  * Create an MCP server from a FULL app config (tables already defined).
@@ -23,26 +23,19 @@ export function createMcpServer(config, db) {
23
23
  }
24
24
 
25
25
  /**
26
- * Create an MCP server from a LIGHTWEIGHT config (auto-resolve columns from DB).
26
+ * Create an MCP server from a LIGHTWEIGHT config (auto-resolve columns).
27
27
  *
28
- * Queries INFORMATION_SCHEMA to discover columns, types, PKs, defaults.
29
- * Only needs a simple config listing which tables + operations to expose.
30
- *
31
- * @param {{ name: string, version?: string, tables: Record<string, import('./SchemaAdapter.js').TableConfig> }} config
28
+ * @param {Object} config - app config with tables
32
29
  * @param {{ query: Function }} db - Pool or SQLService
30
+ * @param {{ resolveSchemas?: Function }} [opts]
31
+ * resolveSchemas: (db, tableConfigs, opts) => Promise<TableSchema[]>
32
+ * Defaults to INFORMATION_SCHEMA-based resolver.
33
+ * Inject your own to use icode-server's SchemasService or any other source.
33
34
  * @returns {Promise<McpServer>}
34
- *
35
- * @example
36
- * const server = await createMcpServerAuto({
37
- * name: 'paaal',
38
- * tables: {
39
- * plans: { operations: ['list','get','update'], pkType: 'uuid' },
40
- * achievements: { operations: ['list','get','add','update'] },
41
- * actions: { operations: ['list','get','add','update'] },
42
- * }
43
- * }, db);
44
35
  */
45
- export async function createMcpServerAuto(config, db) {
36
+ export async function createMcpServerAuto(config, db, opts = {}) {
37
+ const resolveSchemas = opts.resolveSchemas || defaultResolveSchemas;
38
+
46
39
  const tables = await resolveSchemas(db, config.tables, {
47
40
  cacheKey: config.appName,
48
41
  });
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Auto-derives MCP TableSchema from the live database (INFORMATION_SCHEMA).
2
+ * Auto-derives MCP TableSchema from INFORMATION_SCHEMA.
3
3
  *
4
4
  * Follows icode-server conventions:
5
5
  * - PK: `guid` (string)
6
6
  * - Soft delete: `_deleted` (0 = active, 1 = deleted)
7
7
  * - Timestamp: `_created`
8
- * - Meta fields: `_time`, `_by`, `_createdBy`, `pk`, etc. → readOnly
8
+ * - Meta fields: `_created`, `_createdBy`, `_time`, `_by`, `_deleted`, `pk` → readOnly
9
9
  *
10
10
  * Works with:
11
11
  * - mysql2/promise Pool.query() (standalone)
@@ -14,7 +14,7 @@
14
14
 
15
15
  // ── MySQL → MCP type mapping ─────────────────────────────────────────────────
16
16
 
17
- const MYSQL_TYPE_MAP = {
17
+ const DATATYPE_MAP = {
18
18
  varchar: 'string',
19
19
  char: 'string',
20
20
  tinytext: 'string',
@@ -25,6 +25,7 @@ const MYSQL_TYPE_MAP = {
25
25
  bigint: 'number',
26
26
  smallint: 'number',
27
27
  mediumint: 'number',
28
+ tinyint: 'number',
28
29
  float: 'number',
29
30
  double: 'number',
30
31
  decimal: 'number',
@@ -35,9 +36,8 @@ const MYSQL_TYPE_MAP = {
35
36
  enum: 'enum',
36
37
  };
37
38
 
38
- // ── icode-server meta fields (auto-managed, always readOnly) ─────────────────
39
+ // ── Meta fields (auto-managed by icode-server, always readOnly) ──────────────
39
40
 
40
- // Meta fields auto-managed by icode-server — always readOnly
41
41
  const META_FIELDS = new Set([
42
42
  '_created', // created timestamp (epoch ms)
43
43
  '_createdBy', // who created
@@ -53,21 +53,12 @@ const cache = new Map();
53
53
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
54
54
 
55
55
  /**
56
- * Resolve full TableSchema[] from a lightweight config + live DB.
56
+ * Resolve full TableSchema[] from config + INFORMATION_SCHEMA.
57
57
  *
58
58
  * @param {{ query: Function }} db - Pool or SQLService
59
- * @param {Record<string, TableConfig>} tableConfigs - lightweight per-table config
59
+ * @param {Record<string, TableConfig>} tableConfigs
60
60
  * @param {{ cacheKey?: string }} [opts]
61
61
  * @returns {Promise<import('./types.js').TableSchema[]>}
62
- *
63
- * @typedef {Object} TableConfig
64
- * @property {('list'|'get'|'add'|'update'|'delete')[]} operations
65
- * @property {'auto'|'string'} [pkType] - override PK type detection
66
- * @property {boolean} [softDelete] - default: auto-detected (_deleted col)
67
- * @property {string} [singular] - override singular derivation
68
- * @property {string} [displayName] - override display name
69
- * @property {string} [listOrderBy] - default: `_created` DESC
70
- * @property {Record<string, Partial<import('./types.js').ColumnDef>>} [columns] - per-column overrides
71
62
  */
72
63
  export async function resolveSchemas(db, tableConfigs, opts = {}) {
73
64
  // Check cache
@@ -78,7 +69,6 @@ export async function resolveSchemas(db, tableConfigs, opts = {}) {
78
69
  }
79
70
  }
80
71
 
81
- // Detect current database name
82
72
  const [dbRows] = await db.query('SELECT DATABASE() as db');
83
73
  const dbName = dbRows[0].db;
84
74
 
@@ -99,16 +89,14 @@ export async function resolveSchemas(db, tableConfigs, opts = {}) {
99
89
  continue;
100
90
  }
101
91
 
102
- // ── Detect primary key ────────────────────────────────────────────────
103
- // icode-server uses `guid` (string), fallback to `pk` (auto-increment)
92
+ // ── Detect primary key ──────────────────────────────────────────────
104
93
  const hasGuid = rows.some(r => r.COLUMN_NAME === 'guid');
105
94
  const hasPk = rows.some(r => r.COLUMN_NAME === 'pk');
106
95
  const pkName = config.primaryKey || (hasGuid ? 'guid' : (hasPk ? 'pk' : 'guid'));
107
96
  const pkActualRow = rows.find(r => r.COLUMN_NAME === pkName);
108
97
  const isStringPk = pkActualRow?.DATA_TYPE === 'varchar';
109
98
 
110
- // ── Detect soft delete ────────────────────────────────────────────────
111
- // icode-server standard: _deleted = 0 (not deleted), _deleted = 1 (deleted)
99
+ // ── Detect soft delete ──────────────────────────────────────────────
112
100
  const hasDeletedCol = rows.some(r => r.COLUMN_NAME === '_deleted');
113
101
 
114
102
  let softDeleteFilter = null;
@@ -119,22 +107,23 @@ export async function resolveSchemas(db, tableConfigs, opts = {}) {
119
107
  softDeleteSet = '`_deleted` = 1';
120
108
  }
121
109
 
122
- // ── Build columns ─────────────────────────────────────────────────────
110
+ // ── Build columns ───────────────────────────────────────────────────
123
111
  const columns = rows.map(row => {
124
112
  const col = buildColumn(row, pkName);
125
113
 
126
- // Apply per-column overrides from config
114
+ // Apply per-column overrides from config (description, filterable)
127
115
  if (config.columns?.[row.COLUMN_NAME]) {
128
- Object.assign(col, config.columns[row.COLUMN_NAME]);
116
+ const override = config.columns[row.COLUMN_NAME];
117
+ if (override.description) col.description = override.description;
118
+ if (override.filterable) col.filterable = true;
119
+ if (override.required !== undefined) col.required = override.required;
120
+ if (override.nullable !== undefined) col.nullable = override.nullable;
129
121
  }
130
122
 
131
123
  return col;
132
124
  });
133
125
 
134
- // ── Derive singular name ──────────────────────────────────────────────
135
126
  const singular = config.singular || deriveSingular(tableName);
136
-
137
- // ── Default ORDER BY ──────────────────────────────────────────────────
138
127
  const hasCreated = rows.some(r => r.COLUMN_NAME === '_created');
139
128
  const defaultOrder = hasCreated ? '`_created` DESC' : undefined;
140
129
 
@@ -178,37 +167,16 @@ function buildColumn(row, pkName) {
178
167
  type: resolveType(row),
179
168
  };
180
169
 
181
- // Primary key readOnly
182
- if (row.COLUMN_NAME === pkName) {
183
- col.readOnly = true;
184
- }
185
-
186
- // Auto-increment → readOnly
187
- if (row.EXTRA?.includes('auto_increment')) {
188
- col.readOnly = true;
189
- }
190
-
191
- // _created → autoSet
192
- if (row.COLUMN_NAME === '_created') {
193
- col.autoSet = 'created_at';
194
- }
170
+ if (row.COLUMN_NAME === pkName) col.readOnly = true;
171
+ if (row.EXTRA?.includes('auto_increment')) col.readOnly = true;
172
+ if (row.COLUMN_NAME === '_created') col.autoSet = 'created_at';
173
+ if (META_FIELDS.has(row.COLUMN_NAME)) col.readOnly = true;
174
+ if (row.IS_NULLABLE === 'YES') col.nullable = true;
195
175
 
196
- // icode-server meta fields → readOnly (auto-managed by the platform)
197
- if (META_FIELDS.has(row.COLUMN_NAME)) {
198
- col.readOnly = true;
199
- }
200
-
201
- // Nullable
202
- if (row.IS_NULLABLE === 'YES') {
203
- col.nullable = true;
204
- }
205
-
206
- // Default value
207
176
  if (row.COLUMN_DEFAULT !== null && row.COLUMN_DEFAULT !== undefined) {
208
177
  col.default = parseDefault(row);
209
178
  }
210
179
 
211
- // Required: NOT NULL, no default, not auto-set, not PK, not meta
212
180
  if (row.IS_NULLABLE === 'NO'
213
181
  && row.COLUMN_DEFAULT === null
214
182
  && !col.readOnly
@@ -217,14 +185,12 @@ function buildColumn(row, pkName) {
217
185
  col.required = true;
218
186
  }
219
187
 
220
- // Filterable: indexed columns and guid/FK references
221
188
  if (row.COLUMN_KEY === 'MUL'
222
189
  || (row.COLUMN_NAME.endsWith('_id') && row.COLUMN_NAME !== pkName)
223
190
  || (row.COLUMN_NAME === 'guid' && row.COLUMN_NAME !== pkName)) {
224
191
  col.filterable = true;
225
192
  }
226
193
 
227
- // Enum values
228
194
  if (row.DATA_TYPE === 'enum') {
229
195
  col.enumValues = parseEnum(row.COLUMN_TYPE);
230
196
  }
@@ -232,19 +198,11 @@ function buildColumn(row, pkName) {
232
198
  return col;
233
199
  }
234
200
 
235
- // ── Type resolution ──────────────────────────────────────────────────────────
236
-
237
201
  function resolveType(row) {
238
- // tinyint(1) → boolean
239
202
  if (row.COLUMN_TYPE === 'tinyint(1)') return 'boolean';
240
-
241
- // bigint for timestamp columns → timestamp (epoch ms)
242
203
  if (row.DATA_TYPE === 'bigint' && (row.COLUMN_NAME === '_created' || row.COLUMN_NAME === '_time')) return 'timestamp';
243
-
244
- // varchar(36) named guid → string (not uuid — icode guids aren't always uuid format)
245
204
  if (row.COLUMN_NAME === 'guid') return 'string';
246
-
247
- return MYSQL_TYPE_MAP[row.DATA_TYPE] || 'string';
205
+ return DATATYPE_MAP[row.DATA_TYPE] || 'string';
248
206
  }
249
207
 
250
208
  function parseEnum(columnType) {
@@ -47,11 +47,15 @@ function parseAppParam(param) {
47
47
  * — app configs keyed by appName: { paaal: paaalConfig }
48
48
  * @param {(appName: string, env: string) => Promise<{ query: Function }>} opts.getDb
49
49
  * — DB provider function, e.g. (name, env) => getAppDbService(name, env)
50
+ * @param {(db, tableConfigs, opts) => Promise<TableSchema[]>} [opts.resolveSchemas]
51
+ * — Custom schema resolver. Default: INFORMATION_SCHEMA.
52
+ * — Inject your own to use SchemasService or any other source.
50
53
  * @param {string} [opts.prefix='/v1/mcp']
51
54
  * @param {boolean} [opts.public=true]
52
55
  */
53
56
  export default async function mcpPlugin(fastify, opts = {}) {
54
57
  const getDb = opts.getDb;
58
+ const resolveSchemas = opts.resolveSchemas;
55
59
  const prefix = opts.prefix || '/v1/mcp';
56
60
  const isPublic = opts.public ?? true;
57
61
 
@@ -91,7 +95,7 @@ export default async function mcpPlugin(fastify, opts = {}) {
91
95
  } catch (err) {
92
96
  return reply.code(400).send({ ok: false, error: err.message || err.sqlMessage || 'Failed to get DB connection' });
93
97
  }
94
- const server = await createMcpServerAuto(appConfig, db);
98
+ const server = await createMcpServerAuto(appConfig, db, { resolveSchemas });
95
99
 
96
100
  const transport = new StreamableHTTPServerTransport({
97
101
  sessionIdGenerator: () => randomUUID(),