@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.
- package/dist/src/_shared/auth_middleware.js +117 -29
- package/dist/src/_shared/config.js +1 -2
- package/dist/src/_shared/init_server_http.js +7 -7
- package/dist/src/_shared/supabase_session.js +89 -0
- package/dist/src/index_server.js +2 -1
- package/dist/src/tools/db_crud.js +293 -0
- package/dist/src/tools/flow_hybrid_search.js +5 -6
- package/dist/src/tools/process_hybrid_search.js +5 -6
- package/package.json +9 -8
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
67
|
-
|
|
68
|
-
isAuthenticated: false,
|
|
69
|
-
response: 'You are not an authenticated user.',
|
|
70
|
-
};
|
|
112
|
+
else if (typeof cachedPayload === 'object') {
|
|
113
|
+
applyCachedObject(cachedPayload);
|
|
71
114
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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:
|
|
85
|
-
userId:
|
|
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
|
|
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
|
-
|
|
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
|
|
12
|
-
regProcessSearchTool(server, bearerKey
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/index_server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
31
|
-
"@supabase/supabase-js": "^2.
|
|
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.
|
|
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.
|
|
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": "^
|
|
42
|
+
"npm-check-updates": "^19.0.0",
|
|
42
43
|
"prettier": "^3.6.2",
|
|
43
|
-
"prettier-plugin-organize-imports": "^4.
|
|
44
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
44
45
|
"shx": "^0.4.0",
|
|
45
|
-
"tsx": "^4.20.
|
|
46
|
+
"tsx": "^4.20.6",
|
|
46
47
|
"typescript": "^5.9.2"
|
|
47
48
|
}
|
|
48
49
|
}
|