icode-mcp-adapter 1.0.5 → 1.0.6
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.6",
|
|
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
|
@@ -7,16 +7,17 @@ import { resolveSchemas as defaultResolveSchemas } from './SchemaAdapter.js';
|
|
|
7
7
|
*
|
|
8
8
|
* @param {import('./types.js').AppConfig} config - with tables[] fully specified
|
|
9
9
|
* @param {{ query: Function }} db
|
|
10
|
+
* @param {{ userId: string }} [identity] - Caller identity; required when a table is owner-scoped
|
|
10
11
|
* @returns {McpServer}
|
|
11
12
|
*/
|
|
12
|
-
export function createMcpServer(config, db) {
|
|
13
|
+
export function createMcpServer(config, db, identity) {
|
|
13
14
|
const server = new McpServer({
|
|
14
15
|
name: `${config.appName}-mcp`,
|
|
15
16
|
version: config.version,
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
for (const table of config.tables) {
|
|
19
|
-
registerTableTools(server, db, table);
|
|
20
|
+
registerTableTools(server, db, table, identity);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
return server;
|
|
@@ -27,22 +28,24 @@ export function createMcpServer(config, db) {
|
|
|
27
28
|
*
|
|
28
29
|
* @param {Object} config - app config with tables
|
|
29
30
|
* @param {{ query: Function }} db - Pool or SQLService
|
|
30
|
-
* @param {{ resolveSchemas?: Function }} [opts]
|
|
31
|
+
* @param {{ resolveSchemas?: Function, identity?: { userId: string } }} [opts]
|
|
31
32
|
* resolveSchemas: (db, tableConfigs, opts) => Promise<TableSchema[]>
|
|
32
33
|
* Defaults to INFORMATION_SCHEMA-based resolver.
|
|
33
34
|
* Inject your own to use icode-server's SchemasService or any other source.
|
|
35
|
+
* identity: caller identity used to scope owner-scoped tables.
|
|
34
36
|
* @returns {Promise<McpServer>}
|
|
35
37
|
*/
|
|
36
38
|
export async function createMcpServerAuto(config, db, opts = {}) {
|
|
37
39
|
const resolveSchemas = opts.resolveSchemas || defaultResolveSchemas;
|
|
38
40
|
|
|
39
41
|
const tables = await resolveSchemas(db, config.tables, {
|
|
40
|
-
cacheKey: config.appName
|
|
42
|
+
cacheKey: `${config.appName}--${config.env || 'prod'}`,
|
|
43
|
+
ownerColumn: config.ownerColumn,
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
return createMcpServer({
|
|
44
47
|
appName: config.appName,
|
|
45
48
|
version: config.version || '1.0.0',
|
|
46
49
|
tables,
|
|
47
|
-
}, db);
|
|
50
|
+
}, db, opts.identity);
|
|
48
51
|
}
|
|
@@ -57,7 +57,8 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
57
57
|
*
|
|
58
58
|
* @param {{ query: Function }} db - Pool or SQLService
|
|
59
59
|
* @param {Record<string, TableConfig>} tableConfigs
|
|
60
|
-
* @param {{ cacheKey?: string }} [opts]
|
|
60
|
+
* @param {{ cacheKey?: string, ownerColumn?: string }} [opts]
|
|
61
|
+
* ownerColumn: app-level default owner column (per-table `ownerColumn` overrides; `null` opts out)
|
|
61
62
|
* @returns {Promise<import('./types.js').TableSchema[]>}
|
|
62
63
|
*/
|
|
63
64
|
export async function resolveSchemas(db, tableConfigs, opts = {}) {
|
|
@@ -127,6 +128,11 @@ export async function resolveSchemas(db, tableConfigs, opts = {}) {
|
|
|
127
128
|
const hasCreated = rows.some(r => r.COLUMN_NAME === '_created');
|
|
128
129
|
const defaultOrder = hasCreated ? '`_created` DESC' : undefined;
|
|
129
130
|
|
|
131
|
+
const ownerColumn = resolveOwnerColumn(config, opts.ownerColumn);
|
|
132
|
+
if (ownerColumn && !rows.some(r => r.COLUMN_NAME === ownerColumn)) {
|
|
133
|
+
throw new Error(`[MCP] Table '${tableName}' is owner-scoped on '${ownerColumn}' but no such column exists in '${dbName}'`);
|
|
134
|
+
}
|
|
135
|
+
|
|
130
136
|
tables.push({
|
|
131
137
|
name: tableName,
|
|
132
138
|
singular,
|
|
@@ -138,6 +144,7 @@ export async function resolveSchemas(db, tableConfigs, opts = {}) {
|
|
|
138
144
|
softDeleteSet,
|
|
139
145
|
operations: config.operations || ['list', 'get', 'add', 'update'],
|
|
140
146
|
listOrderBy: config.listOrderBy || defaultOrder,
|
|
147
|
+
ownerColumn,
|
|
141
148
|
columns,
|
|
142
149
|
...(config.customDescriptions && { customDescriptions: config.customDescriptions }),
|
|
143
150
|
});
|
|
@@ -226,6 +233,11 @@ function parseDefault(row) {
|
|
|
226
233
|
|
|
227
234
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
228
235
|
|
|
236
|
+
/** Owner column for a table: per-table override → app default → none (null). */
|
|
237
|
+
function resolveOwnerColumn(config, appDefault) {
|
|
238
|
+
return config.ownerColumn !== undefined ? config.ownerColumn : (appDefault ?? null);
|
|
239
|
+
}
|
|
240
|
+
|
|
229
241
|
function deriveSingular(name) {
|
|
230
242
|
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
|
|
231
243
|
if (name.endsWith('ses')) return name.slice(0, -2);
|
|
@@ -37,14 +37,26 @@ function zodFor(col, opts = {}) {
|
|
|
37
37
|
return s;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Get writable columns (exclude readOnly, autoSet, and
|
|
40
|
+
/** Get writable columns (exclude readOnly, autoSet, primaryKey, and the owner column) */
|
|
41
41
|
function writableCols(table) {
|
|
42
|
-
return table.columns.filter(c =>
|
|
42
|
+
return table.columns.filter(c =>
|
|
43
|
+
!c.readOnly && !c.autoSet && c.name !== table.primaryKey && c.name !== table.ownerColumn);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/** Get filterable columns */
|
|
46
|
+
/** Get filterable columns (the owner column is implicit, never a caller filter) */
|
|
46
47
|
function filterCols(table) {
|
|
47
|
-
return table.columns.filter(c => c.filterable);
|
|
48
|
+
return table.columns.filter(c => c.filterable && c.name !== table.ownerColumn);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** WHERE clause + params targeting one row by PK, scoped to the owner when set. */
|
|
52
|
+
function pkWhere(table, pkVal, owner) {
|
|
53
|
+
let clause = `${q(table.primaryKey)} = ?`;
|
|
54
|
+
const params = [pkVal];
|
|
55
|
+
if (owner) {
|
|
56
|
+
clause += ` AND ${q(owner.col)} = ?`;
|
|
57
|
+
params.push(owner.id);
|
|
58
|
+
}
|
|
59
|
+
return { clause, params };
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** Build PK Zod schema */
|
|
@@ -61,24 +73,35 @@ function pkSchema(table) {
|
|
|
61
73
|
* @param {{ query: (sql: string, params: any[]) => Promise<[any[], any]> }} db
|
|
62
74
|
* — any object with query(sql, params) → [rows, fields]
|
|
63
75
|
* @param {import('./types.js').TableSchema} table
|
|
76
|
+
* @param {{ userId: string }} [identity] - Caller identity; required for owner-scoped tables.
|
|
64
77
|
*/
|
|
65
|
-
export function registerTableTools(server, db, table) {
|
|
78
|
+
export function registerTableTools(server, db, table, identity) {
|
|
66
79
|
const display = table.displayName || table.singular;
|
|
67
80
|
|
|
81
|
+
// Row-level ownership: scope every query to the calling user. Fail closed —
|
|
82
|
+
// an owner-scoped table is never served without an identity.
|
|
83
|
+
let owner = null;
|
|
84
|
+
if (table.ownerColumn) {
|
|
85
|
+
if (!identity?.userId) {
|
|
86
|
+
throw new Error(`[MCP] '${table.name}' is owner-scoped but no user identity was provided`);
|
|
87
|
+
}
|
|
88
|
+
owner = { col: table.ownerColumn, id: identity.userId };
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
for (const op of table.operations) {
|
|
69
92
|
switch (op) {
|
|
70
|
-
case 'list': regList(server, db, table, display); break;
|
|
71
|
-
case 'get': regGet(server, db, table, display); break;
|
|
72
|
-
case 'add': regAdd(server, db, table, display); break;
|
|
73
|
-
case 'update': regUpdate(server, db, table, display); break;
|
|
74
|
-
case 'delete': regDelete(server, db, table, display); break;
|
|
93
|
+
case 'list': regList(server, db, table, display, owner); break;
|
|
94
|
+
case 'get': regGet(server, db, table, display, owner); break;
|
|
95
|
+
case 'add': regAdd(server, db, table, display, owner); break;
|
|
96
|
+
case 'update': regUpdate(server, db, table, display, owner); break;
|
|
97
|
+
case 'delete': regDelete(server, db, table, display, owner); break;
|
|
75
98
|
}
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
// ── LIST ──────────────────────────────────────────────────────────────────────
|
|
80
103
|
|
|
81
|
-
function regList(server, db, table, display) {
|
|
104
|
+
function regList(server, db, table, display, owner) {
|
|
82
105
|
const filters = filterCols(table);
|
|
83
106
|
const inputSchema = {};
|
|
84
107
|
for (const f of filters) {
|
|
@@ -96,6 +119,10 @@ function regList(server, db, table, display) {
|
|
|
96
119
|
const vals = [];
|
|
97
120
|
|
|
98
121
|
if (table.softDeleteFilter) where.push(table.softDeleteFilter);
|
|
122
|
+
if (owner) {
|
|
123
|
+
where.push(`${q(owner.col)} = ?`);
|
|
124
|
+
vals.push(owner.id);
|
|
125
|
+
}
|
|
99
126
|
|
|
100
127
|
for (const f of filters) {
|
|
101
128
|
if (args[f.name] !== undefined && args[f.name] !== null) {
|
|
@@ -121,7 +148,7 @@ function regList(server, db, table, display) {
|
|
|
121
148
|
|
|
122
149
|
// ── GET ───────────────────────────────────────────────────────────────────────
|
|
123
150
|
|
|
124
|
-
function regGet(server, db, table, display) {
|
|
151
|
+
function regGet(server, db, table, display, owner) {
|
|
125
152
|
server.registerTool(`get_${table.singular}`, {
|
|
126
153
|
title: `Get ${display}`,
|
|
127
154
|
description: table.customDescriptions?.get
|
|
@@ -129,9 +156,10 @@ function regGet(server, db, table, display) {
|
|
|
129
156
|
inputSchema: { [table.primaryKey]: pkSchema(table) },
|
|
130
157
|
}, async (args) => {
|
|
131
158
|
try {
|
|
159
|
+
const { clause, params } = pkWhere(table, args[table.primaryKey], owner);
|
|
132
160
|
const [rows] = await db.query(
|
|
133
|
-
`SELECT * FROM ${q(table.name)} WHERE ${
|
|
134
|
-
|
|
161
|
+
`SELECT * FROM ${q(table.name)} WHERE ${clause} LIMIT 1`,
|
|
162
|
+
params,
|
|
135
163
|
);
|
|
136
164
|
const row = rows[0] || null;
|
|
137
165
|
return {
|
|
@@ -146,7 +174,7 @@ function regGet(server, db, table, display) {
|
|
|
146
174
|
|
|
147
175
|
// ── ADD ───────────────────────────────────────────────────────────────────────
|
|
148
176
|
|
|
149
|
-
function regAdd(server, db, table, display) {
|
|
177
|
+
function regAdd(server, db, table, display, owner) {
|
|
150
178
|
const cols = writableCols(table);
|
|
151
179
|
const inputSchema = {};
|
|
152
180
|
for (const c of cols) {
|
|
@@ -182,6 +210,13 @@ function regAdd(server, db, table, display) {
|
|
|
182
210
|
placeholders.push('?');
|
|
183
211
|
}
|
|
184
212
|
|
|
213
|
+
// Owner scope — force the owning user; never trust client input
|
|
214
|
+
if (owner) {
|
|
215
|
+
insertCols.push(q(owner.col));
|
|
216
|
+
insertVals.push(owner.id);
|
|
217
|
+
placeholders.push('?');
|
|
218
|
+
}
|
|
219
|
+
|
|
185
220
|
// User-provided columns
|
|
186
221
|
for (const c of cols) {
|
|
187
222
|
if (args[c.name] !== undefined) {
|
|
@@ -213,7 +248,7 @@ function regAdd(server, db, table, display) {
|
|
|
213
248
|
|
|
214
249
|
// ── UPDATE ────────────────────────────────────────────────────────────────────
|
|
215
250
|
|
|
216
|
-
function regUpdate(server, db, table, display) {
|
|
251
|
+
function regUpdate(server, db, table, display, owner) {
|
|
217
252
|
const cols = writableCols(table);
|
|
218
253
|
const inputSchema = { [table.primaryKey]: pkSchema(table) };
|
|
219
254
|
for (const c of cols) {
|
|
@@ -227,7 +262,6 @@ function regUpdate(server, db, table, display) {
|
|
|
227
262
|
inputSchema,
|
|
228
263
|
}, async (args) => {
|
|
229
264
|
try {
|
|
230
|
-
const pkVal = args[table.primaryKey];
|
|
231
265
|
const setClauses = [];
|
|
232
266
|
const vals = [];
|
|
233
267
|
|
|
@@ -242,15 +276,15 @@ function regUpdate(server, db, table, display) {
|
|
|
242
276
|
return { content: [{ type: 'text', text: 'No fields to update.' }] };
|
|
243
277
|
}
|
|
244
278
|
|
|
245
|
-
|
|
279
|
+
const target = pkWhere(table, args[table.primaryKey], owner);
|
|
246
280
|
await db.query(
|
|
247
|
-
`UPDATE ${q(table.name)} SET ${setClauses.join(', ')} WHERE ${
|
|
248
|
-
vals,
|
|
281
|
+
`UPDATE ${q(table.name)} SET ${setClauses.join(', ')} WHERE ${target.clause}`,
|
|
282
|
+
[...vals, ...target.params],
|
|
249
283
|
);
|
|
250
284
|
|
|
251
285
|
const [rows] = await db.query(
|
|
252
|
-
`SELECT * FROM ${q(table.name)} WHERE ${
|
|
253
|
-
|
|
286
|
+
`SELECT * FROM ${q(table.name)} WHERE ${target.clause}`,
|
|
287
|
+
target.params,
|
|
254
288
|
);
|
|
255
289
|
|
|
256
290
|
return {
|
|
@@ -265,7 +299,7 @@ function regUpdate(server, db, table, display) {
|
|
|
265
299
|
|
|
266
300
|
// ── DELETE ────────────────────────────────────────────────────────────────────
|
|
267
301
|
|
|
268
|
-
function regDelete(server, db, table, display) {
|
|
302
|
+
function regDelete(server, db, table, display, owner) {
|
|
269
303
|
server.registerTool(`delete_${table.singular}`, {
|
|
270
304
|
title: `Delete ${display}`,
|
|
271
305
|
description: table.customDescriptions?.delete
|
|
@@ -273,17 +307,17 @@ function regDelete(server, db, table, display) {
|
|
|
273
307
|
inputSchema: { [table.primaryKey]: pkSchema(table) },
|
|
274
308
|
}, async (args) => {
|
|
275
309
|
try {
|
|
276
|
-
const
|
|
310
|
+
const { clause, params } = pkWhere(table, args[table.primaryKey], owner);
|
|
277
311
|
|
|
278
312
|
if (table.softDeleteSet) {
|
|
279
313
|
await db.query(
|
|
280
|
-
`UPDATE ${q(table.name)} SET ${table.softDeleteSet} WHERE ${
|
|
281
|
-
|
|
314
|
+
`UPDATE ${q(table.name)} SET ${table.softDeleteSet} WHERE ${clause}`,
|
|
315
|
+
params,
|
|
282
316
|
);
|
|
283
317
|
} else {
|
|
284
318
|
await db.query(
|
|
285
|
-
`DELETE FROM ${q(table.name)} WHERE ${
|
|
286
|
-
|
|
319
|
+
`DELETE FROM ${q(table.name)} WHERE ${clause}`,
|
|
320
|
+
params,
|
|
287
321
|
);
|
|
288
322
|
}
|
|
289
323
|
|
package/src/engine/types.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* @property {ColumnDef[]} columns
|
|
24
24
|
* @property {('list'|'get'|'add'|'update'|'delete')[]} operations
|
|
25
25
|
* @property {string} [listOrderBy] - Default ORDER BY clause (use backticks for reserved words)
|
|
26
|
+
* @property {string|null} [ownerColumn] - Row-level owner column; every query is scoped to the caller on it
|
|
26
27
|
* @property {Object} [customDescriptions] - Override tool descriptions per operation
|
|
27
28
|
*
|
|
28
29
|
* @typedef {Object} DbConfig
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
* @property {string} displayName - Human-readable (e.g., 'PAAAL Coach')
|
|
41
42
|
* @property {string} version
|
|
42
43
|
* @property {DbConfig} db
|
|
44
|
+
* @property {string} [ownerColumn] - App-wide default owner column for row-level scoping (per-table `ownerColumn` overrides)
|
|
43
45
|
* @property {TableSchema[]} tables
|
|
44
46
|
*/
|
|
45
47
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* await app.register(mcpPlugin, {
|
|
10
10
|
* apps: { paaal: paaalConfig },
|
|
11
11
|
* getDb: (appName, env) => getAppDbService(appName, env),
|
|
12
|
+
* getUser: (appName, env, sid) => resolveSession(appName, env, sid), // → { userId } | null
|
|
12
13
|
* });
|
|
13
14
|
*
|
|
14
15
|
* Route param format:
|
|
@@ -40,6 +41,26 @@ function parseAppParam(param) {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/** Read the Bearer token (an icode session id) from the Authorization header. */
|
|
45
|
+
function readBearer(req) {
|
|
46
|
+
const header = req.headers['authorization'];
|
|
47
|
+
if (typeof header !== 'string') return null;
|
|
48
|
+
const [scheme, token] = header.split(/\s+/, 2);
|
|
49
|
+
return /^bearer$/i.test(scheme) && token ? token.trim() : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** MCP-spec 401 — points the client at the protected-resource metadata (Login with icode). */
|
|
53
|
+
function unauthorized(req, reply) {
|
|
54
|
+
const proto = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
55
|
+
const host = req.headers['x-forwarded-host'] || req.headers.host;
|
|
56
|
+
reply.header('WWW-Authenticate', `Bearer resource_metadata="${proto}://${host}/.well-known/oauth-protected-resource"`);
|
|
57
|
+
return reply.code(401).send({
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
error: { code: -32001, message: 'Authentication required' },
|
|
60
|
+
id: null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
/**
|
|
44
65
|
* @param {import('fastify').FastifyInstance} fastify
|
|
45
66
|
* @param {Object} opts
|
|
@@ -50,17 +71,35 @@ function parseAppParam(param) {
|
|
|
50
71
|
* @param {(db, tableConfigs, opts) => Promise<TableSchema[]>} [opts.resolveSchemas]
|
|
51
72
|
* — Custom schema resolver. Default: INFORMATION_SCHEMA.
|
|
52
73
|
* — Inject your own to use SchemasService or any other source.
|
|
74
|
+
* @param {(appName: string, env: string, sid: string) => Promise<{ userId: string } | null>} [opts.getUser]
|
|
75
|
+
* — Resolve a Bearer session id to the caller's identity. When provided, the MCP
|
|
76
|
+
* endpoint requires a valid session and scopes data to that user; when omitted,
|
|
77
|
+
* requests are unauthenticated (owner-scoped tables then fail closed).
|
|
53
78
|
* @param {string} [opts.prefix='/v1/mcp']
|
|
54
79
|
* @param {boolean} [opts.public=true]
|
|
55
80
|
*/
|
|
56
81
|
export default async function mcpPlugin(fastify, opts = {}) {
|
|
57
82
|
const getDb = opts.getDb;
|
|
83
|
+
const getUser = opts.getUser;
|
|
58
84
|
const resolveSchemas = opts.resolveSchemas;
|
|
59
85
|
const prefix = opts.prefix || '/v1/mcp';
|
|
60
86
|
const isPublic = opts.public ?? true;
|
|
61
87
|
|
|
62
88
|
if (!getDb) throw new Error('icode-mcp-adapter: opts.getDb is required');
|
|
63
89
|
|
|
90
|
+
// Resolve the request's Bearer session id to { userId } via the injected getUser.
|
|
91
|
+
// Null when no getUser is configured, no token is sent, or the session is invalid.
|
|
92
|
+
async function identityFor(req, appName, env) {
|
|
93
|
+
if (!getUser) return null;
|
|
94
|
+
const sid = readBearer(req);
|
|
95
|
+
if (!sid) return null;
|
|
96
|
+
try {
|
|
97
|
+
return await getUser(appName, env, sid);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
// Build lookup: key = "appName--env" (or "appName--prod" for default)
|
|
65
104
|
const apps = new Map();
|
|
66
105
|
for (const config of Object.values(opts.apps || {})) {
|
|
@@ -81,25 +120,41 @@ export default async function mcpPlugin(fastify, opts = {}) {
|
|
|
81
120
|
|
|
82
121
|
const sessionId = req.headers['mcp-session-id'];
|
|
83
122
|
|
|
84
|
-
// Existing session
|
|
123
|
+
// Existing session — re-check the caller still owns it (revoked sessions drop here)
|
|
85
124
|
if (sessionId && sessions.has(sessionId)) {
|
|
86
|
-
|
|
125
|
+
const session = sessions.get(sessionId);
|
|
126
|
+
if (getUser) {
|
|
127
|
+
const identity = await identityFor(req, appName, env);
|
|
128
|
+
if (!identity?.userId || identity.userId !== session.userId) {
|
|
129
|
+
return unauthorized(req, reply);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await session.transport.handleRequest(req.raw, reply.raw, req.body);
|
|
87
133
|
return reply.hijack();
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
// New session (must be initialize request)
|
|
91
137
|
if (!sessionId && isInitializeRequest(req.body)) {
|
|
138
|
+
const identity = await identityFor(req, appName, env);
|
|
139
|
+
if (getUser && !identity?.userId) return unauthorized(req, reply);
|
|
140
|
+
|
|
92
141
|
let db;
|
|
93
142
|
try {
|
|
94
143
|
db = await getDb(appName, env);
|
|
95
144
|
} catch (err) {
|
|
96
145
|
return reply.code(400).send({ ok: false, error: err.message || err.sqlMessage || 'Failed to get DB connection' });
|
|
97
146
|
}
|
|
98
|
-
|
|
147
|
+
|
|
148
|
+
let server;
|
|
149
|
+
try {
|
|
150
|
+
server = await createMcpServerAuto(appConfig, db, { resolveSchemas, identity });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return reply.code(500).send({ ok: false, error: err.message || 'Failed to initialize MCP server' });
|
|
153
|
+
}
|
|
99
154
|
|
|
100
155
|
const transport = new StreamableHTTPServerTransport({
|
|
101
156
|
sessionIdGenerator: () => randomUUID(),
|
|
102
|
-
onsessioninitialized: (id) => sessions.set(id, { transport, server }),
|
|
157
|
+
onsessioninitialized: (id) => sessions.set(id, { transport, server, userId: identity?.userId }),
|
|
103
158
|
});
|
|
104
159
|
transport.onclose = () => {
|
|
105
160
|
if (transport.sessionId) sessions.delete(transport.sessionId);
|