@tiangong-lca/mcp-server 0.0.16 → 0.0.18

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.
@@ -3,11 +3,43 @@ import { Redis } from '@upstash/redis';
3
3
  import { authenticateCognitoToken } from './cognito_auth.js';
4
4
  import { redis_token, redis_url, supabase_base_url, supabase_publishable_key } from './config.js';
5
5
  import decodeApiKey from './decode_api_key.js';
6
+ import { normalizeSupabaseSession } from './supabase_session.js';
6
7
  const supabase = createClient(supabase_base_url, supabase_publishable_key);
7
8
  const redis = new Redis({
8
9
  url: redis_url,
9
10
  token: redis_token,
10
11
  });
12
+ const SESSION_EXPIRY_BUFFER_SECONDS = 30;
13
+ function isSupabaseSessionReusable(session) {
14
+ if (!session) {
15
+ return false;
16
+ }
17
+ if (typeof session.access_token !== 'string' || session.access_token.length === 0) {
18
+ return false;
19
+ }
20
+ const expiresAt = typeof session.expires_at === 'number'
21
+ ? session.expires_at
22
+ : session.expires_at === null
23
+ ? null
24
+ : undefined;
25
+ if (!expiresAt) {
26
+ return false;
27
+ }
28
+ const nowSeconds = Math.floor(Date.now() / 1000);
29
+ return expiresAt - nowSeconds > SESSION_EXPIRY_BUFFER_SECONDS;
30
+ }
31
+ function calculateCacheTtlSeconds(session) {
32
+ const expiresAt = typeof session.expires_at === 'number' ? session.expires_at : null;
33
+ if (!expiresAt) {
34
+ return null;
35
+ }
36
+ const nowSeconds = Math.floor(Date.now() / 1000);
37
+ const remaining = Math.floor(expiresAt - nowSeconds);
38
+ if (remaining <= 0) {
39
+ return null;
40
+ }
41
+ return Math.max(1, Math.min(remaining, 3600));
42
+ }
11
43
  export function getTokenType(bearerKey) {
12
44
  const jwtPattern = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
13
45
  if (jwtPattern.test(bearerKey)) {
@@ -51,38 +83,89 @@ async function authenticateApiKeyRequest(bearerKey) {
51
83
  const credentials = decodeApiKey(bearerKey);
52
84
  if (credentials) {
53
85
  const { email = '', password = '' } = credentials;
54
- const userIdFromRedis = await redis.get('lca_' + email);
55
- if (!userIdFromRedis) {
56
- const { data, error } = await supabase.auth.signInWithPassword({
57
- email: email,
58
- password: password,
59
- });
60
- if (error) {
61
- return {
62
- isAuthenticated: false,
63
- response: 'Unauthorized',
64
- };
86
+ const cacheKey = 'lca_' + email;
87
+ const cachedPayload = (await redis.get(cacheKey));
88
+ let cachedUserId;
89
+ let cachedSession;
90
+ if (cachedPayload) {
91
+ const applyCachedObject = (record) => {
92
+ const recordUserId = record['userId'];
93
+ if (typeof recordUserId === 'string') {
94
+ cachedUserId = recordUserId;
95
+ }
96
+ if ('session' in record) {
97
+ const normalized = normalizeSupabaseSession(record['session']);
98
+ if (normalized) {
99
+ cachedSession = normalized;
100
+ }
101
+ }
102
+ };
103
+ if (typeof cachedPayload === 'string') {
104
+ try {
105
+ const parsed = JSON.parse(cachedPayload);
106
+ applyCachedObject(parsed);
107
+ }
108
+ catch (_error) {
109
+ cachedUserId = cachedPayload;
110
+ }
65
111
  }
66
- if (data.user.role !== 'authenticated') {
67
- return {
68
- isAuthenticated: false,
69
- response: 'You are not an authenticated user.',
70
- };
112
+ else if (typeof cachedPayload === 'object') {
113
+ applyCachedObject(cachedPayload);
71
114
  }
72
- else {
73
- await redis.setex('lca_' + email, 3600, data.user.id);
74
- return {
75
- isAuthenticated: true,
76
- response: data.user.id,
77
- userId: data.user.id,
78
- email: data.user.email,
79
- };
115
+ }
116
+ if (cachedUserId && isSupabaseSessionReusable(cachedSession)) {
117
+ const ttlSeconds = calculateCacheTtlSeconds(cachedSession);
118
+ if (ttlSeconds) {
119
+ try {
120
+ await redis.expire(cacheKey, ttlSeconds);
121
+ }
122
+ catch (error) {
123
+ console.warn('Failed to refresh Redis TTL for cached Supabase session:', error);
124
+ }
80
125
  }
126
+ return {
127
+ isAuthenticated: true,
128
+ response: cachedUserId,
129
+ userId: cachedUserId,
130
+ email,
131
+ supabaseSession: cachedSession,
132
+ };
81
133
  }
134
+ const { data, error } = await supabase.auth.signInWithPassword({
135
+ email,
136
+ password,
137
+ });
138
+ if (error || !data.user) {
139
+ return {
140
+ isAuthenticated: false,
141
+ response: 'Unauthorized',
142
+ };
143
+ }
144
+ if (data.user.role !== 'authenticated') {
145
+ return {
146
+ isAuthenticated: false,
147
+ response: 'You are not an authenticated user.',
148
+ };
149
+ }
150
+ const sessionTokens = data.session?.access_token
151
+ ? {
152
+ access_token: data.session.access_token,
153
+ refresh_token: data.session.refresh_token ?? null,
154
+ expires_at: typeof data.session.expires_at === 'number' ? data.session.expires_at : null,
155
+ }
156
+ : undefined;
157
+ const cacheValue = {
158
+ userId: data.user.id,
159
+ session: sessionTokens ?? null,
160
+ };
161
+ const cacheTtl = sessionTokens ? (calculateCacheTtlSeconds(sessionTokens) ?? 3600) : 3600;
162
+ await redis.setex(cacheKey, cacheTtl, JSON.stringify(cacheValue));
82
163
  return {
83
164
  isAuthenticated: true,
84
- response: String(userIdFromRedis),
85
- userId: String(userIdFromRedis),
165
+ response: data.user.id,
166
+ userId: data.user.id,
167
+ email: data.user.email ?? email,
168
+ supabaseSession: sessionTokens,
86
169
  };
87
170
  }
88
171
  return {
@@ -91,7 +174,6 @@ async function authenticateApiKeyRequest(bearerKey) {
91
174
  };
92
175
  }
93
176
  async function authenticateSupabaseRequest(bearerKey) {
94
- console.log(bearerKey);
95
177
  const { data: authData } = await supabase.auth.getUser(bearerKey);
96
178
  if (authData.user?.role === 'authenticated') {
97
179
  return {
@@ -99,6 +181,9 @@ async function authenticateSupabaseRequest(bearerKey) {
99
181
  response: authData.user?.id,
100
182
  userId: authData.user?.id,
101
183
  email: authData.user?.email,
184
+ supabaseSession: {
185
+ access_token: bearerKey,
186
+ },
102
187
  };
103
188
  }
104
189
  if (!authData || !authData.user) {
@@ -120,5 +205,8 @@ async function authenticateSupabaseRequest(bearerKey) {
120
205
  response: authData.user.id,
121
206
  userId: authData.user.id,
122
207
  email: authData.user.email,
208
+ supabaseSession: {
209
+ access_token: bearerKey,
210
+ },
123
211
  };
124
212
  }
@@ -1,8 +1,9 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { regBomCalculationTool } from '../tools/bom_calculation.js';
3
+ import { regCrudTool } from '../tools/db_crud.js';
3
4
  import { regFlowSearchTool } from '../tools/flow_hybrid_search.js';
4
5
  import { regProcessSearchTool } from '../tools/process_hybrid_search.js';
5
- export function initializeServer(bearerKey) {
6
+ export function initializeServer(bearerKey, supabaseSession) {
6
7
  const server = new McpServer({
7
8
  name: 'TianGong-LCA-MCP-Server',
8
9
  version: '1.0.0',
@@ -10,8 +11,9 @@ export function initializeServer(bearerKey) {
10
11
  regFlowSearchTool(server, bearerKey);
11
12
  regProcessSearchTool(server, bearerKey);
12
13
  regBomCalculationTool(server);
14
+ regCrudTool(server, supabaseSession ?? bearerKey);
13
15
  return server;
14
16
  }
15
- export function getServer(bearerKey) {
16
- return initializeServer(bearerKey);
17
+ export function getServer(bearerKey, supabaseSession) {
18
+ return initializeServer(bearerKey, supabaseSession);
17
19
  }
@@ -0,0 +1,89 @@
1
+ function isNonEmptyString(value) {
2
+ return typeof value === 'string' && value.length > 0;
3
+ }
4
+ function normalizeRefreshToken(value) {
5
+ if (typeof value === 'string') {
6
+ return value;
7
+ }
8
+ if (value === null) {
9
+ return null;
10
+ }
11
+ return undefined;
12
+ }
13
+ function normalizeExpiresAt(value) {
14
+ if (typeof value === 'number') {
15
+ return value;
16
+ }
17
+ if (value === null) {
18
+ return null;
19
+ }
20
+ return undefined;
21
+ }
22
+ const nestedSessionKeys = ['session', 'supabaseSession', 'supabaseSessionTokens'];
23
+ export function normalizeSupabaseSession(input) {
24
+ if (!input) {
25
+ return undefined;
26
+ }
27
+ if (typeof input === 'string') {
28
+ const trimmed = input.trim();
29
+ if (!trimmed.startsWith('{')) {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const parsed = JSON.parse(trimmed);
34
+ return normalizeSupabaseSession(parsed);
35
+ }
36
+ catch (_error) {
37
+ return undefined;
38
+ }
39
+ }
40
+ if (typeof input !== 'object') {
41
+ return undefined;
42
+ }
43
+ const record = input;
44
+ const candidateAccessToken = record['access_token'] ?? record['accessToken'];
45
+ const accessToken = isNonEmptyString(candidateAccessToken) ? candidateAccessToken : undefined;
46
+ if (accessToken) {
47
+ const candidateRefreshToken = record['refresh_token'] ?? record['refreshToken'];
48
+ const refreshToken = normalizeRefreshToken(candidateRefreshToken);
49
+ const candidateExpiresAt = record['expires_at'] ?? record['expiresAt'];
50
+ const expiresAt = normalizeExpiresAt(candidateExpiresAt);
51
+ const normalized = {
52
+ access_token: accessToken,
53
+ };
54
+ if (refreshToken !== undefined) {
55
+ normalized.refresh_token = refreshToken;
56
+ }
57
+ if (expiresAt !== undefined) {
58
+ normalized.expires_at = expiresAt;
59
+ }
60
+ return normalized;
61
+ }
62
+ for (const key of nestedSessionKeys) {
63
+ if (key in record) {
64
+ const nestedValue = record[key];
65
+ if (nestedValue && nestedValue !== input) {
66
+ const normalized = normalizeSupabaseSession(nestedValue);
67
+ if (normalized) {
68
+ return normalized;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ export function resolveSupabaseAccessToken(input) {
76
+ const session = normalizeSupabaseSession(input);
77
+ if (session) {
78
+ return {
79
+ session,
80
+ accessToken: session.access_token,
81
+ };
82
+ }
83
+ if (typeof input === 'string' && input.trim().length > 0) {
84
+ return {
85
+ accessToken: input.trim(),
86
+ };
87
+ }
88
+ return {};
89
+ }
@@ -35,6 +35,7 @@ const authenticateBearer = async (req, res, next) => {
35
35
  return;
36
36
  }
37
37
  req.bearerKey = bearerKey;
38
+ req.supabaseSession = authResult.supabaseSession;
38
39
  next();
39
40
  };
40
41
  const app = express();
@@ -53,7 +54,7 @@ app.use((req, res, next) => {
53
54
  });
54
55
  app.post('/mcp', authenticateBearer, async (req, res) => {
55
56
  try {
56
- const server = getServer(req.bearerKey);
57
+ const server = getServer(req.bearerKey, req.supabaseSession);
57
58
  const transport = new StreamableHTTPServerTransport({
58
59
  sessionIdGenerator: undefined,
59
60
  });
@@ -0,0 +1,346 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createClient, FunctionRegion } from '@supabase/supabase-js';
3
+ import { createContact, createFlow, createLifeCycleModel, createProcess, createSource, } from '@tiangong-lca/tidas-sdk/core';
4
+ import { z } from 'zod';
5
+ import { supabase_base_url, supabase_publishable_key } from '../_shared/config.js';
6
+ import { resolveSupabaseAccessToken } from '../_shared/supabase_session.js';
7
+ const allowedTables = ['contacts', 'flows', 'lifecyclemodels', 'processes', 'sources'];
8
+ const tableSchema = z.enum(allowedTables);
9
+ const UPDATE_FUNCTION_NAME = 'update_data';
10
+ const tablePrimaryKey = {
11
+ contacts: 'id',
12
+ flows: 'id',
13
+ lifecyclemodels: 'id',
14
+ processes: 'id',
15
+ sources: 'id',
16
+ };
17
+ function getPrimaryKeyColumn(table) {
18
+ return tablePrimaryKey[table] ?? 'id';
19
+ }
20
+ const jsonValueSchema = z.lazy(() => z.union([
21
+ z.string(),
22
+ z.number(),
23
+ z.boolean(),
24
+ z.null(),
25
+ z.array(jsonValueSchema),
26
+ z.record(jsonValueSchema),
27
+ ]));
28
+ const filterValueSchema = z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean(),
32
+ z.null(),
33
+ ]);
34
+ const filtersSchema = z.record(filterValueSchema);
35
+ const toolParamsSchema = {
36
+ operation: z
37
+ .enum(['select', 'insert', 'update', 'delete'])
38
+ .describe('CRUD operation to perform: select optionally accepts limit/id/version/filters, insert requires jsonOrdered (id auto-generated), update requires id/version/jsonOrdered, delete requires id/version.'),
39
+ table: tableSchema.describe('Target table for the operation; must be one of contacts, flows, lifecyclemodels, processes, or sources.'),
40
+ limit: z
41
+ .number()
42
+ .int()
43
+ .positive()
44
+ .optional()
45
+ .describe('Maximum number of records to return (select only).'),
46
+ id: z
47
+ .string()
48
+ .uuid()
49
+ .optional()
50
+ .describe('UUID string stored in the `id` column (required for update/delete, optional filter for select).'),
51
+ version: z
52
+ .string()
53
+ .min(1)
54
+ .optional()
55
+ .describe('String stored in the `version` column (required for update/delete, optional filter for select).'),
56
+ filters: filtersSchema
57
+ .optional()
58
+ .describe('Optional equality filters as JSON object, e.g. { "name": "Example" }. Only used for select operations. Leave empty for insert/update/delete operations.'),
59
+ jsonOrdered: jsonValueSchema
60
+ .optional()
61
+ .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete).'),
62
+ };
63
+ const refinedInputSchema = z
64
+ .object(toolParamsSchema)
65
+ .strict()
66
+ .superRefine((data, ctx) => {
67
+ switch (data.operation) {
68
+ case 'insert':
69
+ if (data.jsonOrdered === undefined) {
70
+ ctx.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ message: 'jsonOrdered is required for insert operations.',
73
+ path: ['jsonOrdered'],
74
+ });
75
+ }
76
+ break;
77
+ case 'update':
78
+ if (data.id === undefined) {
79
+ ctx.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ message: 'id is required for update operations.',
82
+ path: ['id'],
83
+ });
84
+ }
85
+ if (data.version === undefined) {
86
+ ctx.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ message: 'version is required for update operations.',
89
+ path: ['version'],
90
+ });
91
+ }
92
+ if (data.jsonOrdered === undefined) {
93
+ ctx.addIssue({
94
+ code: z.ZodIssueCode.custom,
95
+ message: 'jsonOrdered is required for update operations.',
96
+ path: ['jsonOrdered'],
97
+ });
98
+ }
99
+ break;
100
+ case 'delete':
101
+ if (data.id === undefined) {
102
+ ctx.addIssue({
103
+ code: z.ZodIssueCode.custom,
104
+ message: 'id is required for delete operations.',
105
+ path: ['id'],
106
+ });
107
+ }
108
+ if (data.version === undefined) {
109
+ ctx.addIssue({
110
+ code: z.ZodIssueCode.custom,
111
+ message: 'version is required for delete operations.',
112
+ path: ['version'],
113
+ });
114
+ }
115
+ break;
116
+ default:
117
+ break;
118
+ }
119
+ });
120
+ function requireAccessToken(accessToken) {
121
+ if (!accessToken) {
122
+ throw new Error('An authenticated Supabase session is required for update operations. Provide a valid access token.');
123
+ }
124
+ return accessToken;
125
+ }
126
+ function ensureRows(rows, errorMessage) {
127
+ if (!Array.isArray(rows) || rows.length === 0) {
128
+ throw new Error(errorMessage);
129
+ }
130
+ return rows;
131
+ }
132
+ function validateJsonOrdered(table, jsonOrdered) {
133
+ try {
134
+ let validationResult;
135
+ switch (table) {
136
+ case 'contacts': {
137
+ const contact = createContact(jsonOrdered, { mode: 'strict' });
138
+ validationResult = contact.validate();
139
+ break;
140
+ }
141
+ case 'flows': {
142
+ const flow = createFlow(jsonOrdered, { mode: 'strict' });
143
+ validationResult = flow.validate();
144
+ break;
145
+ }
146
+ case 'lifecyclemodels': {
147
+ const lifecycleModel = createLifeCycleModel(jsonOrdered, { mode: 'strict' });
148
+ validationResult = lifecycleModel.validate();
149
+ break;
150
+ }
151
+ case 'processes': {
152
+ const process = createProcess(jsonOrdered, { mode: 'strict' });
153
+ validationResult = process.validate();
154
+ break;
155
+ }
156
+ case 'sources': {
157
+ const source = createSource(jsonOrdered, { mode: 'strict' });
158
+ validationResult = source.validate();
159
+ break;
160
+ }
161
+ default: {
162
+ const exhaustiveCheck = table;
163
+ throw new Error(`Unsupported table type: ${table}`);
164
+ }
165
+ }
166
+ if (!validationResult.success) {
167
+ const errorDetails = validationResult.error?.issues
168
+ ? JSON.stringify(validationResult.error.issues, null, 2)
169
+ : JSON.stringify(validationResult.error);
170
+ throw new Error(`Validation failed for table "${table}". Errors: ${errorDetails}`);
171
+ }
172
+ }
173
+ catch (error) {
174
+ if (error instanceof Error) {
175
+ throw new Error(`Failed to validate jsonOrdered for table "${table}": ${error.message}`);
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+ async function createSupabaseClient(bearerKey) {
181
+ const { session: normalizedSession, accessToken: bearerToken } = resolveSupabaseAccessToken(bearerKey);
182
+ const supabase = createClient(supabase_base_url, supabase_publishable_key, {
183
+ auth: {
184
+ persistSession: false,
185
+ autoRefreshToken: Boolean(normalizedSession?.refresh_token),
186
+ },
187
+ ...(bearerToken
188
+ ? {
189
+ global: {
190
+ headers: {
191
+ Authorization: `Bearer ${bearerToken}`,
192
+ },
193
+ },
194
+ }
195
+ : {}),
196
+ });
197
+ if (normalizedSession?.refresh_token) {
198
+ const { error: setSessionError } = await supabase.auth.setSession({
199
+ access_token: normalizedSession.access_token,
200
+ refresh_token: normalizedSession.refresh_token,
201
+ });
202
+ if (setSessionError) {
203
+ console.warn('Failed to set Supabase session for CRUD tool:', setSessionError.message);
204
+ }
205
+ }
206
+ return { supabase, accessToken: normalizedSession?.access_token ?? bearerToken };
207
+ }
208
+ async function handleSelect(supabase, input) {
209
+ const { table, limit, id, version, filters } = input;
210
+ const keyColumn = getPrimaryKeyColumn(table);
211
+ let queryBuilder = supabase.from(table).select('*');
212
+ if (filters) {
213
+ for (const [column, value] of Object.entries(filters)) {
214
+ if (value !== null && value !== undefined) {
215
+ queryBuilder = queryBuilder.eq(column, value);
216
+ }
217
+ }
218
+ }
219
+ if (id) {
220
+ queryBuilder = queryBuilder.eq(keyColumn, id);
221
+ }
222
+ if (version) {
223
+ queryBuilder = queryBuilder.eq('version', version);
224
+ }
225
+ if (limit) {
226
+ queryBuilder = queryBuilder.limit(limit);
227
+ }
228
+ const { data, error } = await queryBuilder;
229
+ if (error) {
230
+ console.error('Error querying the database:', error);
231
+ throw error;
232
+ }
233
+ return JSON.stringify({ data: data ?? [], count: data?.length ?? 0 });
234
+ }
235
+ async function handleInsert(supabase, input) {
236
+ const { table, jsonOrdered } = input;
237
+ if (jsonOrdered === undefined) {
238
+ throw new Error('jsonOrdered is required for insert operations.');
239
+ }
240
+ validateJsonOrdered(table, jsonOrdered);
241
+ const newId = randomUUID();
242
+ const keyColumn = getPrimaryKeyColumn(table);
243
+ const { data, error } = await supabase
244
+ .from(table)
245
+ .insert([{ [keyColumn]: newId, json_ordered: jsonOrdered }])
246
+ .select();
247
+ if (error) {
248
+ console.error('Error inserting into the database:', error);
249
+ throw error;
250
+ }
251
+ return JSON.stringify({ id: newId, data: data ?? [] });
252
+ }
253
+ async function handleUpdate(supabase, accessToken, input) {
254
+ const { table, id, version, jsonOrdered } = input;
255
+ if (id === undefined) {
256
+ throw new Error('id is required for update operations.');
257
+ }
258
+ if (version === undefined) {
259
+ throw new Error('version is required for update operations.');
260
+ }
261
+ if (jsonOrdered === undefined) {
262
+ throw new Error('jsonOrdered is required for update operations.');
263
+ }
264
+ validateJsonOrdered(table, jsonOrdered);
265
+ const token = requireAccessToken(accessToken);
266
+ const { data: functionPayload, error } = await supabase.functions.invoke(UPDATE_FUNCTION_NAME, {
267
+ headers: {
268
+ Authorization: `Bearer ${token}`,
269
+ },
270
+ body: { id, version, table, data: { json_ordered: jsonOrdered } },
271
+ region: FunctionRegion.UsEast1,
272
+ });
273
+ if (error) {
274
+ console.error('Error invoking update_data function:', error);
275
+ throw error;
276
+ }
277
+ const { data: updatedRows, error: functionError } = (functionPayload ??
278
+ {});
279
+ if (functionError) {
280
+ console.error('Supabase update_data returned an error:', functionError);
281
+ const message = functionError.message ?? 'Supabase update_data function rejected the request.';
282
+ throw new Error(message);
283
+ }
284
+ const keyColumn = getPrimaryKeyColumn(table);
285
+ const rows = ensureRows(updatedRows, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
286
+ return JSON.stringify({ id, version, data: rows });
287
+ }
288
+ async function handleDelete(supabase, input) {
289
+ const { table, id, version } = input;
290
+ if (id === undefined) {
291
+ throw new Error('id is required for delete operations.');
292
+ }
293
+ if (version === undefined) {
294
+ throw new Error('version is required for delete operations.');
295
+ }
296
+ const keyColumn = getPrimaryKeyColumn(table);
297
+ const { data, error } = await supabase
298
+ .from(table)
299
+ .delete()
300
+ .eq(keyColumn, id)
301
+ .eq('version', version)
302
+ .select();
303
+ if (error) {
304
+ console.error('Error deleting from the database:', error);
305
+ throw error;
306
+ }
307
+ const rows = ensureRows(data, `Delete affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
308
+ return JSON.stringify({ id, version, data: rows });
309
+ }
310
+ async function performCrud(input, bearerKey) {
311
+ try {
312
+ const { supabase, accessToken } = await createSupabaseClient(bearerKey);
313
+ switch (input.operation) {
314
+ case 'select':
315
+ return handleSelect(supabase, input);
316
+ case 'insert':
317
+ return handleInsert(supabase, input);
318
+ case 'update':
319
+ return handleUpdate(supabase, accessToken, input);
320
+ case 'delete':
321
+ return handleDelete(supabase, input);
322
+ default: {
323
+ const exhaustiveCheck = input;
324
+ throw new Error('Unsupported operation supplied to CRUD tool.');
325
+ }
326
+ }
327
+ }
328
+ catch (error) {
329
+ console.error('Error making the request:', error);
330
+ throw error;
331
+ }
332
+ }
333
+ export function regCrudTool(server, bearerKey) {
334
+ server.tool('Database_CRUD_Tool', 'Perform select/insert/update/delete against allowed Supabase tables (insert needs jsonOrdered, update/delete need id and version).', toolParamsSchema, async (rawInput) => {
335
+ const input = refinedInputSchema.parse(rawInput);
336
+ const result = await performCrud(input, bearerKey);
337
+ return {
338
+ content: [
339
+ {
340
+ type: 'text',
341
+ text: result,
342
+ },
343
+ ],
344
+ };
345
+ });
346
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiangong-lca/mcp-server",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "TianGong LCA MCP Server",
5
5
  "license": "MIT",
6
6
  "author": "Nan LI",
@@ -27,22 +27,23 @@
27
27
  "ncu:update": "npx npm-check-updates -u"
28
28
  },
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.17.4",
31
- "@supabase/supabase-js": "^2.56.0",
30
+ "@modelcontextprotocol/sdk": "^1.19.1",
31
+ "@supabase/supabase-js": "^2.58.0",
32
+ "@tiangong-lca/tidas-sdk": "^0.1.16",
32
33
  "@types/express": "^5.0.3",
33
- "@upstash/redis": "^1.35.3",
34
- "aws-jwt-verify": "^5.1.0",
34
+ "@upstash/redis": "^1.35.4",
35
+ "aws-jwt-verify": "^5.1.1",
35
36
  "olca-ipc": "^2.2.1",
36
37
  "zod": "^3.25.76"
37
38
  },
38
39
  "devDependencies": {
39
- "@modelcontextprotocol/inspector": "^0.16.5",
40
+ "@modelcontextprotocol/inspector": "^0.16.8",
40
41
  "dotenv-cli": "^10.0.0",
41
- "npm-check-updates": "^18.0.2",
42
+ "npm-check-updates": "^19.0.0",
42
43
  "prettier": "^3.6.2",
43
- "prettier-plugin-organize-imports": "^4.2.0",
44
+ "prettier-plugin-organize-imports": "^4.3.0",
44
45
  "shx": "^0.4.0",
45
- "tsx": "^4.20.5",
46
- "typescript": "^5.9.2"
46
+ "tsx": "^4.20.6",
47
+ "typescript": "^5.9.3"
47
48
  }
48
49
  }