icode-mcp-adapter 1.0.3 → 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.
|
|
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",
|
package/src/engine/McpEngine.js
CHANGED
|
@@ -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
|
|
26
|
+
* Create an MCP server from a LIGHTWEIGHT config (auto-resolve columns).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
-
// ──
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
if (row.
|
|
183
|
-
|
|
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(),
|