@@ -132,7 +132,29 @@ export class OpenApiToolLoader {
132132 auth ?: UpstreamAuth ,
133133 ) : Promise < OpenApiToolLoader > {
134134 const hash = await sha256Hex ( JSON . stringify ( spec ) ) ;
135- return new OpenApiToolLoader ( spec , hash , options , auth ) ;
135+
136+ // Resolve baseUrl from spec.servers when not explicitly provided
137+ let resolvedBaseUrl = options ?. baseUrl ;
138+ if ( ! resolvedBaseUrl ) {
139+ const servers = spec [ 'servers' ] as Array < { url ?: string } > | undefined ;
140+ if ( servers ?. [ 0 ] ?. url ) {
141+ try {
142+ const serverUrl = new URL ( servers [ 0 ] . url ) ;
143+ resolvedBaseUrl = serverUrl . origin + serverUrl . pathname ;
144+ } catch {
145+ throw new Error (
146+ `Cannot resolve relative server URL "${ servers [ 0 ] . url } " without a source URL. Use fromURL() or provide an explicit baseUrl option.` ,
147+ ) ;
148+ }
149+ }
150+ }
151+
152+ return new OpenApiToolLoader (
153+ spec ,
154+ hash ,
155+ resolvedBaseUrl ? { ...options , baseUrl : resolvedBaseUrl } : options ,
156+ auth ,
157+ ) ;
136158 }
137159
138160 /**
@@ -182,7 +204,7 @@ export class OpenApiToolLoader {
182204 ? `${ this . options . sourceName } _${ baseName } `
183205 : baseName ;
184206
185- const argsSchema = this . buildArgsSchema ( op ) ;
207+ const { schema : argsSchema , bodyMediaType } = this . buildArgsSchema ( op ) ;
186208 const timeout = this . options . perToolDeadlineMs ?? 30000 ;
187209
188210 const tool : ToolDefinition = {
@@ -192,7 +214,7 @@ export class OpenApiToolLoader {
192214 config : {
193215 timeout,
194216 } ,
195- handler : this . createHandler ( op , baseUrl ) ,
217+ handler : this . createHandler ( op , baseUrl , bodyMediaType ) ,
196218 } ;
197219
198220 this . tools . push ( tool ) ;
@@ -257,8 +279,9 @@ export class OpenApiToolLoader {
257279 /**
258280 * Build a Zod args schema from an OpenAPI operation.
259281 */
260- private buildArgsSchema ( op : ParsedOperation ) : z . ZodType {
282+ private buildArgsSchema ( op : ParsedOperation ) : { schema : z . ZodType ; bodyMediaType ?: string } {
261283 const shape : Record < string , z . ZodType > = { } ;
284+ let bodyMediaType : string | undefined ;
262285
263286 // Add parameters with OpenAPI type mapping
264287 if ( op . parameters ) {
@@ -270,21 +293,23 @@ export class OpenApiToolLoader {
270293
271294 // Add request body as 'body' parameter, inspecting content media type
272295 if ( op . requestBody ?. content ) {
273- const bodySchema = this . buildBodySchema ( op . requestBody . content ) ;
296+ const { schema : bodySchema , mediaType } = this . buildBodySchema ( op . requestBody . content ) ;
274297 shape [ 'body' ] = op . requestBody . required ? bodySchema : bodySchema . optional ( ) ;
298+ bodyMediaType = mediaType ;
275299 } else if ( op . requestBody ) {
276300 const fallback = z . record ( z . string ( ) , z . unknown ( ) ) ;
277301 shape [ 'body' ] = op . requestBody . required ? fallback : fallback . optional ( ) ;
278302 }
279303
280- return Object . keys ( shape ) . length > 0 ? z . object ( shape ) : z . record ( z . string ( ) , z . unknown ( ) ) ;
304+ const schema = Object . keys ( shape ) . length > 0 ? z . object ( shape ) : z . record ( z . string ( ) , z . unknown ( ) ) ;
305+ return { schema, bodyMediaType } ;
281306 }
282307
283308 /**
284309 * Map an OpenAPI schema type to the corresponding Zod type.
285310 */
286311 private mapOpenApiType ( schema ?: Record < string , unknown > ) : z . ZodType {
287- if ( ! schema || ! schema [ 'type' ] ) return z . string ( ) ;
312+ if ( ! schema || ! schema [ 'type' ] ) return z . unknown ( ) ;
288313
289314 const type = schema [ 'type' ] as string ;
290315 const enumValues = schema [ 'enum' ] as string [ ] | undefined ;
@@ -298,47 +323,56 @@ export class OpenApiToolLoader {
298323 return z . boolean ( ) ;
299324 case 'array' :
300325 return z . array ( this . mapOpenApiType ( schema [ 'items' ] as Record < string , unknown > | undefined ) ) ;
326+ case 'object' :
327+ return z . record ( z . string ( ) , z . unknown ( ) ) ;
301328 case 'string' :
302329 if ( enumValues && enumValues . length > 0 ) {
303330 return z . enum ( enumValues as [ string , ...string [ ] ] ) ;
304331 }
305332 return z . string ( ) ;
306333 default :
307- return z . string ( ) ;
334+ return z . unknown ( ) ;
308335 }
309336 }
310337
311338 /**
312339 * Build a Zod schema for the request body based on the media type.
340+ * Returns both the schema and the chosen media type for correct serialization.
313341 */
314- private buildBodySchema ( content : Record < string , { schema ?: Record < string , unknown > } > ) : z . ZodType {
342+ private buildBodySchema ( content : Record < string , { schema ?: Record < string , unknown > } > ) : {
343+ schema : z . ZodType ;
344+ mediaType : string ;
345+ } {
315346 // Prefer JSON media types
316347 const jsonKey = Object . keys ( content ) . find ( ( k ) => k . includes ( 'json' ) ) ;
317348 if ( jsonKey ) {
318349 const schema = content [ jsonKey ] . schema ;
319- if ( schema ) return this . mapOpenApiType ( schema ) ;
320- return z . record ( z . string ( ) , z . unknown ( ) ) ;
350+ if ( schema ) return { schema : this . mapOpenApiType ( schema ) , mediaType : jsonKey } ;
351+ return { schema : z . record ( z . string ( ) , z . unknown ( ) ) , mediaType : jsonKey } ;
321352 }
322353
323354 // Form data
324- if ( content [ 'application/x-www-form-urlencoded' ] || content [ 'multipart/form-data' ] ) {
325- return z . record ( z . string ( ) , z . unknown ( ) ) ;
355+ if ( content [ 'application/x-www-form-urlencoded' ] ) {
356+ return { schema : z . record ( z . string ( ) , z . unknown ( ) ) , mediaType : 'application/x-www-form-urlencoded' } ;
357+ }
358+ if ( content [ 'multipart/form-data' ] ) {
359+ return { schema : z . record ( z . string ( ) , z . unknown ( ) ) , mediaType : 'multipart/form-data' } ;
326360 }
327361
328362 // Plain text
329363 const textKey = Object . keys ( content ) . find ( ( k ) => k . startsWith ( 'text/' ) ) ;
330364 if ( textKey ) {
331- return z . string ( ) ;
365+ return { schema : z . string ( ) , mediaType : textKey } ;
332366 }
333367
334368 // Fallback
335- return z . record ( z . string ( ) , z . unknown ( ) ) ;
369+ return { schema : z . record ( z . string ( ) , z . unknown ( ) ) , mediaType : 'application/json' } ;
336370 }
337371
338372 /**
339373 * Create a handler function for an OpenAPI operation.
340374 */
341- private createHandler ( op : ParsedOperation , baseUrl : string ) : ToolDefinition [ 'handler' ] {
375+ private createHandler ( op : ParsedOperation , baseUrl : string , bodyMediaType ?: string ) : ToolDefinition [ 'handler' ] {
342376 const auth = this . auth ;
343377 const headers = this . options . headers ?? { } ;
344378
@@ -370,8 +404,11 @@ export class OpenApiToolLoader {
370404
371405 // Build request headers (only set Content-Type for methods with a body)
372406 const hasBody = [ 'post' , 'put' , 'patch' ] . includes ( op . method ) && params [ 'body' ] != null ;
407+ const resolvedMediaType = bodyMediaType ?? 'application/json' ;
408+ const isMultipart = resolvedMediaType === 'multipart/form-data' ;
373409 const requestHeaders : Record < string , string > = {
374- ...( hasBody && { 'Content-Type' : 'application/json' } ) ,
410+ // Omit Content-Type for multipart/form-data to let the runtime set the boundary
411+ ...( hasBody && ! isMultipart && { 'Content-Type' : resolvedMediaType } ) ,
375412 ...headers ,
376413 } ;
377414
@@ -390,10 +427,16 @@ export class OpenApiToolLoader {
390427 }
391428 }
392429
393- // Add header parameters
430+ // Add header parameters (skip protected auth headers to prevent credential overwrite)
431+ const protectedHeaders = new Set < string > ( ) ;
432+ if ( auth ) {
433+ if ( auth . type === 'bearer' || auth . type === 'basic' ) protectedHeaders . add ( 'authorization' ) ;
434+ if ( auth . type === 'api-key' ) protectedHeaders . add ( ( auth . header ?? 'X-API-Key' ) . toLowerCase ( ) ) ;
435+ }
394436 if ( op . parameters ) {
395437 for ( const param of op . parameters ) {
396438 if ( param . in === 'header' && params [ param . name ] !== undefined ) {
439+ if ( protectedHeaders . has ( param . name . toLowerCase ( ) ) ) continue ;
397440 requestHeaders [ param . name ] = String ( params [ param . name ] ) ;
398441 }
399442 }
@@ -407,7 +450,22 @@ export class OpenApiToolLoader {
407450 } ;
408451
409452 if ( hasBody ) {
410- fetchOptions . body = JSON . stringify ( params [ 'body' ] ) ;
453+ if ( resolvedMediaType . includes ( 'json' ) ) {
454+ fetchOptions . body = JSON . stringify ( params [ 'body' ] ) ;
455+ } else if ( resolvedMediaType === 'application/x-www-form-urlencoded' ) {
456+ fetchOptions . body = new URLSearchParams ( params [ 'body' ] as Record < string , string > ) . toString ( ) ;
457+ } else if ( resolvedMediaType === 'multipart/form-data' ) {
458+ const formData = new FormData ( ) ;
459+ const bodyObj = params [ 'body' ] as Record < string , unknown > ;
460+ for ( const [ key , value ] of Object . entries ( bodyObj ) ) {
461+ formData . append ( key , value instanceof Blob ? value : String ( value ) ) ;
462+ }
463+ fetchOptions . body = formData ;
464+ } else if ( resolvedMediaType . startsWith ( 'text/' ) ) {
465+ fetchOptions . body = String ( params [ 'body' ] ) ;
466+ } else {
467+ fetchOptions . body = JSON . stringify ( params [ 'body' ] ) ;
468+ }
411469 }
412470
413471 const response = await fetch ( url , fetchOptions ) ;
0 commit comments