@tokenlabai/mcp-server 0.6.17 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenlabai/mcp-server",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "OpenAPI-generated TokenLab MCP server with 31 default and 80 full-profile tools for catalog, native AI, multimodal, and async APIs.",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -245,6 +245,59 @@ function responseMeta(response) {
245
245
  });
246
246
  }
247
247
 
248
+ function boundedText(value, limit = 4_000) {
249
+ return typeof value === "string" && value.length > 0 ? value.slice(0, limit) : undefined;
250
+ }
251
+
252
+ function retryAfterSeconds(value) {
253
+ if (typeof value !== "number" && typeof value !== "string") return undefined;
254
+ if (typeof value === "string" && value.trim() === "") return undefined;
255
+ const seconds = Number(value);
256
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds;
257
+ if (typeof value !== "string" || /^-?\d/.test(value)) return undefined;
258
+ const date = Date.parse(value);
259
+ return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1_000)) : undefined;
260
+ }
261
+
262
+ class TokenLabHttpError extends Error {
263
+ constructor(response, text) {
264
+ let body;
265
+ try { body = JSON.parse(text); } catch { /* Preserve non-JSON diagnostics below. */ }
266
+ const apiError = body?.error && typeof body.error === "object" ? body.error : body;
267
+ const meta = responseMeta(response);
268
+ const requestId = boundedText(meta["tokenlab/requestId"], 256)
269
+ || boundedText(apiError?.request_id, 256)
270
+ || boundedText(body?.request_id, 256);
271
+ const retryAfterHeader = boundedText(response.headers.get("retry-after"), 256);
272
+ const retryAfter = retryAfterSeconds(retryAfterHeader)
273
+ ?? retryAfterSeconds(apiError?.retry_after ?? body?.retry_after);
274
+ const declaredRetryable = apiError?.retryable ?? body?.retryable;
275
+ const message = boundedText(apiError?.message) || boundedText(text) || response.statusText;
276
+ super(`TokenLab request failed: ${response.status} ${response.statusText}${requestId ? ` (request ${requestId})` : ""}\n${boundedText(text) || message}`);
277
+ this.name = "TokenLabHttpError";
278
+ this.details = definedValues({
279
+ status: response.status,
280
+ error: definedValues({
281
+ message,
282
+ code: boundedText(apiError?.code, 256),
283
+ type: boundedText(apiError?.type, 256),
284
+ param: boundedText(apiError?.param, 256)
285
+ }),
286
+ request_id: requestId,
287
+ retryable: typeof declaredRetryable === "boolean"
288
+ ? declaredRetryable
289
+ : [408, 425, 429].includes(response.status) || response.status >= 500,
290
+ retry_after: retryAfter
291
+ });
292
+ this.meta = definedValues({
293
+ ...meta,
294
+ "tokenlab/requestId": requestId,
295
+ "tokenlab/retryAfter": retryAfterHeader,
296
+ "tokenlab/retryAfterSeconds": retryAfter
297
+ });
298
+ }
299
+ }
300
+
248
301
  async function executeGeneratedTool(tool, input) {
249
302
  requireApiKey(tool);
250
303
  const { pathArguments, queryArguments, headerArguments, bodyArguments } = collectArguments(tool, input);
@@ -287,9 +340,7 @@ async function executeGeneratedTool(tool, input) {
287
340
  const meta = responseMeta(response);
288
341
 
289
342
  if (!response.ok) {
290
- const detail = (await response.text()).slice(0, 4_000);
291
- const requestId = meta["tokenlab/requestId"] ? ` (request ${meta["tokenlab/requestId"]})` : "";
292
- throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}${requestId}\n${detail}`);
343
+ throw new TokenLabHttpError(response, await response.text());
293
344
  }
294
345
 
295
346
  if (mimeType === "application/json" || mimeType.endsWith("+json")) {
@@ -332,7 +383,9 @@ async function compareModels({ models, include_raw }) {
332
383
  const encoded = encodeURIComponent(model);
333
384
  const [details, pricing] = await Promise.all([
334
385
  executePublicJson(`/v1/models/${encoded}`),
335
- executePublicJson(`/v1/models/${encoded}/pricing`).catch((error) => ({ error: error.message }))
386
+ executePublicJson(`/v1/models/${encoded}/pricing`).catch((error) => (
387
+ error instanceof TokenLabHttpError ? { ...error.details, diagnostic: error.message } : { error: error.message }
388
+ ))
336
389
  ]);
337
390
  if (include_raw) return { model, details, pricing };
338
391
  const tokenlab = details?.tokenlab && typeof details.tokenlab === "object" ? details.tokenlab : {};
@@ -444,6 +497,14 @@ const registeredToolsByName = new Map(registeredTools.map((tool) => [
444
497
  ]));
445
498
 
446
499
  function toolExecutionError(error) {
500
+ if (error instanceof TokenLabHttpError) {
501
+ return {
502
+ content: [{ type: "text", text: error.message }],
503
+ structuredContent: error.details,
504
+ _meta: error.meta,
505
+ isError: true
506
+ };
507
+ }
447
508
  return {
448
509
  content: [{
449
510
  type: "text",
@@ -576,7 +637,7 @@ async function executePublicJson(path) {
576
637
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
577
638
  });
578
639
  const text = await response.text();
579
- if (!response.ok) throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${text.slice(0, 2_000)}`);
640
+ if (!response.ok) throw new TokenLabHttpError(response, text);
580
641
  return JSON.parse(text);
581
642
  }
582
643
 
@@ -586,7 +647,7 @@ async function executePublicText(path) {
586
647
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
587
648
  });
588
649
  const text = await response.text();
589
- if (!response.ok) throw new Error(`TokenLab request failed: ${response.status} ${response.statusText}\n${text.slice(0, 2_000)}`);
650
+ if (!response.ok) throw new TokenLabHttpError(response, text);
590
651
  return text;
591
652
  }
592
653
 
@@ -1,7 +1,7 @@
1
1
  const DRAFT_07_SCHEMA = "http://json-schema.org/draft-07/schema#";
2
2
  const PORTABLE_PROPERTY_DEPTH = 3;
3
- const MAX_DESCRIPTION_LENGTH = 500;
4
- const MAX_STRICT_DESCRIPTION_LENGTH = 160;
3
+ // Keep model-facing prose compact; execution still validates the complete schema.
4
+ const MAX_DESCRIPTION_LENGTH = 100;
5
5
 
6
6
  export const TOOL_SCHEMA_MODES = ["exact", "portable", "strict"];
7
7
 
@@ -12,6 +12,16 @@ function compactDescription(value, maxLength = MAX_DESCRIPTION_LENGTH) {
12
12
  : `${value.slice(0, maxLength - 1)}…`;
13
13
  }
14
14
 
15
+ function portableKeyword(key, value) {
16
+ const literal = key === "const" ? value : key === "enum" && value.length === 1 ? value[0] : undefined;
17
+ // Numeric singleton enums (for example duration=-1) are rejected by Gemini.
18
+ // Equal bounds express the same value without changing canonical validation.
19
+ if (typeof literal === "number" && Number.isFinite(literal)) {
20
+ return { minimum: literal, maximum: literal };
21
+ }
22
+ return { [key]: structuredClone(value) };
23
+ }
24
+
15
25
  function portableScalarSchema(schema) {
16
26
  const projected = {};
17
27
  for (const key of [
@@ -28,7 +38,7 @@ function portableScalarSchema(schema) {
28
38
  "maxLength",
29
39
  "pattern"
30
40
  ]) {
31
- if (Object.hasOwn(schema, key)) projected[key] = structuredClone(schema[key]);
41
+ if (Object.hasOwn(schema, key)) Object.assign(projected, portableKeyword(key, schema[key]));
32
42
  }
33
43
  const description = compactDescription(schema.description);
34
44
  if (description) projected.description = description;
@@ -88,7 +98,7 @@ function portableSchemaNode(schema, propertyDepth) {
88
98
  projected[key] = value.map((entry) => portableSchemaNode(entry, propertyDepth + 1));
89
99
  continue;
90
100
  }
91
- projected[key] = structuredClone(value);
101
+ Object.assign(projected, portableKeyword(key, value));
92
102
  }
93
103
  return projected;
94
104
  }
@@ -107,7 +117,7 @@ function isSimpleScalarSchema(schema) {
107
117
 
108
118
  function strictScalarSchema(schema) {
109
119
  const projected = portableScalarSchema(schema);
110
- const description = compactDescription(schema.description, MAX_STRICT_DESCRIPTION_LENGTH);
120
+ const description = compactDescription(schema.description);
111
121
  if (description) projected.description = description;
112
122
  else delete projected.description;
113
123
  delete projected.default;
@@ -115,7 +125,7 @@ function strictScalarSchema(schema) {
115
125
  }
116
126
 
117
127
  function strictPropertySchema(schema, optional) {
118
- const description = compactDescription(schema?.description, MAX_STRICT_DESCRIPTION_LENGTH);
128
+ const description = compactDescription(schema?.description);
119
129
  const valueSchema = isSimpleScalarSchema(schema)
120
130
  ? strictScalarSchema(schema)
121
131
  : {
@@ -126,6 +136,9 @@ function strictPropertySchema(schema, optional) {
126
136
  ].filter(Boolean).join(" ")
127
137
  };
128
138
  if (!optional) return valueSchema;
139
+ if (!valueSchema.enum && !Object.hasOwn(valueSchema, "const")) {
140
+ return { ...valueSchema, type: [valueSchema.type, "null"] };
141
+ }
129
142
  return {
130
143
  anyOf: [
131
144
  valueSchema,