@frontmcp/adapters 1.0.4 → 1.1.0-beta.1

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/openapi/index.js CHANGED
@@ -42,156 +42,248 @@ var import_sdk4 = require("@frontmcp/sdk");
42
42
  var import_mcp_from_openapi2 = require("mcp-from-openapi");
43
43
 
44
44
  // libs/adapters/src/openapi/openapi.tool.ts
45
- var import_zod2 = require("zod");
46
- var import_sdk2 = require("@frontmcp/sdk");
47
45
  var import_zod_from_json_schema = require("zod-from-json-schema");
46
+ var import_lazy_zod2 = require("@frontmcp/lazy-zod");
47
+ var import_sdk2 = require("@frontmcp/sdk");
48
48
 
49
- // libs/adapters/src/openapi/openapi.utils.ts
50
- var import_utils = require("@frontmcp/utils");
51
- var import_sdk = require("@frontmcp/sdk");
52
- function coerceToString(value, paramName, location) {
53
- if (value === null || value === void 0) {
54
- return "";
49
+ // libs/adapters/src/openapi/openapi.frontmcp-schema.ts
50
+ var import_lazy_zod = require("@frontmcp/lazy-zod");
51
+ var FRONTMCP_SCHEMA_VERSION = "1.0";
52
+ var SUPPORTED_VERSIONS = ["1.0"];
53
+ var FrontMcpAnnotationsSchema = import_lazy_zod.z.object({
54
+ /** Human-readable title for the tool */
55
+ title: import_lazy_zod.z.string().optional(),
56
+ /** If true, the tool does not modify its environment */
57
+ readOnlyHint: import_lazy_zod.z.boolean().optional(),
58
+ /** If true, the tool may perform destructive updates */
59
+ destructiveHint: import_lazy_zod.z.boolean().optional(),
60
+ /** If true, calling repeatedly with same args has no additional effect */
61
+ idempotentHint: import_lazy_zod.z.boolean().optional(),
62
+ /** If true, tool may interact with external entities */
63
+ openWorldHint: import_lazy_zod.z.boolean().optional()
64
+ });
65
+ var FrontMcpCacheSchema = import_lazy_zod.z.object({
66
+ /** Time-to-live in seconds for cached responses */
67
+ ttl: import_lazy_zod.z.number().int().positive().optional(),
68
+ /** If true, cache window slides on each access */
69
+ slideWindow: import_lazy_zod.z.boolean().optional()
70
+ });
71
+ var FrontMcpCodeCallSchema = import_lazy_zod.z.object({
72
+ /** Whether this tool can be used via CodeCall */
73
+ enabledInCodeCall: import_lazy_zod.z.boolean().optional(),
74
+ /** If true, stays visible in list_tools when CodeCall is active */
75
+ visibleInListTools: import_lazy_zod.z.boolean().optional()
76
+ });
77
+ var FrontMcpExampleSchema = import_lazy_zod.z.object({
78
+ /** Description of the example */
79
+ description: import_lazy_zod.z.string(),
80
+ /** Example input values */
81
+ input: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.any()),
82
+ /** Expected output (optional) */
83
+ output: import_lazy_zod.z.any().optional()
84
+ });
85
+ var KNOWN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
86
+ "version",
87
+ "annotations",
88
+ "cache",
89
+ "codecall",
90
+ "tags",
91
+ "hideFromDiscovery",
92
+ "examples"
93
+ ]);
94
+ var KNOWN_ANNOTATION_FIELDS = /* @__PURE__ */ new Set([
95
+ "title",
96
+ "readOnlyHint",
97
+ "destructiveHint",
98
+ "idempotentHint",
99
+ "openWorldHint"
100
+ ]);
101
+ var KNOWN_CACHE_FIELDS = /* @__PURE__ */ new Set(["ttl", "slideWindow"]);
102
+ var KNOWN_CODECALL_FIELDS = /* @__PURE__ */ new Set(["enabledInCodeCall", "visibleInListTools"]);
103
+ function validateFrontMcpExtension(rawData, toolName, logger) {
104
+ const warnings = [];
105
+ if (rawData === null || rawData === void 0) {
106
+ return { success: true, data: null, warnings: [] };
55
107
  }
56
- if (typeof value === "object") {
57
- if (Array.isArray(value)) {
58
- if (location === "query") {
59
- return value.map(String).join(",");
60
- }
61
- throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);
62
- }
63
- throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);
108
+ if (typeof rawData !== "object" || Array.isArray(rawData)) {
109
+ warnings.push(`x-frontmcp must be an object, got ${Array.isArray(rawData) ? "array" : typeof rawData}`);
110
+ logger.warn(`[${toolName}] Invalid x-frontmcp extension: ${warnings[0]}`);
111
+ return { success: false, data: null, warnings };
64
112
  }
65
- return String(value);
66
- }
67
- function appendCookie(headers, name, value) {
68
- if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
69
- throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);
113
+ const data = rawData;
114
+ const version = typeof data["version"] === "string" ? data["version"] : FRONTMCP_SCHEMA_VERSION;
115
+ if (!SUPPORTED_VERSIONS.includes(version)) {
116
+ warnings.push(`Unsupported x-frontmcp version '${version}'. Supported versions: ${SUPPORTED_VERSIONS.join(", ")}`);
117
+ logger.warn(`[${toolName}] ${warnings[0]}`);
118
+ return { success: false, data: null, warnings };
70
119
  }
71
- const existing = headers.get("Cookie") || "";
72
- const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, "cookie"))}`;
73
- const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
74
- headers.set("Cookie", combined);
120
+ const validData = extractValidFields(data, version, warnings);
121
+ if (warnings.length > 0) {
122
+ logger.warn(`[${toolName}] x-frontmcp extension has invalid fields that were ignored:`);
123
+ warnings.forEach((w) => logger.warn(` - ${w}`));
124
+ }
125
+ return {
126
+ success: true,
127
+ data: validData,
128
+ warnings
129
+ };
75
130
  }
76
- function buildRequest(tool2, input, security, baseUrl) {
77
- const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
78
- (0, import_utils.validateBaseUrl)(rawBaseUrl);
79
- const apiBaseUrl = rawBaseUrl.replace(/\/+$/, "");
80
- let path = tool2.metadata.path;
81
- const queryParams = new URLSearchParams();
82
- const headers = new Headers({
83
- accept: "application/json",
84
- ...security.headers
85
- });
86
- let body;
87
- for (const mapper of tool2.mapper) {
88
- if (mapper.security) continue;
89
- const value = input[mapper.inputKey];
90
- if (value === void 0 || value === null) {
91
- if (mapper.required) {
92
- throw new Error(
93
- `Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool2.name}'`
94
- );
95
- }
96
- continue;
131
+ function extractValidFields(data, version, warnings) {
132
+ const result = {
133
+ version
134
+ };
135
+ for (const key of Object.keys(data)) {
136
+ if (!KNOWN_EXTENSION_FIELDS.has(key)) {
137
+ warnings.push(`Unknown field '${key}' in x-frontmcp (will be ignored)`);
97
138
  }
98
- switch (mapper.type) {
99
- case "path":
100
- path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, "path")));
101
- break;
102
- case "query":
103
- queryParams.set(mapper.key, coerceToString(value, mapper.key, "query"));
104
- break;
105
- case "header": {
106
- const headerValue = coerceToString(value, mapper.key, "header");
107
- if (/[\r\n\x00\f\v]/.test(headerValue)) {
108
- throw new import_sdk.PublicMcpError(
109
- `Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
110
- "INVALID_HEADER_VALUE",
111
- 400
112
- );
113
- }
114
- headers.set(mapper.key, headerValue);
115
- break;
116
- }
117
- case "cookie":
118
- appendCookie(headers, mapper.key, value);
119
- break;
120
- case "body":
121
- if (!body) body = {};
122
- body[mapper.key] = value;
123
- break;
124
- default:
125
- throw new Error(
126
- `Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool2.name}'`
127
- );
139
+ }
140
+ if (data["annotations"] !== void 0) {
141
+ const annotationsResult = FrontMcpAnnotationsSchema.safeParse(data["annotations"]);
142
+ if (annotationsResult.success) {
143
+ result.annotations = annotationsResult.data;
144
+ } else {
145
+ const issues = formatZodIssues(annotationsResult.error.issues, "annotations");
146
+ warnings.push(...issues);
147
+ result.annotations = extractValidAnnotations(data["annotations"], warnings);
128
148
  }
129
149
  }
130
- Object.entries(security.query).forEach(([key, value]) => {
131
- if (queryParams.has(key)) {
132
- throw new Error(
133
- `Query parameter collision: '${key}' is provided both as user input and security parameter. This could indicate a security misconfiguration in operation '${tool2.name}'.`
134
- );
150
+ if (data["cache"] !== void 0) {
151
+ const cacheResult = FrontMcpCacheSchema.safeParse(data["cache"]);
152
+ if (cacheResult.success) {
153
+ result.cache = cacheResult.data;
154
+ } else {
155
+ const issues = formatZodIssues(cacheResult.error.issues, "cache");
156
+ warnings.push(...issues);
157
+ result.cache = extractValidCache(data["cache"], warnings);
135
158
  }
136
- queryParams.set(key, coerceToString(value, key, "security query"));
137
- });
138
- if (security.cookies && Object.keys(security.cookies).length > 0) {
139
- Object.entries(security.cookies).forEach(([key, value]) => {
140
- appendCookie(headers, key, value);
141
- });
142
159
  }
143
- if (path.includes("{")) {
144
- throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool2.name}`);
160
+ if (data["codecall"] !== void 0) {
161
+ const codecallResult = FrontMcpCodeCallSchema.safeParse(data["codecall"]);
162
+ if (codecallResult.success) {
163
+ result.codecall = codecallResult.data;
164
+ } else {
165
+ const issues = formatZodIssues(codecallResult.error.issues, "codecall");
166
+ warnings.push(...issues);
167
+ result.codecall = extractValidCodeCall(data["codecall"], warnings);
168
+ }
145
169
  }
146
- const queryString = queryParams.toString();
147
- const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ""}`;
148
- return { url, headers, body };
170
+ if (data["tags"] !== void 0) {
171
+ const tagsSchema = import_lazy_zod.z.array(import_lazy_zod.z.string());
172
+ const tagsResult = tagsSchema.safeParse(data["tags"]);
173
+ if (tagsResult.success) {
174
+ result.tags = tagsResult.data;
175
+ } else {
176
+ warnings.push(`Invalid 'tags': expected array of strings`);
177
+ result.tags = extractValidTags(data["tags"]);
178
+ }
179
+ }
180
+ if (data["hideFromDiscovery"] !== void 0) {
181
+ if (typeof data["hideFromDiscovery"] === "boolean") {
182
+ result.hideFromDiscovery = data["hideFromDiscovery"];
183
+ } else {
184
+ warnings.push(`Invalid 'hideFromDiscovery': expected boolean, got ${typeof data["hideFromDiscovery"]}`);
185
+ }
186
+ }
187
+ if (data["examples"] !== void 0) {
188
+ const examplesSchema = import_lazy_zod.z.array(FrontMcpExampleSchema);
189
+ const examplesResult = examplesSchema.safeParse(data["examples"]);
190
+ if (examplesResult.success) {
191
+ result.examples = examplesResult.data;
192
+ } else {
193
+ warnings.push(`Invalid 'examples': some examples have invalid format`);
194
+ result.examples = extractValidExamples(data["examples"], warnings);
195
+ }
196
+ }
197
+ return result;
149
198
  }
150
- function applyAdditionalHeaders(headers, additionalHeaders) {
151
- if (!additionalHeaders) return;
152
- Object.entries(additionalHeaders).forEach(([key, value]) => {
153
- headers.set(key, value);
154
- });
199
+ function extractValidAnnotations(data, warnings) {
200
+ if (typeof data !== "object" || data === null) return void 0;
201
+ const obj = data;
202
+ const result = {};
203
+ for (const field of KNOWN_ANNOTATION_FIELDS) {
204
+ if (obj[field] !== void 0) {
205
+ if (field === "title" && typeof obj[field] === "string") {
206
+ result.title = obj[field];
207
+ } else if (field !== "title" && typeof obj[field] === "boolean") {
208
+ result[field] = obj[field];
209
+ }
210
+ }
211
+ }
212
+ for (const key of Object.keys(obj)) {
213
+ if (!KNOWN_ANNOTATION_FIELDS.has(key)) {
214
+ warnings.push(`Unknown field 'annotations.${key}' (will be ignored)`);
215
+ }
216
+ }
217
+ return Object.keys(result).length > 0 ? result : void 0;
155
218
  }
156
- var DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
157
- async function parseResponse(response, options) {
158
- const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
159
- const contentLength = response.headers.get("content-length");
160
- if (contentLength) {
161
- const length = parseInt(contentLength, 10);
162
- if (!isNaN(length) && isFinite(length) && length > maxSize) {
163
- throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);
219
+ function extractValidCache(data, warnings) {
220
+ if (typeof data !== "object" || data === null) return void 0;
221
+ const obj = data;
222
+ const result = {};
223
+ if (typeof obj["ttl"] === "number" && Number.isInteger(obj["ttl"]) && obj["ttl"] > 0) {
224
+ result.ttl = obj["ttl"];
225
+ } else if (obj["ttl"] !== void 0) {
226
+ warnings.push(`Invalid 'cache.ttl': expected positive integer`);
227
+ }
228
+ if (typeof obj["slideWindow"] === "boolean") {
229
+ result.slideWindow = obj["slideWindow"];
230
+ } else if (obj["slideWindow"] !== void 0) {
231
+ warnings.push(`Invalid 'cache.slideWindow': expected boolean`);
232
+ }
233
+ for (const key of Object.keys(obj)) {
234
+ if (!KNOWN_CACHE_FIELDS.has(key)) {
235
+ warnings.push(`Unknown field 'cache.${key}' (will be ignored)`);
164
236
  }
165
237
  }
166
- const text = await response.text();
167
- const byteSize = new TextEncoder().encode(text).length;
168
- if (byteSize > maxSize) {
169
- throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
238
+ return Object.keys(result).length > 0 ? result : void 0;
239
+ }
240
+ function extractValidCodeCall(data, warnings) {
241
+ if (typeof data !== "object" || data === null) return void 0;
242
+ const obj = data;
243
+ const result = {};
244
+ if (typeof obj["enabledInCodeCall"] === "boolean") {
245
+ result.enabledInCodeCall = obj["enabledInCodeCall"];
246
+ } else if (obj["enabledInCodeCall"] !== void 0) {
247
+ warnings.push(`Invalid 'codecall.enabledInCodeCall': expected boolean`);
170
248
  }
171
- const contentType = response.headers.get("content-type");
172
- let data;
173
- if (contentType?.toLowerCase().includes("application/json")) {
174
- try {
175
- data = JSON.parse(text);
176
- } catch {
177
- data = text;
249
+ if (typeof obj["visibleInListTools"] === "boolean") {
250
+ result.visibleInListTools = obj["visibleInListTools"];
251
+ } else if (obj["visibleInListTools"] !== void 0) {
252
+ warnings.push(`Invalid 'codecall.visibleInListTools': expected boolean`);
253
+ }
254
+ for (const key of Object.keys(obj)) {
255
+ if (!KNOWN_CODECALL_FIELDS.has(key)) {
256
+ warnings.push(`Unknown field 'codecall.${key}' (will be ignored)`);
178
257
  }
179
- } else {
180
- data = text;
181
258
  }
182
- if (!response.ok) {
183
- return {
184
- status: response.status,
185
- ok: false,
186
- data,
187
- error: typeof data === "object" && data !== null && "message" in data ? String(data.message) : typeof data === "string" ? data : `HTTP ${response.status} error`
188
- };
259
+ return Object.keys(result).length > 0 ? result : void 0;
260
+ }
261
+ function extractValidTags(data) {
262
+ if (!Array.isArray(data)) return void 0;
263
+ const validTags = data.filter((item) => typeof item === "string");
264
+ return validTags.length > 0 ? validTags : void 0;
265
+ }
266
+ function extractValidExamples(data, warnings) {
267
+ if (!Array.isArray(data)) return void 0;
268
+ const validExamples = [];
269
+ for (let i = 0; i < data.length; i++) {
270
+ const item = data[i];
271
+ const result = FrontMcpExampleSchema.safeParse(item);
272
+ if (result.success) {
273
+ validExamples.push(result.data);
274
+ } else {
275
+ warnings.push(
276
+ `Invalid example at index ${i}: ${formatZodIssues(result.error.issues, `examples[${i}]`).join(", ")}`
277
+ );
278
+ }
189
279
  }
190
- return {
191
- status: response.status,
192
- ok: true,
193
- data
194
- };
280
+ return validExamples.length > 0 ? validExamples : void 0;
281
+ }
282
+ function formatZodIssues(issues, prefix) {
283
+ return issues.map((issue) => {
284
+ const path = issue.path.length > 0 ? `${prefix}.${issue.path.join(".")}` : prefix;
285
+ return `Invalid '${path}': ${issue.message}`;
286
+ });
195
287
  }
196
288
 
197
289
  // libs/adapters/src/openapi/openapi.security.ts
@@ -316,301 +408,209 @@ function validateSecurityConfiguration(tools, options) {
316
408
  if (options.authProviderMapper || schemesInInput.size > 0) {
317
409
  result.securityRiskScore = schemesInInput.size > 0 ? "medium" : "low";
318
410
  if (schemesInInput.size > 0) {
319
- result.warnings.push(
320
- `INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
321
- );
322
- }
323
- for (const scheme of securitySchemes) {
324
- if (schemesInInput.has(scheme)) {
325
- continue;
326
- }
327
- if (!options.authProviderMapper?.[scheme]) {
328
- result.valid = false;
329
- result.missingMappings.push(scheme);
330
- }
331
- }
332
- if (!result.valid) {
333
- result.warnings.push(
334
- `ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
335
- );
336
- }
337
- return result;
338
- }
339
- if (securitySchemes.size > 0) {
340
- result.securityRiskScore = "medium";
341
- result.warnings.push(
342
- `INFO: No auth configuration provided. Using default ctx.authInfo.token for all security schemes: ${Array.from(
343
- securitySchemes
344
- ).join(", ")}`
345
- );
346
- result.warnings.push(
347
- "RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
348
- );
349
- }
350
- return result;
351
- }
352
- async function resolveToolSecurity(tool2, ctx, options) {
353
- const securityResolver = new import_mcp_from_openapi.SecurityResolver();
354
- const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
355
- const hasAuth = securityContext.jwt || securityContext.apiKey || securityContext.basic || securityContext.oauth2Token || securityContext.apiKeys && Object.keys(securityContext.apiKeys).length > 0 || securityContext.customHeaders && Object.keys(securityContext.customHeaders).length > 0;
356
- const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
357
- if (requiresSecurity && !hasAuth) {
358
- const requiredSchemes = tool2.mapper.filter((m) => m.security && m.required === true).map((m) => m.security?.scheme ?? "unknown");
359
- const uniqueSchemes = [...new Set(requiredSchemes)];
360
- const schemesStr = uniqueSchemes.join(", ") || "unknown";
361
- const firstScheme = uniqueSchemes[0] || "BearerAuth";
362
- throw new Error(
363
- `Authentication required for tool '${tool2.name}' but no auth configuration found.
364
- Required security schemes: ${schemesStr}
365
- Solutions:
366
- 1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
367
- 2. Add securityResolver: (tool, ctx) => ({ jwt: ctx.authInfo.token })
368
- 3. Add staticAuth: { jwt: process.env.API_TOKEN }
369
- 4. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
370
- );
371
- }
372
- return await securityResolver.resolve(tool2.mapper, securityContext);
373
- }
374
-
375
- // libs/adapters/src/openapi/openapi.frontmcp-schema.ts
376
- var import_zod = require("zod");
377
- var FRONTMCP_SCHEMA_VERSION = "1.0";
378
- var SUPPORTED_VERSIONS = ["1.0"];
379
- var FrontMcpAnnotationsSchema = import_zod.z.object({
380
- /** Human-readable title for the tool */
381
- title: import_zod.z.string().optional(),
382
- /** If true, the tool does not modify its environment */
383
- readOnlyHint: import_zod.z.boolean().optional(),
384
- /** If true, the tool may perform destructive updates */
385
- destructiveHint: import_zod.z.boolean().optional(),
386
- /** If true, calling repeatedly with same args has no additional effect */
387
- idempotentHint: import_zod.z.boolean().optional(),
388
- /** If true, tool may interact with external entities */
389
- openWorldHint: import_zod.z.boolean().optional()
390
- });
391
- var FrontMcpCacheSchema = import_zod.z.object({
392
- /** Time-to-live in seconds for cached responses */
393
- ttl: import_zod.z.number().int().positive().optional(),
394
- /** If true, cache window slides on each access */
395
- slideWindow: import_zod.z.boolean().optional()
396
- });
397
- var FrontMcpCodeCallSchema = import_zod.z.object({
398
- /** Whether this tool can be used via CodeCall */
399
- enabledInCodeCall: import_zod.z.boolean().optional(),
400
- /** If true, stays visible in list_tools when CodeCall is active */
401
- visibleInListTools: import_zod.z.boolean().optional()
402
- });
403
- var FrontMcpExampleSchema = import_zod.z.object({
404
- /** Description of the example */
405
- description: import_zod.z.string(),
406
- /** Example input values */
407
- input: import_zod.z.record(import_zod.z.string(), import_zod.z.any()),
408
- /** Expected output (optional) */
409
- output: import_zod.z.any().optional()
410
- });
411
- var KNOWN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
412
- "version",
413
- "annotations",
414
- "cache",
415
- "codecall",
416
- "tags",
417
- "hideFromDiscovery",
418
- "examples"
419
- ]);
420
- var KNOWN_ANNOTATION_FIELDS = /* @__PURE__ */ new Set([
421
- "title",
422
- "readOnlyHint",
423
- "destructiveHint",
424
- "idempotentHint",
425
- "openWorldHint"
426
- ]);
427
- var KNOWN_CACHE_FIELDS = /* @__PURE__ */ new Set(["ttl", "slideWindow"]);
428
- var KNOWN_CODECALL_FIELDS = /* @__PURE__ */ new Set(["enabledInCodeCall", "visibleInListTools"]);
429
- function validateFrontMcpExtension(rawData, toolName, logger) {
430
- const warnings = [];
431
- if (rawData === null || rawData === void 0) {
432
- return { success: true, data: null, warnings: [] };
433
- }
434
- if (typeof rawData !== "object" || Array.isArray(rawData)) {
435
- warnings.push(`x-frontmcp must be an object, got ${Array.isArray(rawData) ? "array" : typeof rawData}`);
436
- logger.warn(`[${toolName}] Invalid x-frontmcp extension: ${warnings[0]}`);
437
- return { success: false, data: null, warnings };
438
- }
439
- const data = rawData;
440
- const version = typeof data["version"] === "string" ? data["version"] : FRONTMCP_SCHEMA_VERSION;
441
- if (!SUPPORTED_VERSIONS.includes(version)) {
442
- warnings.push(`Unsupported x-frontmcp version '${version}'. Supported versions: ${SUPPORTED_VERSIONS.join(", ")}`);
443
- logger.warn(`[${toolName}] ${warnings[0]}`);
444
- return { success: false, data: null, warnings };
445
- }
446
- const validData = extractValidFields(data, version, warnings);
447
- if (warnings.length > 0) {
448
- logger.warn(`[${toolName}] x-frontmcp extension has invalid fields that were ignored:`);
449
- warnings.forEach((w) => logger.warn(` - ${w}`));
450
- }
451
- return {
452
- success: true,
453
- data: validData,
454
- warnings
455
- };
456
- }
457
- function extractValidFields(data, version, warnings) {
458
- const result = {
459
- version
460
- };
461
- for (const key of Object.keys(data)) {
462
- if (!KNOWN_EXTENSION_FIELDS.has(key)) {
463
- warnings.push(`Unknown field '${key}' in x-frontmcp (will be ignored)`);
464
- }
465
- }
466
- if (data["annotations"] !== void 0) {
467
- const annotationsResult = FrontMcpAnnotationsSchema.safeParse(data["annotations"]);
468
- if (annotationsResult.success) {
469
- result.annotations = annotationsResult.data;
470
- } else {
471
- const issues = formatZodIssues(annotationsResult.error.issues, "annotations");
472
- warnings.push(...issues);
473
- result.annotations = extractValidAnnotations(data["annotations"], warnings);
474
- }
475
- }
476
- if (data["cache"] !== void 0) {
477
- const cacheResult = FrontMcpCacheSchema.safeParse(data["cache"]);
478
- if (cacheResult.success) {
479
- result.cache = cacheResult.data;
480
- } else {
481
- const issues = formatZodIssues(cacheResult.error.issues, "cache");
482
- warnings.push(...issues);
483
- result.cache = extractValidCache(data["cache"], warnings);
484
- }
485
- }
486
- if (data["codecall"] !== void 0) {
487
- const codecallResult = FrontMcpCodeCallSchema.safeParse(data["codecall"]);
488
- if (codecallResult.success) {
489
- result.codecall = codecallResult.data;
490
- } else {
491
- const issues = formatZodIssues(codecallResult.error.issues, "codecall");
492
- warnings.push(...issues);
493
- result.codecall = extractValidCodeCall(data["codecall"], warnings);
494
- }
495
- }
496
- if (data["tags"] !== void 0) {
497
- const tagsSchema = import_zod.z.array(import_zod.z.string());
498
- const tagsResult = tagsSchema.safeParse(data["tags"]);
499
- if (tagsResult.success) {
500
- result.tags = tagsResult.data;
501
- } else {
502
- warnings.push(`Invalid 'tags': expected array of strings`);
503
- result.tags = extractValidTags(data["tags"]);
504
- }
505
- }
506
- if (data["hideFromDiscovery"] !== void 0) {
507
- if (typeof data["hideFromDiscovery"] === "boolean") {
508
- result.hideFromDiscovery = data["hideFromDiscovery"];
509
- } else {
510
- warnings.push(`Invalid 'hideFromDiscovery': expected boolean, got ${typeof data["hideFromDiscovery"]}`);
511
- }
512
- }
513
- if (data["examples"] !== void 0) {
514
- const examplesSchema = import_zod.z.array(FrontMcpExampleSchema);
515
- const examplesResult = examplesSchema.safeParse(data["examples"]);
516
- if (examplesResult.success) {
517
- result.examples = examplesResult.data;
518
- } else {
519
- warnings.push(`Invalid 'examples': some examples have invalid format`);
520
- result.examples = extractValidExamples(data["examples"], warnings);
411
+ result.warnings.push(
412
+ `INFO: Per-scheme security control enabled. Schemes in input: ${Array.from(schemesInInput).join(", ")}`
413
+ );
521
414
  }
522
- }
523
- return result;
524
- }
525
- function extractValidAnnotations(data, warnings) {
526
- if (typeof data !== "object" || data === null) return void 0;
527
- const obj = data;
528
- const result = {};
529
- for (const field of KNOWN_ANNOTATION_FIELDS) {
530
- if (obj[field] !== void 0) {
531
- if (field === "title" && typeof obj[field] === "string") {
532
- result.title = obj[field];
533
- } else if (field !== "title" && typeof obj[field] === "boolean") {
534
- result[field] = obj[field];
415
+ for (const scheme of securitySchemes) {
416
+ if (schemesInInput.has(scheme)) {
417
+ continue;
418
+ }
419
+ if (!options.authProviderMapper?.[scheme]) {
420
+ result.valid = false;
421
+ result.missingMappings.push(scheme);
535
422
  }
536
423
  }
537
- }
538
- for (const key of Object.keys(obj)) {
539
- if (!KNOWN_ANNOTATION_FIELDS.has(key)) {
540
- warnings.push(`Unknown field 'annotations.${key}' (will be ignored)`);
424
+ if (!result.valid) {
425
+ result.warnings.push(
426
+ `ERROR: Missing auth provider mappings for security schemes: ${result.missingMappings.join(", ")}`
427
+ );
541
428
  }
429
+ return result;
542
430
  }
543
- return Object.keys(result).length > 0 ? result : void 0;
431
+ if (securitySchemes.size > 0) {
432
+ result.securityRiskScore = "medium";
433
+ result.warnings.push(
434
+ `INFO: No auth configuration provided. Using default ctx.authInfo.token for all security schemes: ${Array.from(
435
+ securitySchemes
436
+ ).join(", ")}`
437
+ );
438
+ result.warnings.push(
439
+ "RECOMMENDATION: For multiple auth providers, use authProviderMapper or securityResolver to map each security scheme to the correct auth provider."
440
+ );
441
+ }
442
+ return result;
544
443
  }
545
- function extractValidCache(data, warnings) {
546
- if (typeof data !== "object" || data === null) return void 0;
547
- const obj = data;
548
- const result = {};
549
- if (typeof obj["ttl"] === "number" && Number.isInteger(obj["ttl"]) && obj["ttl"] > 0) {
550
- result.ttl = obj["ttl"];
551
- } else if (obj["ttl"] !== void 0) {
552
- warnings.push(`Invalid 'cache.ttl': expected positive integer`);
444
+ async function resolveToolSecurity(tool2, ctx, options) {
445
+ const securityResolver = new import_mcp_from_openapi.SecurityResolver();
446
+ const securityContext = await createSecurityContextFromAuth(tool2, ctx, options);
447
+ const hasAuth = securityContext.jwt || securityContext.apiKey || securityContext.basic || securityContext.oauth2Token || securityContext.apiKeys && Object.keys(securityContext.apiKeys).length > 0 || securityContext.customHeaders && Object.keys(securityContext.customHeaders).length > 0;
448
+ const requiresSecurity = tool2.mapper.some((m) => m.security && m.required === true);
449
+ if (requiresSecurity && !hasAuth) {
450
+ const requiredSchemes = tool2.mapper.filter((m) => m.security && m.required === true).map((m) => m.security?.scheme ?? "unknown");
451
+ const uniqueSchemes = [...new Set(requiredSchemes)];
452
+ const schemesStr = uniqueSchemes.join(", ") || "unknown";
453
+ const firstScheme = uniqueSchemes[0] || "BearerAuth";
454
+ throw new Error(
455
+ `Authentication required for tool '${tool2.name}' but no auth configuration found.
456
+ Required security schemes: ${schemesStr}
457
+ Solutions:
458
+ 1. Add authProviderMapper: { '${firstScheme}': (ctx) => ctx.authInfo.user?.token }
459
+ 2. Add securityResolver: (tool, ctx) => ({ jwt: ctx.authInfo.token })
460
+ 3. Add staticAuth: { jwt: process.env.API_TOKEN }
461
+ 4. Set generateOptions.includeSecurityInInput: true (not recommended for production)`
462
+ );
553
463
  }
554
- if (typeof obj["slideWindow"] === "boolean") {
555
- result.slideWindow = obj["slideWindow"];
556
- } else if (obj["slideWindow"] !== void 0) {
557
- warnings.push(`Invalid 'cache.slideWindow': expected boolean`);
464
+ return await securityResolver.resolve(tool2.mapper, securityContext);
465
+ }
466
+
467
+ // libs/adapters/src/openapi/openapi.utils.ts
468
+ var import_utils = require("@frontmcp/utils");
469
+ var import_sdk = require("@frontmcp/sdk");
470
+ function coerceToString(value, paramName, location) {
471
+ if (value === null || value === void 0) {
472
+ return "";
558
473
  }
559
- for (const key of Object.keys(obj)) {
560
- if (!KNOWN_CACHE_FIELDS.has(key)) {
561
- warnings.push(`Unknown field 'cache.${key}' (will be ignored)`);
474
+ if (typeof value === "object") {
475
+ if (Array.isArray(value)) {
476
+ if (location === "query") {
477
+ return value.map(String).join(",");
478
+ }
479
+ throw new Error(`${location} parameter '${paramName}' cannot be an array. Received: ${JSON.stringify(value)}`);
562
480
  }
481
+ throw new Error(`${location} parameter '${paramName}' cannot be an object. Received: ${JSON.stringify(value)}`);
563
482
  }
564
- return Object.keys(result).length > 0 ? result : void 0;
483
+ return String(value);
565
484
  }
566
- function extractValidCodeCall(data, warnings) {
567
- if (typeof data !== "object" || data === null) return void 0;
568
- const obj = data;
569
- const result = {};
570
- if (typeof obj["enabledInCodeCall"] === "boolean") {
571
- result.enabledInCodeCall = obj["enabledInCodeCall"];
572
- } else if (obj["enabledInCodeCall"] !== void 0) {
573
- warnings.push(`Invalid 'codecall.enabledInCodeCall': expected boolean`);
574
- }
575
- if (typeof obj["visibleInListTools"] === "boolean") {
576
- result.visibleInListTools = obj["visibleInListTools"];
577
- } else if (obj["visibleInListTools"] !== void 0) {
578
- warnings.push(`Invalid 'codecall.visibleInListTools': expected boolean`);
485
+ function appendCookie(headers, name, value) {
486
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
487
+ throw new Error(`Invalid cookie name: '${name}'. Cookie names must be valid tokens.`);
579
488
  }
580
- for (const key of Object.keys(obj)) {
581
- if (!KNOWN_CODECALL_FIELDS.has(key)) {
582
- warnings.push(`Unknown field 'codecall.${key}' (will be ignored)`);
489
+ const existing = headers.get("Cookie") || "";
490
+ const cookiePair = `${name}=${encodeURIComponent(coerceToString(value, name, "cookie"))}`;
491
+ const combined = existing ? `${existing}; ${cookiePair}` : cookiePair;
492
+ headers.set("Cookie", combined);
493
+ }
494
+ function buildRequest(tool2, input, security, baseUrl) {
495
+ const rawBaseUrl = tool2.metadata.servers?.[0]?.url || baseUrl;
496
+ (0, import_utils.validateBaseUrl)(rawBaseUrl);
497
+ const apiBaseUrl = rawBaseUrl.replace(/\/+$/, "");
498
+ let path = tool2.metadata.path;
499
+ const queryParams = new URLSearchParams();
500
+ const headers = new Headers({
501
+ accept: "application/json",
502
+ ...security.headers
503
+ });
504
+ let body;
505
+ for (const mapper of tool2.mapper) {
506
+ if (mapper.security) continue;
507
+ const value = input[mapper.inputKey];
508
+ if (value === void 0 || value === null) {
509
+ if (mapper.required) {
510
+ throw new Error(
511
+ `Required ${mapper.type} parameter '${mapper.key}' (input: '${mapper.inputKey}') is missing for operation '${tool2.name}'`
512
+ );
513
+ }
514
+ continue;
515
+ }
516
+ switch (mapper.type) {
517
+ case "path":
518
+ path = path.replaceAll(`{${mapper.key}}`, encodeURIComponent(coerceToString(value, mapper.key, "path")));
519
+ break;
520
+ case "query":
521
+ queryParams.set(mapper.key, coerceToString(value, mapper.key, "query"));
522
+ break;
523
+ case "header": {
524
+ const headerValue = coerceToString(value, mapper.key, "header");
525
+ if (/[\r\n\x00\f\v]/.test(headerValue)) {
526
+ throw new import_sdk.PublicMcpError(
527
+ `Invalid header value for '${mapper.key}': contains control characters (possible header injection attack)`,
528
+ "INVALID_HEADER_VALUE",
529
+ 400
530
+ );
531
+ }
532
+ headers.set(mapper.key, headerValue);
533
+ break;
534
+ }
535
+ case "cookie":
536
+ appendCookie(headers, mapper.key, value);
537
+ break;
538
+ case "body":
539
+ if (!body) body = {};
540
+ body[mapper.key] = value;
541
+ break;
542
+ default:
543
+ throw new Error(
544
+ `Unknown mapper type '${mapper.type}' for parameter '${mapper.key}' in operation '${tool2.name}'`
545
+ );
583
546
  }
584
547
  }
585
- return Object.keys(result).length > 0 ? result : void 0;
586
- }
587
- function extractValidTags(data) {
588
- if (!Array.isArray(data)) return void 0;
589
- const validTags = data.filter((item) => typeof item === "string");
590
- return validTags.length > 0 ? validTags : void 0;
591
- }
592
- function extractValidExamples(data, warnings) {
593
- if (!Array.isArray(data)) return void 0;
594
- const validExamples = [];
595
- for (let i = 0; i < data.length; i++) {
596
- const item = data[i];
597
- const result = FrontMcpExampleSchema.safeParse(item);
598
- if (result.success) {
599
- validExamples.push(result.data);
600
- } else {
601
- warnings.push(
602
- `Invalid example at index ${i}: ${formatZodIssues(result.error.issues, `examples[${i}]`).join(", ")}`
548
+ Object.entries(security.query).forEach(([key, value]) => {
549
+ if (queryParams.has(key)) {
550
+ throw new Error(
551
+ `Query parameter collision: '${key}' is provided both as user input and security parameter. This could indicate a security misconfiguration in operation '${tool2.name}'.`
603
552
  );
604
553
  }
554
+ queryParams.set(key, coerceToString(value, key, "security query"));
555
+ });
556
+ if (security.cookies && Object.keys(security.cookies).length > 0) {
557
+ Object.entries(security.cookies).forEach(([key, value]) => {
558
+ appendCookie(headers, key, value);
559
+ });
605
560
  }
606
- return validExamples.length > 0 ? validExamples : void 0;
561
+ if (path.includes("{")) {
562
+ throw new Error(`Failed to resolve all path parameters in ${path} for operation ${tool2.name}`);
563
+ }
564
+ const queryString = queryParams.toString();
565
+ const url = `${apiBaseUrl}${path}${queryString ? `?${queryString}` : ""}`;
566
+ return { url, headers, body };
607
567
  }
608
- function formatZodIssues(issues, prefix) {
609
- return issues.map((issue) => {
610
- const path = issue.path.length > 0 ? `${prefix}.${issue.path.join(".")}` : prefix;
611
- return `Invalid '${path}': ${issue.message}`;
568
+ function applyAdditionalHeaders(headers, additionalHeaders) {
569
+ if (!additionalHeaders) return;
570
+ Object.entries(additionalHeaders).forEach(([key, value]) => {
571
+ headers.set(key, value);
612
572
  });
613
573
  }
574
+ var DEFAULT_MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
575
+ async function parseResponse(response, options) {
576
+ const maxSize = options?.maxResponseSize ?? DEFAULT_MAX_RESPONSE_SIZE;
577
+ const contentLength = response.headers.get("content-length");
578
+ if (contentLength) {
579
+ const length = parseInt(contentLength, 10);
580
+ if (!isNaN(length) && isFinite(length) && length > maxSize) {
581
+ throw new Error(`Response size (${length} bytes) exceeds maximum allowed (${maxSize} bytes)`);
582
+ }
583
+ }
584
+ const text = await response.text();
585
+ const byteSize = new TextEncoder().encode(text).length;
586
+ if (byteSize > maxSize) {
587
+ throw new Error(`Response size (${byteSize} bytes) exceeds maximum allowed (${maxSize} bytes)`);
588
+ }
589
+ const contentType = response.headers.get("content-type");
590
+ let data;
591
+ if (contentType?.toLowerCase().includes("application/json")) {
592
+ try {
593
+ data = JSON.parse(text);
594
+ } catch {
595
+ data = text;
596
+ }
597
+ } else {
598
+ data = text;
599
+ }
600
+ if (!response.ok) {
601
+ return {
602
+ status: response.status,
603
+ ok: false,
604
+ data,
605
+ error: typeof data === "object" && data !== null && "message" in data ? String(data.message) : typeof data === "string" ? data : `HTTP ${response.status} error`
606
+ };
607
+ }
608
+ return {
609
+ status: response.status,
610
+ ok: true,
611
+ data
612
+ };
613
+ }
614
614
 
615
615
  // libs/adapters/src/openapi/openapi.tool.ts
616
616
  function createOpenApiTool(openapiTool, options, logger) {
@@ -874,7 +874,7 @@ async function injectTransformedValues(input, transforms, ctx) {
874
874
  function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
875
875
  if (typeof jsonSchema !== "object" || jsonSchema === null) {
876
876
  logger.warn(`[${toolName}] No valid JSON schema provided, using permissive schema`);
877
- return { schema: import_zod2.z.looseObject({}), conversionFailed: true, error: "No valid JSON schema" };
877
+ return { schema: import_lazy_zod2.z.looseObject({}), conversionFailed: true, error: "No valid JSON schema" };
878
878
  }
879
879
  try {
880
880
  const zodSchema = (0, import_zod_from_json_schema.convertJsonSchemaToZod)(jsonSchema);
@@ -887,7 +887,7 @@ function getZodSchemaFromJsonSchema(jsonSchema, toolName, logger) {
887
887
  logger.warn(
888
888
  `[${toolName}] Failed to generate Zod schema, using permissive schema. Tool will accept any input but may fail at API level. Error: ${errorMessage}`
889
889
  );
890
- return { schema: import_zod2.z.looseObject({}), conversionFailed: true, error: errorMessage };
890
+ return { schema: import_lazy_zod2.z.looseObject({}), conversionFailed: true, error: errorMessage };
891
891
  }
892
892
  }
893
893