@frontmcp/adapters 0.7.1 → 0.8.0

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/esm/index.mjs CHANGED
@@ -125,9 +125,6 @@ function applyAdditionalHeaders(headers, additionalHeaders) {
125
125
  var DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
126
126
  async function parseResponse(response, options) {
127
127
  const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
128
- if (!response.ok) {
129
- throw new Error(`API request failed: ${response.status}`);
130
- }
131
128
  const contentLength = response.headers.get("content-length");
132
129
  if (contentLength) {
133
130
  const length = parseInt(contentLength, 10);
@@ -141,14 +138,29 @@ async function parseResponse(response, options) {
141
138
  throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
142
139
  }
143
140
  const contentType = response.headers.get("content-type");
141
+ let data;
144
142
  if (contentType?.toLowerCase().includes("application/json")) {
145
143
  try {
146
- return { data: JSON.parse(text) };
144
+ data = JSON.parse(text);
147
145
  } catch {
148
- return { data: text };
146
+ data = text;
149
147
  }
148
+ } else {
149
+ data = text;
150
+ }
151
+ if (!response.ok) {
152
+ return {
153
+ status: response.status,
154
+ ok: false,
155
+ data,
156
+ error: typeof data === "object" && data !== null && "message" in data ? String(data.message) : typeof data === "string" ? data : `HTTP ${response.status} error`
157
+ };
150
158
  }
151
- return { data: text };
159
+ return {
160
+ status: response.status,
161
+ ok: true,
162
+ data
163
+ };
152
164
  }
153
165
 
154
166
  // libs/adapters/src/openapi/openapi.security.ts
@@ -574,15 +586,32 @@ function createOpenApiTool(openapiTool, options, logger) {
574
586
  const metadata = openapiTool.metadata;
575
587
  const inputTransforms = metadata.adapter?.inputTransforms ?? [];
576
588
  const toolTransform = metadata.adapter?.toolTransform ?? {};
589
+ const postToolTransform = metadata.adapter?.postToolTransform;
577
590
  const frontmcpValidation = validateFrontMcpExtension(metadata.frontmcp, openapiTool.name, logger);
578
591
  const frontmcpExt = frontmcpValidation.data;
579
592
  const schemaResult = getZodSchemaFromJsonSchema(openapiTool.inputSchema, openapiTool.name, logger);
593
+ let wrappedOutputSchema;
594
+ if (openapiTool.outputSchema !== void 0) {
595
+ const baseOutputSchema = openapiTool.outputSchema ?? { type: "string" };
596
+ wrappedOutputSchema = {
597
+ type: "object",
598
+ properties: {
599
+ status: { type: "number", description: "HTTP status code" },
600
+ ok: { type: "boolean", description: "Whether the response was successful" },
601
+ data: baseOutputSchema,
602
+ error: { type: "string", description: "Error message for non-ok responses" }
603
+ },
604
+ required: ["status", "ok"]
605
+ };
606
+ }
580
607
  const toolMetadata = {
581
608
  id: openapiTool.name,
582
609
  name: openapiTool.name,
583
610
  description: openapiTool.description,
584
611
  inputSchema: schemaResult.schema.shape || {},
585
- rawInputSchema: openapiTool.inputSchema
612
+ rawInputSchema: openapiTool.inputSchema,
613
+ // Add output schema for tool/list to expose (only if not moved to description)
614
+ ...wrappedOutputSchema && { rawOutputSchema: wrappedOutputSchema }
586
615
  };
587
616
  if (schemaResult.conversionFailed) {
588
617
  toolMetadata["_schemaConversionFailed"] = true;
@@ -695,11 +724,71 @@ function createOpenApiTool(openapiTool, options, logger) {
695
724
  body: serializedBody,
696
725
  signal: controller.signal
697
726
  });
698
- return await parseResponse(response, { maxResponseSize: options.maxResponseSize });
727
+ const apiResponse = await parseResponse(response, { maxResponseSize: options.maxResponseSize });
728
+ let transformedData = apiResponse.data;
729
+ if (postToolTransform) {
730
+ const transformCtx = {
731
+ ctx,
732
+ tool: openapiTool,
733
+ status: apiResponse.status,
734
+ ok: apiResponse.ok,
735
+ adapterOptions: options
736
+ };
737
+ const shouldTransform = postToolTransform.filter ? postToolTransform.filter(transformCtx) : true;
738
+ if (shouldTransform) {
739
+ try {
740
+ transformedData = await postToolTransform.transform(apiResponse.data, transformCtx);
741
+ } catch (err) {
742
+ const errorMessage = err instanceof Error ? err.message : String(err);
743
+ const errorStack = err instanceof Error ? err.stack : void 0;
744
+ logger.error(`[${openapiTool.name}] Post-tool output transform failed`, {
745
+ error: errorMessage,
746
+ stack: errorStack,
747
+ status: apiResponse.status,
748
+ ok: apiResponse.ok
749
+ });
750
+ }
751
+ }
752
+ }
753
+ if (!apiResponse.ok) {
754
+ return {
755
+ content: [
756
+ {
757
+ type: "text",
758
+ text: JSON.stringify({
759
+ status: apiResponse.status,
760
+ error: apiResponse.error,
761
+ data: transformedData
762
+ })
763
+ }
764
+ ],
765
+ isError: true,
766
+ _meta: {
767
+ status: apiResponse.status,
768
+ errorCode: "OPENAPI_ERROR"
769
+ }
770
+ };
771
+ }
772
+ return {
773
+ status: apiResponse.status,
774
+ ok: true,
775
+ data: transformedData
776
+ };
699
777
  } catch (err) {
700
778
  if (err instanceof Error && err.name === "AbortError") {
701
- throw new Error(`Request timeout after ${requestTimeout}ms for tool '${openapiTool.name}'`);
779
+ const timeoutError = new Error(`Request timeout after ${requestTimeout}ms for tool '${openapiTool.name}'`);
780
+ logger.error(`[${openapiTool.name}] API request timeout`, {
781
+ timeout: requestTimeout,
782
+ url,
783
+ method: openapiTool.metadata.method.toUpperCase()
784
+ });
785
+ throw timeoutError;
702
786
  }
787
+ logger.error(`[${openapiTool.name}] API request failed`, {
788
+ url,
789
+ method: openapiTool.metadata.method.toUpperCase(),
790
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : err
791
+ });
703
792
  throw err;
704
793
  } finally {
705
794
  clearTimeout(timeoutId);
@@ -870,6 +959,15 @@ Add one of the following to your adapter configuration:
870
959
  if (this.options.inputTransforms) {
871
960
  transformedTools = transformedTools.map((tool2) => this.applyInputTransforms(tool2));
872
961
  }
962
+ if (this.options.schemaTransforms) {
963
+ transformedTools = transformedTools.map((tool2) => this.applySchemaTransforms(tool2));
964
+ }
965
+ const dataTransforms = this.options.dataTransforms || this.options.outputTransforms;
966
+ if (this.options.outputSchema || dataTransforms) {
967
+ transformedTools = await Promise.all(
968
+ transformedTools.map((tool2) => this.applyOutputSchemaOptions(tool2, dataTransforms))
969
+ );
970
+ }
873
971
  const tools = transformedTools.map((openapiTool) => createOpenApiTool(openapiTool, this.options, this.logger));
874
972
  return { tools };
875
973
  }
@@ -1139,6 +1237,251 @@ ${opDescription}`;
1139
1237
  }
1140
1238
  };
1141
1239
  }
1240
+ /**
1241
+ * Apply schema transforms to an OpenAPI tool.
1242
+ * Modifies input and output schema definitions.
1243
+ * @private
1244
+ */
1245
+ applySchemaTransforms(tool2) {
1246
+ const opts = this.options.schemaTransforms;
1247
+ if (!opts) return tool2;
1248
+ const ctx = {
1249
+ tool: tool2,
1250
+ adapterOptions: this.options
1251
+ };
1252
+ let newInputSchema = tool2.inputSchema;
1253
+ let newOutputSchema = tool2.outputSchema;
1254
+ if (opts.input) {
1255
+ const inputTransform = this.collectInputSchemaTransform(tool2);
1256
+ if (inputTransform) {
1257
+ newInputSchema = inputTransform(newInputSchema, ctx);
1258
+ }
1259
+ }
1260
+ if (opts.output) {
1261
+ const outputTransform = this.collectOutputSchemaTransform(tool2);
1262
+ if (outputTransform) {
1263
+ newOutputSchema = outputTransform(newOutputSchema, ctx);
1264
+ }
1265
+ }
1266
+ if (newInputSchema === tool2.inputSchema && newOutputSchema === tool2.outputSchema) {
1267
+ return tool2;
1268
+ }
1269
+ this.logger.debug(`Applied schema transforms to '${tool2.name}'`);
1270
+ return {
1271
+ ...tool2,
1272
+ inputSchema: newInputSchema,
1273
+ outputSchema: newOutputSchema
1274
+ };
1275
+ }
1276
+ /**
1277
+ * Collect input schema transform for a specific tool.
1278
+ * @private
1279
+ */
1280
+ collectInputSchemaTransform(tool2) {
1281
+ const opts = this.options.schemaTransforms?.input;
1282
+ if (!opts) return void 0;
1283
+ if (opts.generator) {
1284
+ const generated = opts.generator(tool2);
1285
+ if (generated) return generated;
1286
+ }
1287
+ if (opts.perTool?.[tool2.name]) {
1288
+ return opts.perTool[tool2.name];
1289
+ }
1290
+ return opts.global;
1291
+ }
1292
+ /**
1293
+ * Collect output schema transform for a specific tool.
1294
+ * @private
1295
+ */
1296
+ collectOutputSchemaTransform(tool2) {
1297
+ const opts = this.options.schemaTransforms?.output;
1298
+ if (!opts) return void 0;
1299
+ if (opts.generator) {
1300
+ const generated = opts.generator(tool2);
1301
+ if (generated) return generated;
1302
+ }
1303
+ if (opts.perTool?.[tool2.name]) {
1304
+ return opts.perTool[tool2.name];
1305
+ }
1306
+ return opts.global;
1307
+ }
1308
+ /**
1309
+ * Apply output schema options to an OpenAPI tool.
1310
+ * Handles output schema mode and async description formatting.
1311
+ * @private
1312
+ */
1313
+ async applyOutputSchemaOptions(tool2, dataTransforms) {
1314
+ const outputSchemaOpts = this.options.outputSchema;
1315
+ let newDescription = tool2.description;
1316
+ let newOutputSchema = tool2.outputSchema;
1317
+ const mode = outputSchemaOpts?.mode ?? "definition";
1318
+ if (newOutputSchema && (mode === "description" || mode === "both")) {
1319
+ const descriptionFormat = outputSchemaOpts?.descriptionFormat ?? "summary";
1320
+ const formatter = outputSchemaOpts?.descriptionFormatter;
1321
+ const formatterCtx = {
1322
+ tool: tool2,
1323
+ adapterOptions: this.options,
1324
+ originalDescription: tool2.description
1325
+ };
1326
+ let schemaText;
1327
+ if (formatter) {
1328
+ schemaText = await Promise.resolve(formatter(newOutputSchema, formatterCtx));
1329
+ } else {
1330
+ schemaText = this.formatSchemaForDescription(newOutputSchema, descriptionFormat);
1331
+ }
1332
+ newDescription = tool2.description + schemaText;
1333
+ }
1334
+ if (mode === "description") {
1335
+ newOutputSchema = void 0;
1336
+ }
1337
+ const preTransform = this.collectPreToolTransformsFromDataTransforms(tool2, dataTransforms);
1338
+ if (preTransform) {
1339
+ const ctx = {
1340
+ tool: tool2,
1341
+ adapterOptions: this.options
1342
+ };
1343
+ if (preTransform.transformSchema) {
1344
+ newOutputSchema = preTransform.transformSchema(newOutputSchema, ctx);
1345
+ }
1346
+ if (preTransform.transformDescription) {
1347
+ newDescription = preTransform.transformDescription(newDescription, newOutputSchema, ctx);
1348
+ }
1349
+ }
1350
+ const postTransform = this.collectPostToolTransformsFromDataTransforms(tool2, dataTransforms);
1351
+ if (newDescription === tool2.description && newOutputSchema === tool2.outputSchema && !postTransform) {
1352
+ return tool2;
1353
+ }
1354
+ this.logger.debug(`Applied output schema options to '${tool2.name}' (mode: ${mode})`);
1355
+ const metadataRecord = tool2.metadata;
1356
+ const existingAdapter = metadataRecord["adapter"];
1357
+ return {
1358
+ ...tool2,
1359
+ description: newDescription,
1360
+ outputSchema: newOutputSchema,
1361
+ metadata: {
1362
+ ...tool2.metadata,
1363
+ adapter: {
1364
+ ...existingAdapter || {},
1365
+ ...postTransform && { postToolTransform: postTransform }
1366
+ }
1367
+ }
1368
+ };
1369
+ }
1370
+ /**
1371
+ * Format schema for description based on mode.
1372
+ * @private
1373
+ */
1374
+ formatSchemaForDescription(schema, mode) {
1375
+ switch (mode) {
1376
+ case "jsonSchema":
1377
+ return `
1378
+
1379
+ ## Output Schema
1380
+ \`\`\`json
1381
+ ${JSON.stringify(schema, null, 2)}
1382
+ \`\`\``;
1383
+ case "summary":
1384
+ return `
1385
+
1386
+ ## Returns
1387
+ ${this.formatSchemaAsSummary(schema)}`;
1388
+ default:
1389
+ return `
1390
+
1391
+ ## Returns
1392
+ ${this.formatSchemaAsSummary(schema)}`;
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Collect pre-tool transforms from dataTransforms options.
1397
+ * @private
1398
+ */
1399
+ collectPreToolTransformsFromDataTransforms(tool2, dataTransforms) {
1400
+ const opts = dataTransforms?.preToolTransforms;
1401
+ if (!opts) return void 0;
1402
+ let result;
1403
+ if (opts.global) {
1404
+ result = { ...opts.global };
1405
+ }
1406
+ if (opts.perTool?.[tool2.name]) {
1407
+ result = { ...result, ...opts.perTool[tool2.name] };
1408
+ }
1409
+ if (opts.generator) {
1410
+ const generated = opts.generator(tool2);
1411
+ if (generated) {
1412
+ result = { ...result, ...generated };
1413
+ }
1414
+ }
1415
+ return result;
1416
+ }
1417
+ /**
1418
+ * Collect post-tool transforms from dataTransforms options.
1419
+ * @private
1420
+ */
1421
+ collectPostToolTransformsFromDataTransforms(tool2, dataTransforms) {
1422
+ const opts = dataTransforms?.postToolTransforms;
1423
+ if (!opts) return void 0;
1424
+ let result;
1425
+ if (opts.global) {
1426
+ result = { ...opts.global };
1427
+ }
1428
+ if (opts.perTool?.[tool2.name]) {
1429
+ const perTool = opts.perTool[tool2.name];
1430
+ result = result ? {
1431
+ transform: perTool.transform,
1432
+ filter: perTool.filter ?? result.filter
1433
+ } : perTool;
1434
+ }
1435
+ if (opts.generator) {
1436
+ const generated = opts.generator(tool2);
1437
+ if (generated) {
1438
+ result = result ? {
1439
+ transform: generated.transform,
1440
+ filter: generated.filter ?? result.filter
1441
+ } : generated;
1442
+ }
1443
+ }
1444
+ return result;
1445
+ }
1446
+ /**
1447
+ * Format JSON Schema as human-readable summary.
1448
+ * @private
1449
+ */
1450
+ formatSchemaAsSummary(schema) {
1451
+ const lines = [];
1452
+ if (schema.type === "object" && schema.properties) {
1453
+ const required = new Set(schema.required || []);
1454
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
1455
+ const isRequired = required.has(name);
1456
+ const typeStr = this.getSchemaTypeString(propSchema);
1457
+ const desc = propSchema.description ? ` - ${propSchema.description}` : "";
1458
+ const reqStr = isRequired ? " (required)" : " (optional)";
1459
+ lines.push(`- **${name}**: ${typeStr}${reqStr}${desc}`);
1460
+ }
1461
+ } else if (schema.type === "array" && schema.items) {
1462
+ const itemType = this.getSchemaTypeString(schema.items);
1463
+ lines.push(`Array of ${itemType}`);
1464
+ } else {
1465
+ lines.push(this.getSchemaTypeString(schema));
1466
+ }
1467
+ return lines.join("\n");
1468
+ }
1469
+ /**
1470
+ * Get human-readable type string from JSON Schema.
1471
+ * @private
1472
+ */
1473
+ getSchemaTypeString(schema) {
1474
+ if (schema.type === "array") {
1475
+ return schema.items ? `${this.getSchemaTypeString(schema.items)}[]` : "array";
1476
+ }
1477
+ if (schema.type === "object") {
1478
+ return schema.title || "object";
1479
+ }
1480
+ if (Array.isArray(schema.type)) {
1481
+ return schema.type.join(" | ");
1482
+ }
1483
+ return schema.type || "any";
1484
+ }
1142
1485
  };
1143
1486
  OpenapiAdapter = __decorateClass([
1144
1487
  Adapter({
@@ -1149,7 +1492,110 @@ OpenapiAdapter = __decorateClass([
1149
1492
 
1150
1493
  // libs/adapters/src/openapi/openapi.types.ts
1151
1494
  var FRONTMCP_EXTENSION_KEY = "x-frontmcp";
1495
+
1496
+ // libs/adapters/src/openapi/openapi.spec-utils.ts
1497
+ function deepClone(obj) {
1498
+ return JSON.parse(JSON.stringify(obj));
1499
+ }
1500
+ function forceJwtSecurity(spec, options = {}) {
1501
+ const result = deepClone(spec);
1502
+ const {
1503
+ schemeName = "BearerAuth",
1504
+ schemeType = "bearer",
1505
+ apiKeyIn = "header",
1506
+ apiKeyName = "X-API-Key",
1507
+ operations,
1508
+ description
1509
+ } = options;
1510
+ if (!result.components) {
1511
+ result.components = {};
1512
+ }
1513
+ if (!result.components.securitySchemes) {
1514
+ result.components.securitySchemes = {};
1515
+ }
1516
+ let securityScheme;
1517
+ switch (schemeType) {
1518
+ case "bearer":
1519
+ securityScheme = {
1520
+ type: "http",
1521
+ scheme: "bearer",
1522
+ bearerFormat: "JWT",
1523
+ description: description ?? "JWT Bearer token authentication"
1524
+ };
1525
+ break;
1526
+ case "apiKey":
1527
+ securityScheme = {
1528
+ type: "apiKey",
1529
+ in: apiKeyIn,
1530
+ name: apiKeyName,
1531
+ description: description ?? `API Key authentication via ${apiKeyIn}`
1532
+ };
1533
+ break;
1534
+ case "basic":
1535
+ securityScheme = {
1536
+ type: "http",
1537
+ scheme: "basic",
1538
+ description: description ?? "HTTP Basic authentication"
1539
+ };
1540
+ break;
1541
+ default:
1542
+ throw new Error(`Unsupported scheme type: ${schemeType}`);
1543
+ }
1544
+ result.components.securitySchemes[schemeName] = securityScheme;
1545
+ const securityRequirement = {
1546
+ [schemeName]: []
1547
+ };
1548
+ if (!result.paths) {
1549
+ return result;
1550
+ }
1551
+ const operationSet = operations ? new Set(operations) : null;
1552
+ for (const [, pathItem] of Object.entries(result.paths)) {
1553
+ if (!pathItem) continue;
1554
+ const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1555
+ for (const method of methods) {
1556
+ const operation = pathItem[method];
1557
+ if (!operation) continue;
1558
+ if (operationSet) {
1559
+ if (!operation.operationId || !operationSet.has(operation.operationId)) {
1560
+ continue;
1561
+ }
1562
+ }
1563
+ if (!operation.security) {
1564
+ operation.security = [];
1565
+ }
1566
+ const hasSecurityRequirement = operation.security.some((req) => Object.keys(req).includes(schemeName));
1567
+ if (!hasSecurityRequirement) {
1568
+ operation.security.push(securityRequirement);
1569
+ }
1570
+ }
1571
+ }
1572
+ return result;
1573
+ }
1574
+ function removeSecurityFromOperations(spec, operations) {
1575
+ const result = deepClone(spec);
1576
+ if (!result.paths) {
1577
+ return result;
1578
+ }
1579
+ const operationSet = operations ? new Set(operations) : null;
1580
+ for (const [, pathItem] of Object.entries(result.paths)) {
1581
+ if (!pathItem) continue;
1582
+ const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1583
+ for (const method of methods) {
1584
+ const operation = pathItem[method];
1585
+ if (!operation) continue;
1586
+ if (operationSet) {
1587
+ if (!operation.operationId || !operationSet.has(operation.operationId)) {
1588
+ continue;
1589
+ }
1590
+ }
1591
+ operation.security = [];
1592
+ }
1593
+ }
1594
+ return result;
1595
+ }
1152
1596
  export {
1153
1597
  FRONTMCP_EXTENSION_KEY,
1154
- OpenapiAdapter
1598
+ OpenapiAdapter,
1599
+ forceJwtSecurity,
1600
+ removeSecurityFromOperations
1155
1601
  };