@tokenlabai/mcp-server 0.6.7 → 0.6.9

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.7",
3
+ "version": "0.6.9",
4
4
  "description": "OpenAPI-generated TokenLab MCP server with catalog, native AI, multimodal, resource, prompt, and async task support.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -112,6 +112,17 @@ for (const [profileName, profile] of profileEntries) {
112
112
  }
113
113
  }
114
114
  }
115
+ for (const [operationId, variants] of Object.entries(overlay.operation_variants || {})) {
116
+ if (!operations.has(operationId)) {
117
+ throw new Error(`MCP operation variants reference an operation missing from OpenAPI: ${operationId}`);
118
+ }
119
+ if (!Array.isArray(variants) || variants.length === 0) {
120
+ throw new Error(`MCP operation variants for ${operationId} must be a non-empty array`);
121
+ }
122
+ for (const variant of variants) {
123
+ if (!variant.tool_name) throw new Error(`MCP operation variant for ${operationId} is missing tool_name`);
124
+ }
125
+ }
115
126
 
116
127
  function operationProfiles(operationId, tags) {
117
128
  return profileEntries
@@ -136,7 +147,24 @@ function chooseContentTypes(operationId, operation, override) {
136
147
  return [available.includes("application/json") ? "application/json" : available[0]];
137
148
  }
138
149
 
139
- function buildInputSchema(pathItem, operation, contentType) {
150
+ function requestSchemaForOverride(operationId, media, override) {
151
+ if (!override.request_schema) return media.schema || { type: "object", additionalProperties: true };
152
+ if (typeof override.request_schema === "string") {
153
+ if (!override.request_schema.startsWith("#/")) {
154
+ throw new Error(`${operationId} request_schema must be a local OpenAPI reference`);
155
+ }
156
+ if (!getByPointer(override.request_schema)) {
157
+ throw new Error(`${operationId} request_schema does not resolve: ${override.request_schema}`);
158
+ }
159
+ return { $ref: override.request_schema };
160
+ }
161
+ if (typeof override.request_schema !== "object" || Array.isArray(override.request_schema)) {
162
+ throw new Error(`${operationId} request_schema must be an object or local OpenAPI reference`);
163
+ }
164
+ return override.request_schema;
165
+ }
166
+
167
+ function buildInputSchema(operationId, pathItem, operation, contentType, override) {
140
168
  const properties = {};
141
169
  const required = [];
142
170
  const bindings = { path: [], query: [], header: [], body: [], files: [] };
@@ -156,7 +184,7 @@ function buildInputSchema(pathItem, operation, contentType) {
156
184
 
157
185
  if (contentType) {
158
186
  const media = operation.requestBody.content[contentType];
159
- const bodySchema = normalizeSchema(media.schema || { type: "object", additionalProperties: true });
187
+ const bodySchema = normalizeSchema(requestSchemaForOverride(operationId, media, override));
160
188
  if (bodySchema.type === "object" || bodySchema.properties) {
161
189
  for (const [name, schema] of Object.entries(bodySchema.properties || {})) {
162
190
  if (properties[name]) throw new Error(`MCP argument collision for ${operation.operationId}: ${name}`);
@@ -227,6 +255,41 @@ function applyInputOverrides(schema, bindings, override) {
227
255
  }
228
256
  schema.properties[name] = { ...schema.properties[name], default: value };
229
257
  }
258
+ for (const [name, value] of Object.entries(override.fixed_arguments || {})) {
259
+ const property = schema.properties[name];
260
+ if (!property) throw new Error(`Cannot fix unknown MCP argument: ${name}`);
261
+ if (Array.isArray(property.enum) && !property.enum.includes(value)) {
262
+ throw new Error(`Fixed MCP argument ${name} is outside its enum`);
263
+ }
264
+ if (Object.hasOwn(property, "const") && property.const !== value) {
265
+ throw new Error(`Fixed MCP argument ${name} does not match its const value`);
266
+ }
267
+ delete schema.properties[name];
268
+ if (schema.required) schema.required = schema.required.filter((entry) => entry !== name);
269
+ }
270
+ }
271
+
272
+ function mergeOperationOverride(base, variant) {
273
+ return {
274
+ ...base,
275
+ ...variant,
276
+ input_property_overrides: {
277
+ ...(base.input_property_overrides || {}),
278
+ ...(variant.input_property_overrides || {})
279
+ },
280
+ default_arguments: {
281
+ ...(base.default_arguments || {}),
282
+ ...(variant.default_arguments || {})
283
+ },
284
+ fixed_arguments: {
285
+ ...(base.fixed_arguments || {}),
286
+ ...(variant.fixed_arguments || {})
287
+ },
288
+ annotations: {
289
+ ...(base.annotations || {}),
290
+ ...(variant.annotations || {})
291
+ }
292
+ };
230
293
  }
231
294
 
232
295
  function annotations(method) {
@@ -244,39 +307,49 @@ for (const [operationId, indexed] of operations) {
244
307
  const profiles = operationProfiles(operationId, tags);
245
308
  if (profiles.length === 0) continue;
246
309
 
247
- const override = overlay.operation_overrides[operationId] || {};
248
- const contentTypes = chooseContentTypes(operationId, indexed.operation, override);
249
- for (const contentType of contentTypes) {
250
- const suffix = contentTypes.length > 1 && contentType !== "application/json"
251
- ? `_${contentType.split("/").at(-1).replace(/[^A-Za-z0-9]+/g, "_")}`
252
- : "";
253
- const toolName = override.tool_names_by_content_type?.[contentType]
254
- || `${override.tool_name || snakeCase(operationId)}${suffix}`;
255
- const { schema, bindings } = buildInputSchema(indexed.pathItem, indexed.operation, contentType);
256
- applyInputOverrides(schema, bindings, override);
257
- const description = [indexed.operation.summary, indexed.operation.description]
258
- .filter(Boolean)
259
- .join(" ")
260
- .replace(/\s+/g, " ")
261
- .trim();
262
-
263
- tools.push({
264
- name: toolName,
265
- title: indexed.operation.summary?.replace(/\s+/g, " ").trim() || toolName,
266
- operation_id: operationId,
267
- method: indexed.method,
268
- path: indexed.path,
269
- content_type: contentType,
270
- auth: publicAuth.has(operationId) ? "optional" : "required",
271
- tags,
272
- profiles,
273
- description,
274
- input_schema: schema,
275
- bindings,
276
- ...(override.default_arguments ? { default_arguments: override.default_arguments } : {}),
277
- annotations: { ...annotations(indexed.method), ...(override.annotations || {}) },
278
- ...(override.task ? { task: override.task } : {})
279
- });
310
+ const baseOverride = overlay.operation_overrides[operationId] || {};
311
+ const configurations = [
312
+ baseOverride,
313
+ ...(overlay.operation_variants?.[operationId] || []).map((variant) => mergeOperationOverride(baseOverride, variant))
314
+ ];
315
+ for (const override of configurations) {
316
+ const contentTypes = chooseContentTypes(operationId, indexed.operation, override);
317
+ for (const contentType of contentTypes) {
318
+ const suffix = contentTypes.length > 1 && contentType !== "application/json"
319
+ ? `_${contentType.split("/").at(-1).replace(/[^A-Za-z0-9]+/g, "_")}`
320
+ : "";
321
+ const toolName = override.tool_names_by_content_type?.[contentType]
322
+ || `${override.tool_name || snakeCase(operationId)}${suffix}`;
323
+ const { schema, bindings } = buildInputSchema(operationId, indexed.pathItem, indexed.operation, contentType, override);
324
+ applyInputOverrides(schema, bindings, override);
325
+ const description = override.description || [indexed.operation.summary, indexed.operation.description]
326
+ .filter(Boolean)
327
+ .join(" ")
328
+ .replace(/\s+/g, " ")
329
+ .trim();
330
+ const defaultArguments = {
331
+ ...(override.default_arguments || {}),
332
+ ...(override.fixed_arguments || {})
333
+ };
334
+
335
+ tools.push({
336
+ name: toolName,
337
+ title: override.title || indexed.operation.summary?.replace(/\s+/g, " ").trim() || toolName,
338
+ operation_id: operationId,
339
+ method: indexed.method,
340
+ path: indexed.path,
341
+ content_type: contentType,
342
+ auth: publicAuth.has(operationId) ? "optional" : "required",
343
+ tags,
344
+ profiles,
345
+ description,
346
+ input_schema: schema,
347
+ bindings,
348
+ ...(Object.keys(defaultArguments).length > 0 ? { default_arguments: defaultArguments } : {}),
349
+ annotations: { ...annotations(indexed.method), ...(override.annotations || {}) },
350
+ ...(override.task ? { task: override.task } : {})
351
+ });
352
+ }
280
353
  }
281
354
  }
282
355