@tiangong-lca/mcp-server 0.0.28 → 0.0.31

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/README.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  TianGong LCA Model Context Protocol (MCP) Server supports STDIO and Streamable Http protocols.
6
6
 
7
+ ## Environment
8
+
9
+ GLAD dataset search tools require a GLAD API key in the server environment:
10
+
11
+ ```bash
12
+ GLAD_API_KEY=your-glad-api-key
13
+ GLAD_API_BASE_URL=https://www.globallcadataaccess.org/api/v1
14
+ ```
15
+
7
16
  ## Starting MCP Server
8
17
 
9
18
  ### Client STDIO Server
@@ -10,3 +10,5 @@ export const supabase_publishable_key = process.env.SUPABASE_PUBLISHABLE_KEY ??
10
10
  export const x_region = process.env.X_REGION ?? 'us-east-1';
11
11
  export const redis_url = process.env.UPSTASH_REDIS_URL ?? '';
12
12
  export const redis_token = process.env.UPSTASH_REDIS_TOKEN ?? '';
13
+ export const glad_api_base_url = process.env.GLAD_API_BASE_URL ?? 'https://www.globallcadataaccess.org/api/v1';
14
+ export const glad_api_key = process.env.GLAD_API_KEY ?? '';
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { regOpenLcaPrompts } from '../prompts/lca_calculation.js';
3
3
  import { regOpenLcaResources } from '../resources/lca_calculation.js';
4
4
  import { regFlowSearchTool } from '../tools/flow_hybrid_search.js';
5
+ import { regGladDatasetTools } from '../tools/glad_dataset_search.js';
5
6
  import { regLcaCalculationGuidanceTool } from '../tools/lca_calculation_guidance.js';
6
7
  import { regLifecycleModelSearchTool } from '../tools/life_cycle_model_hybrid_search.js';
7
8
  import { regOpenLcaLciaTool } from '../tools/openlca_ipc_lcia.js';
@@ -16,6 +17,7 @@ export function initializeServer(bearerKey) {
16
17
  regFlowSearchTool(server, bearerKey);
17
18
  regProcessSearchTool(server, bearerKey);
18
19
  regLifecycleModelSearchTool(server, bearerKey);
20
+ regGladDatasetTools(server);
19
21
  regOpenLcaLciaTool(server);
20
22
  regOpenLcaListSystemProcessesTool(server);
21
23
  regOpenLcaListLCIAMethodsTool(server);
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { regCrudTool } from '../tools/db_crud.js';
3
3
  import { regFlowSearchTool } from '../tools/flow_hybrid_search.js';
4
+ import { regGladDatasetTools } from '../tools/glad_dataset_search.js';
4
5
  import { regLifecycleModelSearchTool } from '../tools/life_cycle_model_hybrid_search.js';
5
6
  import { regProcessSearchTool } from '../tools/process_hybrid_search.js';
6
7
  export function initializeServer(bearerKey, supabaseSession) {
@@ -11,6 +12,7 @@ export function initializeServer(bearerKey, supabaseSession) {
11
12
  regFlowSearchTool(server, bearerKey);
12
13
  regProcessSearchTool(server, bearerKey);
13
14
  regLifecycleModelSearchTool(server, bearerKey);
15
+ regGladDatasetTools(server);
14
16
  regCrudTool(server, supabaseSession ?? bearerKey);
15
17
  return server;
16
18
  }
@@ -1,10 +1,10 @@
1
- import { createClient, FunctionRegion } from '@supabase/supabase-js';
1
+ import { createClient } from '@supabase/supabase-js';
2
2
  import { z } from 'zod';
3
3
  import { supabase_base_url, supabase_publishable_key } from '../_shared/config.js';
4
4
  import { resolveSupabaseAccessToken } from '../_shared/supabase_session.js';
5
+ import { prepareLifecycleModelFile } from './life_cycle_model_file_tools.js';
5
6
  const allowedTables = ['contacts', 'flows', 'lifecyclemodels', 'processes', 'sources'];
6
7
  const tableSchema = z.enum(allowedTables);
7
- const UPDATE_FUNCTION_NAME = 'update_data';
8
8
  const MAX_VALIDATION_ERROR_LENGTH = 4_000;
9
9
  const tablePrimaryKey = {
10
10
  contacts: 'id',
@@ -50,7 +50,7 @@ const toolParamsSchema = {
50
50
  jsonOrdered: z
51
51
  .unknown()
52
52
  .optional()
53
- .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete).'),
53
+ .describe('JSON value persisted into json_ordered (required for insert/update; omit for select/delete). For lifecyclemodels, native files, platform bundles, raw records, or a single-item array of those are accepted; json_tg and rule_verification are derived automatically before write.'),
54
54
  };
55
55
  const refinedInputSchema = z
56
56
  .object(toolParamsSchema)
@@ -175,6 +175,51 @@ async function validateJsonOrdered(table, jsonOrdered) {
175
175
  throw error;
176
176
  }
177
177
  }
178
+ function sanitizeLifecycleModelRows(rows) {
179
+ return rows.map((row) => {
180
+ const record = row && typeof row === 'object' && !Array.isArray(row)
181
+ ? row
182
+ : {};
183
+ return {
184
+ id: record.id ?? null,
185
+ version: record.version ?? null,
186
+ json_ordered: record.json_ordered ?? null,
187
+ };
188
+ });
189
+ }
190
+ function sanitizeRowsForOutput(table, rows) {
191
+ return table === 'lifecyclemodels' ? sanitizeLifecycleModelRows(rows) : rows;
192
+ }
193
+ async function prepareWritePayload(table, jsonOrdered, inputId, inputVersion, bearerKey) {
194
+ if (table !== 'lifecyclemodels') {
195
+ await validateJsonOrdered(table, jsonOrdered);
196
+ return {
197
+ payload: {
198
+ json_ordered: jsonOrdered,
199
+ },
200
+ resolvedId: inputId,
201
+ resolvedVersion: inputVersion,
202
+ };
203
+ }
204
+ const prepared = await prepareLifecycleModelFile({
205
+ payload: jsonOrdered,
206
+ }, bearerKey);
207
+ if (inputId && inputId !== prepared.lifecycleModelId) {
208
+ throw new Error(`Provided id (${inputId}) does not match lifecycle model UUID (${prepared.lifecycleModelId}).`);
209
+ }
210
+ if (inputVersion && inputVersion !== prepared.lifecycleModelVersion) {
211
+ throw new Error(`Provided version (${inputVersion}) does not match lifecycle model version (${prepared.lifecycleModelVersion}).`);
212
+ }
213
+ return {
214
+ payload: {
215
+ json_ordered: prepared.jsonOrdered,
216
+ json_tg: prepared.jsonTg,
217
+ rule_verification: prepared.ruleVerification,
218
+ },
219
+ resolvedId: prepared.lifecycleModelId,
220
+ resolvedVersion: prepared.lifecycleModelVersion,
221
+ };
222
+ }
178
223
  async function createSupabaseClient(bearerKey) {
179
224
  const { session: normalizedSession, accessToken: bearerToken } = resolveSupabaseAccessToken(bearerKey);
180
225
  const supabase = createClient(supabase_base_url, supabase_publishable_key, {
@@ -206,7 +251,8 @@ async function createSupabaseClient(bearerKey) {
206
251
  async function handleSelect(supabase, input) {
207
252
  const { table, limit, id, version, filters } = input;
208
253
  const keyColumn = getPrimaryKeyColumn(table);
209
- let queryBuilder = supabase.from(table).select('*');
254
+ const selectColumns = table === 'lifecyclemodels' ? 'id, version, json_ordered' : '*';
255
+ let queryBuilder = supabase.from(table).select(selectColumns);
210
256
  if (filters) {
211
257
  for (const [column, value] of Object.entries(filters)) {
212
258
  if (value !== null && value !== undefined) {
@@ -228,10 +274,11 @@ async function handleSelect(supabase, input) {
228
274
  console.error('Error querying the database:', error);
229
275
  throw error;
230
276
  }
231
- return JSON.stringify({ data: data ?? [], count: data?.length ?? 0 });
277
+ const rows = sanitizeRowsForOutput(table, (data ?? []));
278
+ return JSON.stringify({ data: rows, count: rows.length });
232
279
  }
233
- async function handleInsert(supabase, input) {
234
- const { table, jsonOrdered, id } = input;
280
+ async function handleInsert(supabase, input, bearerKey) {
281
+ const { table, jsonOrdered, id, version } = input;
235
282
  if (jsonOrdered === undefined) {
236
283
  throw new Error('jsonOrdered is required for insert operations.');
237
284
  }
@@ -239,19 +286,28 @@ async function handleInsert(supabase, input) {
239
286
  throw new Error('id is required for insert operations.');
240
287
  }
241
288
  const jsonOrderedValue = jsonOrdered;
242
- await validateJsonOrdered(table, jsonOrderedValue);
289
+ const preparedWrite = await prepareWritePayload(table, jsonOrderedValue, id, version, bearerKey);
290
+ const resolvedId = preparedWrite.resolvedId ?? id;
291
+ const resolvedVersion = preparedWrite.resolvedVersion ?? version;
243
292
  const keyColumn = getPrimaryKeyColumn(table);
244
293
  const { data, error } = await supabase
245
294
  .from(table)
246
- .insert([{ [keyColumn]: id, json_ordered: jsonOrderedValue }])
295
+ .insert([
296
+ {
297
+ [keyColumn]: resolvedId,
298
+ ...(resolvedVersion !== undefined ? { version: resolvedVersion } : {}),
299
+ ...preparedWrite.payload,
300
+ },
301
+ ])
247
302
  .select();
248
303
  if (error) {
249
304
  console.error('Error inserting into the database:', error);
250
305
  throw error;
251
306
  }
252
- return JSON.stringify({ id, data: data ?? [] });
307
+ const rows = sanitizeRowsForOutput(table, (data ?? []));
308
+ return JSON.stringify({ id: resolvedId, version: resolvedVersion, data: rows });
253
309
  }
254
- async function handleUpdate(supabase, accessToken, input) {
310
+ async function handleUpdate(supabase, accessToken, input, bearerKey) {
255
311
  const { table, id, version, jsonOrdered } = input;
256
312
  if (id === undefined) {
257
313
  throw new Error('id is required for update operations.');
@@ -263,29 +319,27 @@ async function handleUpdate(supabase, accessToken, input) {
263
319
  throw new Error('jsonOrdered is required for update operations.');
264
320
  }
265
321
  const jsonOrderedValue = jsonOrdered;
266
- await validateJsonOrdered(table, jsonOrderedValue);
267
- const token = requireAccessToken(accessToken);
268
- const { data: functionPayload, error } = await supabase.functions.invoke(UPDATE_FUNCTION_NAME, {
269
- headers: {
270
- Authorization: `Bearer ${token}`,
271
- },
272
- body: { id, version, table, data: { json_ordered: jsonOrderedValue } },
273
- region: FunctionRegion.UsEast1,
274
- });
322
+ const preparedWrite = await prepareWritePayload(table, jsonOrderedValue, id, version, bearerKey);
323
+ requireAccessToken(accessToken);
324
+ const keyColumn = getPrimaryKeyColumn(table);
325
+ const resolvedId = preparedWrite.resolvedId ?? id;
326
+ const resolvedVersion = preparedWrite.resolvedVersion ?? version;
327
+ const { data, error } = await supabase
328
+ .from(table)
329
+ .update(preparedWrite.payload)
330
+ .eq(keyColumn, resolvedId)
331
+ .eq('version', resolvedVersion)
332
+ .select();
275
333
  if (error) {
276
- console.error('Error invoking update_data function:', error);
334
+ console.error('Error updating the database:', error);
277
335
  throw error;
278
336
  }
279
- const { data: updatedRows, error: functionError } = (functionPayload ??
280
- {});
281
- if (functionError) {
282
- console.error('Supabase update_data returned an error:', functionError);
283
- const message = functionError.message ?? 'Supabase update_data function rejected the request.';
284
- throw new Error(message);
285
- }
286
- const keyColumn = getPrimaryKeyColumn(table);
287
- const rows = ensureRows(updatedRows, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
288
- return JSON.stringify({ id, version, data: rows });
337
+ const rows = ensureRows(data, `Update affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${resolvedId}) and version (${resolvedVersion}) exist and are accessible.`);
338
+ return JSON.stringify({
339
+ id: resolvedId,
340
+ version: resolvedVersion,
341
+ data: sanitizeRowsForOutput(table, rows),
342
+ });
289
343
  }
290
344
  async function handleDelete(supabase, input) {
291
345
  const { table, id, version } = input;
@@ -307,7 +361,7 @@ async function handleDelete(supabase, input) {
307
361
  throw error;
308
362
  }
309
363
  const rows = ensureRows(data, `Delete affected 0 rows for table "${table}"; verify the provided ${keyColumn} (${id}) and version (${version}) exist and are accessible.`);
310
- return JSON.stringify({ id, version, data: rows });
364
+ return JSON.stringify({ id, version, data: sanitizeRowsForOutput(table, rows) });
311
365
  }
312
366
  async function performCrud(input, bearerKey) {
313
367
  try {
@@ -316,9 +370,9 @@ async function performCrud(input, bearerKey) {
316
370
  case 'select':
317
371
  return handleSelect(supabase, input);
318
372
  case 'insert':
319
- return handleInsert(supabase, input);
373
+ return handleInsert(supabase, input, bearerKey);
320
374
  case 'update':
321
- return handleUpdate(supabase, accessToken, input);
375
+ return handleUpdate(supabase, accessToken, input, bearerKey);
322
376
  case 'delete':
323
377
  return handleDelete(supabase, input);
324
378
  default: {
@@ -333,7 +387,7 @@ async function performCrud(input, bearerKey) {
333
387
  }
334
388
  }
335
389
  export function regCrudTool(server, bearerKey) {
336
- 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) => {
390
+ server.tool('Database_CRUD_Tool', 'Perform select/insert/update/delete against allowed Supabase tables (insert needs jsonOrdered, update/delete need id and version). lifecyclemodels insert/update automatically validate the payload, derive platform json_tg, compute rule_verification, and then write the row; lifecyclemodels select returns id/version/json_ordered only.', toolParamsSchema, async (rawInput) => {
337
391
  const input = refinedInputSchema.parse(rawInput);
338
392
  const result = await performCrud(input, bearerKey);
339
393
  return {
@@ -0,0 +1,332 @@
1
+ import { z } from 'zod';
2
+ import { glad_api_base_url, glad_api_key } from '../_shared/config.js';
3
+ const sectorSchema = z.enum([
4
+ 'Agriculture',
5
+ 'Chemicals',
6
+ 'Construction',
7
+ 'Electronics',
8
+ 'Energy',
9
+ 'Food',
10
+ 'Metals',
11
+ 'Mining',
12
+ 'Textiles',
13
+ 'Transport',
14
+ 'Other',
15
+ ]);
16
+ const formatSchema = z.enum(['ECOSPOLD1', 'ECOSPOLD2', 'ILCD', 'JSON-LD', 'OTHER', 'UNKNOWN']);
17
+ const processTypeSchema = z.enum([
18
+ 'UNIT',
19
+ 'PARTIALLY_AGGREGATED',
20
+ 'FULLY_AGGREGATED',
21
+ 'BRIDGE',
22
+ 'UNKNOWN',
23
+ ]);
24
+ const modelingTypeSchema = z.enum(['ATTRIBUTIONAL', 'CONSEQUENTIAL', 'BEFORE_MODELING', 'UNKNOWN']);
25
+ const reviewTypeSchema = z.enum(['INTERNAL', 'EXTERNAL', 'PANEL', 'UNKNOWN', 'NONE']);
26
+ const reviewSystemSchema = z.enum([
27
+ 'ILCD',
28
+ 'PEF',
29
+ 'GHG',
30
+ 'LCA_UN',
31
+ 'OTHER',
32
+ 'UNKNOWN',
33
+ 'NOT_APPLICABLE',
34
+ ]);
35
+ const extraFilterValueSchema = z.union([
36
+ z.string(),
37
+ z.number(),
38
+ z.boolean(),
39
+ z.array(z.union([z.string(), z.number(), z.boolean()])),
40
+ ]);
41
+ const gladSearchInputSchema = z.object({
42
+ query: z
43
+ .string()
44
+ .min(1)
45
+ .optional()
46
+ .describe('Full-text search query. GLAD searches name, description, category, and technology; multiple words are combined with AND.'),
47
+ page: z.number().int().min(0).default(0).describe('Zero-based GLAD result page. Default: 0.'),
48
+ pageSize: z
49
+ .number()
50
+ .int()
51
+ .min(1)
52
+ .max(100)
53
+ .default(10)
54
+ .describe('Number of datasets to return. Capped at 100 to keep MCP responses usable.'),
55
+ sortBy: z.string().min(1).optional().describe('Field name to sort by, for example name.'),
56
+ sortOrder: z.enum(['ASC', 'DESC']).default('ASC').describe('Sort order used when sortBy is set.'),
57
+ sectors: z.array(sectorSchema).optional().describe('Filter by one or more GLAD sectors.'),
58
+ format: z.array(formatSchema).optional().describe('Filter by one or more dataset formats.'),
59
+ location: z
60
+ .array(z.string().min(1))
61
+ .optional()
62
+ .describe('Filter by one or more location codes, for example DE or ZA.'),
63
+ dataprovider: z
64
+ .array(z.string().min(1))
65
+ .optional()
66
+ .describe('Filter by one or more data provider names.'),
67
+ supportedNomenclatures: z
68
+ .array(z.string().min(1))
69
+ .optional()
70
+ .describe('Filter by supported nomenclature names, for example ecoinvent 3.5.'),
71
+ lciaMethods: z.array(z.string().min(1)).optional().describe('Filter by LCIA method names.'),
72
+ category: z
73
+ .array(z.string().min(1))
74
+ .optional()
75
+ .describe('Filter by category name or ISIC4 category code.'),
76
+ categoryPaths: z.array(z.string().min(1)).optional().describe('Filter by GLAD category paths.'),
77
+ unspscPaths: z.array(z.string().min(1)).optional().describe('Filter by UNSPSC path values.'),
78
+ co2pePaths: z.array(z.string().min(1)).optional().describe('Filter by CO2PE path values.'),
79
+ processType: z.array(processTypeSchema).optional().describe('Filter by GLAD process type.'),
80
+ modelingType: z.array(modelingTypeSchema).optional().describe('Filter by GLAD modeling type.'),
81
+ reviewType: z.array(reviewTypeSchema).optional().describe('Filter by GLAD review type.'),
82
+ reviewSystem: z.array(reviewSystemSchema).optional().describe('Filter by GLAD review system.'),
83
+ copyrightHolder: z.array(z.string().min(1)).optional().describe('Filter by copyright holder.'),
84
+ contact: z.string().min(1).optional().describe('Filter by contact value.'),
85
+ validFromYear: z.number().int().optional().describe('Filter by validity start year.'),
86
+ validUntilYear: z.number().int().optional().describe('Filter by validity end year.'),
87
+ copyrightProtected: z.boolean().optional().describe('Filter by copyright-protected status.'),
88
+ free: z.boolean().optional().describe('Filter by whether the dataset is available for free.'),
89
+ publiclyAccessible: z
90
+ .boolean()
91
+ .optional()
92
+ .describe('Filter by whether the dataset URL is publicly accessible without further login.'),
93
+ extraFilters: z
94
+ .record(z.string(), extraFilterValueSchema)
95
+ .optional()
96
+ .describe('Advanced GLAD query parameters not modeled explicitly. Use official parameter names; arrays are sent as repeated query parameters.'),
97
+ });
98
+ const gladGetDatasetInputSchema = z.object({
99
+ refId: z.string().min(1).describe('GLAD dataset refId.'),
100
+ dataProvider: z.string().min(1).describe('GLAD data provider name for the dataset.'),
101
+ });
102
+ const resultInfoSchema = z
103
+ .object({
104
+ currentPage: z.number().optional(),
105
+ pageSize: z.number().optional(),
106
+ pageCount: z.number().optional(),
107
+ count: z.number().optional(),
108
+ totalCount: z.number().optional(),
109
+ })
110
+ .passthrough();
111
+ const passthroughObjectSchema = z.object({}).passthrough();
112
+ const gladSearchOutputSchema = z
113
+ .object({
114
+ request: z
115
+ .object({
116
+ endpoint: z.string(),
117
+ page: z.number(),
118
+ pageSize: z.number(),
119
+ })
120
+ .passthrough(),
121
+ resultInfo: resultInfoSchema.optional(),
122
+ data: z.array(passthroughObjectSchema),
123
+ aggregations: z.array(passthroughObjectSchema),
124
+ })
125
+ .passthrough();
126
+ const gladGetDatasetOutputSchema = z
127
+ .object({
128
+ request: z
129
+ .object({
130
+ endpoint: z.string(),
131
+ refId: z.string(),
132
+ dataProvider: z.string(),
133
+ })
134
+ .passthrough(),
135
+ dataSet: passthroughObjectSchema,
136
+ })
137
+ .passthrough();
138
+ const ARRAY_PARAM_MAP = {
139
+ 'sectors[]': 'sectors',
140
+ 'format[]': 'format',
141
+ 'location[]': 'location',
142
+ 'dataprovider[]': 'dataprovider',
143
+ 'supportedNomenclatures[]': 'supportedNomenclatures',
144
+ 'lciaMethods[]': 'lciaMethods',
145
+ 'category[]': 'category',
146
+ 'categoryPaths[]': 'categoryPaths',
147
+ 'unspscPaths[]': 'unspscPaths',
148
+ 'co2pePaths[]': 'co2pePaths',
149
+ 'processType[]': 'processType',
150
+ 'modelingType[]': 'modelingType',
151
+ 'reviewType[]': 'reviewType',
152
+ 'reviewSystem[]': 'reviewSystem',
153
+ 'copyrightHolder[]': 'copyrightHolder',
154
+ };
155
+ const SCALAR_PARAM_NAMES = [
156
+ 'query',
157
+ 'sortBy',
158
+ 'sortOrder',
159
+ 'page',
160
+ 'pageSize',
161
+ 'contact',
162
+ 'validFromYear',
163
+ 'validUntilYear',
164
+ 'copyrightProtected',
165
+ 'free',
166
+ 'publiclyAccessible',
167
+ ];
168
+ function createGladUrl(pathname) {
169
+ const baseUrl = glad_api_base_url.replace(/\/+$/, '');
170
+ return new URL(`${baseUrl}${pathname}`);
171
+ }
172
+ function appendParam(params, key, value) {
173
+ if (value === undefined || value === null || value === '') {
174
+ return;
175
+ }
176
+ if (Array.isArray(value)) {
177
+ for (const item of value) {
178
+ appendParam(params, key, item);
179
+ }
180
+ return;
181
+ }
182
+ params.append(key, String(value));
183
+ }
184
+ function appendSearchParams(url, input) {
185
+ for (const key of SCALAR_PARAM_NAMES) {
186
+ appendParam(url.searchParams, key, input[key]);
187
+ }
188
+ for (const [wireName, inputKey] of Object.entries(ARRAY_PARAM_MAP)) {
189
+ appendParam(url.searchParams, wireName, input[inputKey]);
190
+ }
191
+ if (input.extraFilters) {
192
+ for (const [key, value] of Object.entries(input.extraFilters)) {
193
+ appendParam(url.searchParams, key, value);
194
+ }
195
+ }
196
+ }
197
+ function assertGladApiKey() {
198
+ if (!glad_api_key) {
199
+ throw new Error('Missing GLAD_API_KEY. Add it to the MCP server environment before using GLAD tools.');
200
+ }
201
+ return glad_api_key;
202
+ }
203
+ function buildGladErrorMessage(status, statusText, body) {
204
+ const trimmedBody = body.trim();
205
+ if (trimmedBody.startsWith('<!DOCTYPE html') || trimmedBody.startsWith('<html')) {
206
+ return `GLAD API returned an HTML page instead of JSON (HTTP ${status} ${statusText}). This usually means Cloudflare or browser verification blocked this Node.js runtime before the GLAD API processed the api-key. Verify this runtime can reach GLAD directly, or point GLAD_API_BASE_URL at an accessible GLAD-compatible API endpoint.`;
207
+ }
208
+ try {
209
+ const parsed = JSON.parse(trimmedBody);
210
+ if (typeof parsed === 'object' && parsed !== null && 'message' in parsed) {
211
+ return `GLAD API request failed (HTTP ${status} ${statusText}): ${String(parsed.message)}`;
212
+ }
213
+ }
214
+ catch {
215
+ }
216
+ const preview = trimmedBody.slice(0, 500);
217
+ return `GLAD API request failed (HTTP ${status} ${statusText})${preview ? `: ${preview}` : ''}`;
218
+ }
219
+ async function requestGladJson(url) {
220
+ const response = await fetch(url, {
221
+ method: 'GET',
222
+ headers: {
223
+ Accept: 'application/json',
224
+ 'User-Agent': 'TianGong-LCA-MCP/1.0',
225
+ 'api-key': assertGladApiKey(),
226
+ },
227
+ });
228
+ const text = await response.text();
229
+ if (!response.ok) {
230
+ throw new Error(buildGladErrorMessage(response.status, response.statusText, text));
231
+ }
232
+ try {
233
+ return JSON.parse(text);
234
+ }
235
+ catch (error) {
236
+ throw new Error(`GLAD API returned a non-JSON success response from ${url.pathname}: ${error.message}`);
237
+ }
238
+ }
239
+ async function searchGladDatasets(input) {
240
+ const url = createGladUrl('/search');
241
+ appendSearchParams(url, input);
242
+ const result = await requestGladJson(url);
243
+ return {
244
+ request: {
245
+ endpoint: `${url.origin}${url.pathname}`,
246
+ page: input.page,
247
+ pageSize: input.pageSize,
248
+ },
249
+ resultInfo: result.resultInfo,
250
+ data: result.data ?? [],
251
+ aggregations: result.aggregations ?? [],
252
+ };
253
+ }
254
+ async function getGladDataset(input) {
255
+ const url = createGladUrl(`/search/index/${encodeURIComponent(input.refId)}/${encodeURIComponent(input.dataProvider)}`);
256
+ const dataSet = await requestGladJson(url);
257
+ return {
258
+ request: {
259
+ endpoint: `${url.origin}${url.pathname}`,
260
+ refId: input.refId,
261
+ dataProvider: input.dataProvider,
262
+ },
263
+ dataSet,
264
+ };
265
+ }
266
+ function toSuccessResult(output) {
267
+ return {
268
+ content: [
269
+ {
270
+ type: 'text',
271
+ text: JSON.stringify(output, null, 2),
272
+ },
273
+ ],
274
+ structuredContent: output,
275
+ };
276
+ }
277
+ function toErrorResult(error) {
278
+ const message = error instanceof Error ? error.message : String(error);
279
+ return {
280
+ isError: true,
281
+ content: [
282
+ {
283
+ type: 'text',
284
+ text: message,
285
+ },
286
+ ],
287
+ };
288
+ }
289
+ export function regGladDatasetTools(server) {
290
+ server.registerTool('Search_GLAD_Datasets_Tool', {
291
+ title: 'Search GLAD Datasets',
292
+ description: 'Search Global LCA Data Access (GLAD) dataset descriptors through the official GLAD /search API. Requires GLAD_API_KEY in the MCP server environment.',
293
+ inputSchema: gladSearchInputSchema,
294
+ outputSchema: gladSearchOutputSchema,
295
+ annotations: {
296
+ readOnlyHint: true,
297
+ destructiveHint: false,
298
+ idempotentHint: true,
299
+ openWorldHint: true,
300
+ },
301
+ }, async (rawInput) => {
302
+ try {
303
+ const input = gladSearchInputSchema.parse(rawInput);
304
+ const output = await searchGladDatasets(input);
305
+ return toSuccessResult(output);
306
+ }
307
+ catch (error) {
308
+ return toErrorResult(error);
309
+ }
310
+ });
311
+ server.registerTool('Get_GLAD_Dataset_Tool', {
312
+ title: 'Get GLAD Dataset',
313
+ description: 'Fetch one GLAD dataset descriptor by refId and dataProvider through the official GLAD /search/index/{refId}/{dataProvider} API. Requires GLAD_API_KEY in the MCP server environment.',
314
+ inputSchema: gladGetDatasetInputSchema,
315
+ outputSchema: gladGetDatasetOutputSchema,
316
+ annotations: {
317
+ readOnlyHint: true,
318
+ destructiveHint: false,
319
+ idempotentHint: true,
320
+ openWorldHint: true,
321
+ },
322
+ }, async (rawInput) => {
323
+ try {
324
+ const input = gladGetDatasetInputSchema.parse(rawInput);
325
+ const output = await getGladDataset(input);
326
+ return toSuccessResult(output);
327
+ }
328
+ catch (error) {
329
+ return toErrorResult(error);
330
+ }
331
+ });
332
+ }