@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/README.md +8 -8
- package/contract/openapi.json +935 -309
- package/generated/public-contract.json +6 -6
- package/generated/tools.json +547 -288
- package/package.json +2 -2
- package/scripts/generate-contract.mjs +9 -9
- package/src/index.js +62 -6
- package/src/tool-schema.js +19 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenlabai/mcp-server",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "OpenAPI-generated TokenLab MCP server with catalog, native AI, multimodal,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 : { 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
|
|
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
|
|
645
|
+
if (!response.ok) throw new TokenLabHttpError(response, text);
|
|
590
646
|
return text;
|
|
591
647
|
}
|
|
592
648
|
|
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,
|