@tiangong-lca/mcp-server 0.0.15 → 0.0.17

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.
@@ -1,13 +1,45 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
2
  import { Redis } from '@upstash/redis';
3
3
  import { authenticateCognitoToken } from './cognito_auth.js';
4
- import { redis_token, redis_url, supabase_anon_key, supabase_base_url } from './config.js';
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
- const supabase = createClient(supabase_base_url, supabase_anon_key);
6
+ import { normalizeSupabaseSession } from './supabase_session.js';
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
  }
@@ -6,8 +6,7 @@ export const COGNITO_ISSUER = `https://cognito-idp.${COGNITO_REGION}.amazonaws.c
6
6
  export const COGNITO_JWKS_URL = `${COGNITO_ISSUER}/.well-known/jwks.json`;
7
7
  export const COGNITO_BASE_URL = 'https://us-east-1snsyimond.auth.us-east-1.amazoncognito.com';
8
8
  export const supabase_base_url = process.env.SUPABASE_BASE_URL ?? 'https://qgzvkongdjqiiamzbbts.supabase.co';
9
- export const supabase_anon_key = process.env.SUPABASE_ANON_KEY ??
10
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFnenZrb25nZGpxaWlhbXpiYnRzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAzNjUyMzQsImV4cCI6MjA1NTk0MTIzNH0.PsZIcjAqexpqIg-91twpKjALyw9big6Bn4WRLLoCzTo';
9
+ export const supabase_publishable_key = process.env.SUPABASE_PUBLISHABLE_KEY ?? 'sb_publishable_EFWH4E61tpAtf82WQ37xTA_Fxa5OPyg';
11
10
  export const x_region = process.env.X_REGION ?? 'us-east-1';
12
11
  export const redis_url = process.env.UPSTASH_REDIS_URL ?? '';
13
12
  export const redis_token = process.env.UPSTASH_REDIS_TOKEN ?? '';
@@ -1,19 +1,19 @@
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
- import { getTokenType } from './auth_middleware.js';
6
- export function initializeServer(bearerKey, xApiKey) {
6
+ export function initializeServer(bearerKey, supabaseSession) {
7
7
  const server = new McpServer({
8
8
  name: 'TianGong-LCA-MCP-Server',
9
9
  version: '1.0.0',
10
10
  });
11
- regFlowSearchTool(server, bearerKey, xApiKey);
12
- regProcessSearchTool(server, bearerKey, xApiKey);
11
+ regFlowSearchTool(server, bearerKey);
12
+ regProcessSearchTool(server, bearerKey);
13
13
  regBomCalculationTool(server);
14
+ regCrudTool(server, supabaseSession ?? bearerKey);
14
15
  return server;
15
16
  }
16
- export function getServer(bearerKey) {
17
- const tokenType = bearerKey ? getTokenType(bearerKey) : '';
18
- return initializeServer(tokenType !== 'supabase' ? undefined : bearerKey, tokenType !== 'supabase' ? bearerKey : undefined);
17
+ export function getServer(bearerKey, supabaseSession) {
18
+ return initializeServer(bearerKey, supabaseSession);
19
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,293 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createClient, FunctionRegion } from '@supabase/supabase-js';
3
+ import { z } from 'zod';
4
+ import { supabase_base_url, supabase_publishable_key } from '../_shared/config.js';
5
+ import { resolveSupabaseAccessToken } from '../_shared/supabase_session.js';
6
+ const allowedTables = ['contacts', 'flows', 'lifecyclemodels', 'processes', 'sources'];
7
+ const tableSchema = z.enum(allowedTables);
8
+ const UPDATE_FUNCTION_NAME = 'update_data';
9
+ const tablePrimaryKey = {
10
+ contacts: 'id',
11
+ flows: 'id',
12
+ lifecyclemodels: 'id',
13
+ processes: 'id',
14
+ sources: 'id',
15
+ };
16
+ function getPrimaryKeyColumn(table) {
17
+ return tablePrimaryKey[table] ?? 'id';
18
+ }
19
+ const jsonValueSchema = z.lazy(() => z.union([
20
+ z.string(),
21
+ z.number(),
22
+ z.boolean(),
23
+ z.null(),
24
+ z.array(jsonValueSchema),
25
+ z.record(jsonValueSchema),
26
+ ]));
27
+ const filterValueSchema = z.union([
28
+ z.string(),
29
+ z.number(),
30
+ z.boolean(),
31
+ z.null(),
32
+ ]);
33
+ const filtersSchema = z.record(filterValueSchema);
34
+ const toolParamsSchema = {
35
+ operation: z
36
+ .enum(['select', 'insert', 'update', 'delete'])
37
+ .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.'),
38
+ table: tableSchema.describe('Target table for the operation; must be one of contacts, flows, lifecyclemodels, processes, or sources.'),
39
+ limit: z
40
+ .number()
41
+ .int()
42
+ .positive()
43
+ .optional()
44
+ .describe('Maximum number of records to return (select only).'),
45
+ id: z
46
+ .string()
47
+ .uuid()
48
+ .optional()
49
+ .describe('UUID string stored in the `id` column (required for update/delete, optional filter for select).'),
50
+ version: z
51
+ .string()
52
+ .min(1)
53
+ .optional()
54
+ .describe('String stored in the `version` column (required for update/delete, optional filter for select).'),
55
+ filters: filtersSchema
56
+ .optional()
57
+ .describe('Equality filters such as { "name": "Example" } (select only).'),
58
+ jsonOrdered: jsonValueSchema
59
+ .optional()
60
+ .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete).'),
61
+ };
62
+ const refinedInputSchema = z
63
+ .object(toolParamsSchema)
64
+ .strict()
65
+ .superRefine((data, ctx) => {
66
+ switch (data.operation) {
67
+ case 'insert':
68
+ if (data.jsonOrdered === undefined) {
69
+ ctx.addIssue({
70
+ code: z.ZodIssueCode.custom,
71
+ message: 'jsonOrdered is required for insert operations.',
72
+ path: ['jsonOrdered'],
73
+ });
74
+ }
75
+ break;
76
+ case 'update':
77
+ if (data.id === undefined) {
78
+ ctx.addIssue({
79
+ code: z.ZodIssueCode.custom,
80
+ message: 'id is required for update operations.',
81
+ path: ['id'],
82
+ });
83
+ }
84
+ if (data.version === undefined) {
85
+ ctx.addIssue({
86
+ code: z.ZodIssueCode.custom,
87
+ message: 'version is required for update operations.',
88
+ path: ['version'],
89
+ });
90
+ }
91
+ if (data.jsonOrdered === undefined) {
92
+ ctx.addIssue({
93
+ code: z.ZodIssueCode.custom,
94
+ message: 'jsonOrdered is required for update operations.',
95
+ path: ['jsonOrdered'],
96
+ });
97
+ }
98
+ break;
99
+ case 'delete':
100
+ if (data.id === undefined) {
101
+ ctx.addIssue({
102
+ code: z.ZodIssueCode.custom,
103
+ message: 'id is required for delete operations.',
104
+ path: ['id'],
105
+ });
106
+ }
107
+ if (data.version === undefined) {
108
+ ctx.addIssue({
109
+ code: z.ZodIssueCode.custom,
110
+ message: 'version is required for delete operations.',
111
+ path: ['version'],
112
+ });
113
+ }
114
+ break;
115
+ default:
116
+ break;
117
+ }
118
+ });
119
+ function requireAccessToken(accessToken) {
120
+ if (!accessToken) {
121
+ throw new Error('An authenticated Supabase session is required for update operations. Provide a valid access token.');
122
+ }
123
+ return accessToken;
124
+ }
125
+ function ensureRows(rows, errorMessage) {
126
+ if (!Array.isArray(rows) || rows.length === 0) {
127
+ throw new Error(errorMessage);
128
+ }
129
+ return rows;
130
+ }
131
+ async function createSupabaseClient(bearerKey) {
132
+ const { session: normalizedSession, accessToken: bearerToken } = resolveSupabaseAccessToken(bearerKey);
133
+ const supabase = createClient(supabase_base_url, supabase_publishable_key, {
134
+ auth: {
135
+ persistSession: false,
136
+ autoRefreshToken: Boolean(normalizedSession?.refresh_token),
137
+ },
138
+ ...(bearerToken
139
+ ? {
140
+ global: {
141
+ headers: {
142
+ Authorization: `Bearer ${bearerToken}`,
143
+ },
144
+ },
145
+ }
146
+ : {}),
147
+ });
148
+ if (normalizedSession?.refresh_token) {
149
+ const { error: setSessionError } = await supabase.auth.setSession({
150
+ access_token: normalizedSession.access_token,
151
+ refresh_token: normalizedSession.refresh_token,
152
+ });
153
+ if (setSessionError) {
154
+ console.warn('Failed to set Supabase session for CRUD tool:', setSessionError.message);
155
+ }
156
+ }
157
+ return { supabase, accessToken: normalizedSession?.access_token ?? bearerToken };
158
+ }
159
+ async function handleSelect(supabase, input) {
160
+ const { table, limit, id, version, filters } = input;
161
+ const keyColumn = getPrimaryKeyColumn(table);
162
+ let queryBuilder = supabase.from(table).select('*');
163
+ if (filters) {
164
+ for (const [column, value] of Object.entries(filters)) {
165
+ queryBuilder = queryBuilder.eq(column, value);
166
+ }
167
+ }
168
+ if (id) {
169
+ queryBuilder = queryBuilder.eq(keyColumn, id);
170
+ }
171
+ if (version) {
172
+ queryBuilder = queryBuilder.eq('version', version);
173
+ }
174
+ if (limit) {
175
+ queryBuilder = queryBuilder.limit(limit);
176
+ }
177
+ const { data, error } = await queryBuilder;
178
+ if (error) {
179
+ console.error('Error querying the database:', error);
180
+ throw error;
181
+ }
182
+ return JSON.stringify({ data: data ?? [], count: data?.length ?? 0 });
183
+ }
184
+ async function handleInsert(supabase, input) {
185
+ const { table, jsonOrdered } = input;
186
+ if (jsonOrdered === undefined) {
187
+ throw new Error('jsonOrdered is required for insert operations.');
188
+ }
189
+ const newId = randomUUID();
190
+ const keyColumn = getPrimaryKeyColumn(table);
191
+ const { data, error } = await supabase
192
+ .from(table)
193
+ .insert([{ [keyColumn]: newId, json_ordered: jsonOrdered }])
194
+ .select();
195
+ if (error) {
196
+ console.error('Error inserting into the database:', error);
197
+ throw error;
198
+ }
199
+ return JSON.stringify({ id: newId, data: data ?? [] });
200
+ }
201
+ async function handleUpdate(supabase, accessToken, input) {
202
+ const { table, id, version, jsonOrdered } = input;
203
+ if (id === undefined) {
204
+ throw new Error('id is required for update operations.');
205
+ }
206
+ if (version === undefined) {
207
+ throw new Error('version is required for update operations.');
208
+ }
209
+ if (jsonOrdered === undefined) {
210
+ throw new Error('jsonOrdered is required for update operations.');
211
+ }
212
+ const token = requireAccessToken(accessToken);
213
+ const { data: functionPayload, error } = await supabase.functions.invoke(UPDATE_FUNCTION_NAME, {
214
+ headers: {
215
+ Authorization: `Bearer ${token}`,
216
+ },
217
+ body: { id, version, table, data: { json_ordered: jsonOrdered } },
218
+ region: FunctionRegion.UsEast1,
219
+ });
220
+ if (error) {
221
+ console.error('Error invoking update_data function:', error);
222
+ throw error;
223
+ }
224
+ const { data: updatedRows, error: functionError } = (functionPayload ??
225
+ {});
226
+ if (functionError) {
227
+ console.error('Supabase update_data returned an error:', functionError);
228
+ const message = functionError.message ?? 'Supabase update_data function rejected the request.';
229
+ throw new Error(message);
230
+ }
231
+ const keyColumn = getPrimaryKeyColumn(table);
232
+ const rows = ensureRows(updatedRows, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
233
+ return JSON.stringify({ id, version, data: rows });
234
+ }
235
+ async function handleDelete(supabase, input) {
236
+ const { table, id, version } = input;
237
+ if (id === undefined) {
238
+ throw new Error('id is required for delete operations.');
239
+ }
240
+ if (version === undefined) {
241
+ throw new Error('version is required for delete operations.');
242
+ }
243
+ const keyColumn = getPrimaryKeyColumn(table);
244
+ const { data, error } = await supabase
245
+ .from(table)
246
+ .delete()
247
+ .eq(keyColumn, id)
248
+ .eq('version', version)
249
+ .select();
250
+ if (error) {
251
+ console.error('Error deleting from the database:', error);
252
+ throw error;
253
+ }
254
+ const rows = ensureRows(data, `Delete affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
255
+ return JSON.stringify({ id, version, data: rows });
256
+ }
257
+ async function performCrud(input, bearerKey) {
258
+ try {
259
+ const { supabase, accessToken } = await createSupabaseClient(bearerKey);
260
+ switch (input.operation) {
261
+ case 'select':
262
+ return handleSelect(supabase, input);
263
+ case 'insert':
264
+ return handleInsert(supabase, input);
265
+ case 'update':
266
+ return handleUpdate(supabase, accessToken, input);
267
+ case 'delete':
268
+ return handleDelete(supabase, input);
269
+ default: {
270
+ const exhaustiveCheck = input;
271
+ throw new Error('Unsupported operation supplied to CRUD tool.');
272
+ }
273
+ }
274
+ }
275
+ catch (error) {
276
+ console.error('Error making the request:', error);
277
+ throw error;
278
+ }
279
+ }
280
+ export function regCrudTool(server, bearerKey) {
281
+ 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) => {
282
+ const input = refinedInputSchema.parse(rawInput);
283
+ const result = await performCrud(input, bearerKey);
284
+ return {
285
+ content: [
286
+ {
287
+ type: 'text',
288
+ text: result,
289
+ },
290
+ ],
291
+ };
292
+ });
293
+ }
@@ -1,18 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import cleanObject from '../_shared/clean_object.js';
3
- import { supabase_anon_key, supabase_base_url, x_region } from '../_shared/config.js';
3
+ import { supabase_base_url, x_region } from '../_shared/config.js';
4
4
  const input_schema = {
5
5
  query: z.string().min(1).describe('Queries from user'),
6
6
  };
7
- async function searchFlows({ query }, bearerKey, xApiKey) {
7
+ async function searchFlows({ query }, bearerKey) {
8
8
  const url = `${supabase_base_url}/functions/v1/flow_hybrid_search`;
9
9
  try {
10
10
  const response = await fetch(url, {
11
11
  method: 'POST',
12
12
  headers: {
13
13
  'Content-Type': 'application/json',
14
- Authorization: `Bearer ${bearerKey || supabase_anon_key}`,
15
- ...(xApiKey && { 'x-api-key': xApiKey }),
14
+ Authorization: `Bearer ${bearerKey}`,
16
15
  'x-region': x_region,
17
16
  },
18
17
  body: JSON.stringify(cleanObject({
@@ -30,11 +29,11 @@ async function searchFlows({ query }, bearerKey, xApiKey) {
30
29
  throw error;
31
30
  }
32
31
  }
33
- export function regFlowSearchTool(server, bearerKey, xApiKey) {
32
+ export function regFlowSearchTool(server, bearerKey) {
34
33
  server.tool('Search_flows_Tool', 'Search LCA flows data.', input_schema, async ({ query }) => {
35
34
  const result = await searchFlows({
36
35
  query,
37
- }, bearerKey, xApiKey);
36
+ }, bearerKey);
38
37
  return {
39
38
  content: [
40
39
  {
@@ -1,18 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import cleanObject from '../_shared/clean_object.js';
3
- import { supabase_anon_key, supabase_base_url, x_region } from '../_shared/config.js';
3
+ import { supabase_base_url, x_region } from '../_shared/config.js';
4
4
  const input_schema = {
5
5
  query: z.string().min(1).describe('Queries from user'),
6
6
  };
7
- async function searchProcesses({ query }, bearerKey, xApiKey) {
7
+ async function searchProcesses({ query }, bearerKey) {
8
8
  const url = `${supabase_base_url}/functions/v1/process_hybrid_search`;
9
9
  try {
10
10
  const response = await fetch(url, {
11
11
  method: 'POST',
12
12
  headers: {
13
13
  'Content-Type': 'application/json',
14
- Authorization: `Bearer ${bearerKey || supabase_anon_key}`,
15
- ...(xApiKey && { 'x-api-key': xApiKey }),
14
+ Authorization: `Bearer ${bearerKey}`,
16
15
  'x-region': x_region,
17
16
  },
18
17
  body: JSON.stringify(cleanObject({
@@ -30,11 +29,11 @@ async function searchProcesses({ query }, bearerKey, xApiKey) {
30
29
  throw error;
31
30
  }
32
31
  }
33
- export function regProcessSearchTool(server, bearerKey, xApiKey) {
32
+ export function regProcessSearchTool(server, bearerKey) {
34
33
  server.tool('Search_processes_Tool', 'Search LCA processes data.', input_schema, async ({ query }) => {
35
34
  const result = await searchProcesses({
36
35
  query,
37
- }, bearerKey, xApiKey);
36
+ }, bearerKey);
38
37
  return {
39
38
  content: [
40
39
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiangong-lca/mcp-server",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
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.18.2",
31
+ "@supabase/supabase-js": "^2.58.0",
32
32
  "@types/express": "^5.0.3",
33
- "@upstash/redis": "^1.35.3",
33
+ "@upstash/redis": "^1.35.4",
34
34
  "aws-jwt-verify": "^5.1.0",
35
35
  "olca-ipc": "^2.2.1",
36
36
  "zod": "^3.25.76"
37
37
  },
38
38
  "devDependencies": {
39
- "@modelcontextprotocol/inspector": "^0.16.5",
39
+ "@modelcontextprotocol/inspector": "^0.16.8",
40
+ "@tiangong-lca/tidas-sdk": "^0.1.16",
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
+ "tsx": "^4.20.6",
46
47
  "typescript": "^5.9.2"
47
48
  }
48
49
  }