@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/README.md +3 -2
- package/contract/openapi.json +935 -309
- package/generated/public-contract.json +6 -6
- package/generated/tools.json +547 -288
- package/package.json +1 -1
- package/src/index.js +67 -6
- package/src/tool-schema.js +19 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenlabai/mcp-server",
|
|
3
|
-
"version": "0.6.
|
|
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
|
-
|
|
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) => (
|
|
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
|
|
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
|
|
650
|
+
if (!response.ok) throw new TokenLabHttpError(response, text);
|
|
590
651
|
return text;
|
|
591
652
|
}
|
|
592
653
|
|
package/src/tool-schema.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|