@tokenlabai/mcp-server 0.6.15 → 0.6.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tokenlabai/mcp-server",
3
- "version": "0.6.15",
4
- "description": "OpenAPI-generated TokenLab MCP server with catalog, native AI, multimodal, resource, prompt, and async task support.",
3
+ "version": "0.6.18",
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": {
7
7
  "node": ">=18.17"
@@ -553,21 +553,21 @@ let synchronizedReadme = replaceProjection(
553
553
  );
554
554
  synchronizedReadme = replaceProjection(
555
555
  synchronizedReadme,
556
- /\| `catalog` \| \d+ \|/,
557
- `| \`catalog\` | ${catalogProfile.endpoint_tools} |`,
558
- "README catalog profile count"
556
+ /\| `catalog` \| \d+ \| \d+ \|/,
557
+ `| \`catalog\` | ${catalogProfile.endpoint_tools} | ${catalogProfile.total_tools} |`,
558
+ "README catalog profile counts"
559
559
  );
560
560
  synchronizedReadme = replaceProjection(
561
561
  synchronizedReadme,
562
- /\| `core` \(default\) \| \d+ \|/,
563
- `| \`core\` (default) | ${coreProfile.endpoint_tools} |`,
564
- "README core profile count"
562
+ /\| `core` \(default\) \| \d+ \| \d+ \|/,
563
+ `| \`core\` (default) | ${coreProfile.endpoint_tools} | ${coreProfile.total_tools} |`,
564
+ "README core profile counts"
565
565
  );
566
566
  synchronizedReadme = replaceProjection(
567
567
  synchronizedReadme,
568
- /\| `full` \| \d+ \|/,
569
- `| \`full\` | ${fullProfile.endpoint_tools} |`,
570
- "README full profile count"
568
+ /\| `full` \| \d+ \| \d+ \|/,
569
+ `| \`full\` | ${fullProfile.endpoint_tools} | ${fullProfile.total_tools} |`,
570
+ "README full profile counts"
571
571
  );
572
572
  synchronizedReadme = replaceProjection(
573
573
  synchronizedReadme,
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${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 : { 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,9 @@ const registeredToolsByName = new Map(registeredTools.map((tool) => [
444
497
  ]));
445
498
 
446
499
  function toolExecutionError(error) {
500
+ if (error instanceof TokenLabHttpError) {
501
+ return { ...textResult(error.details, error.meta), isError: true };
502
+ }
447
503
  return {
448
504
  content: [{
449
505
  type: "text",
@@ -576,7 +632,7 @@ async function executePublicJson(path) {
576
632
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
577
633
  });
578
634
  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)}`);
635
+ if (!response.ok) throw new TokenLabHttpError(response, text);
580
636
  return JSON.parse(text);
581
637
  }
582
638
 
@@ -586,7 +642,7 @@ async function executePublicText(path) {
586
642
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
587
643
  });
588
644
  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)}`);
645
+ if (!response.ok) throw new TokenLabHttpError(response, text);
590
646
  return text;
591
647
  }
592
648
 
@@ -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,