OpenAPI adapter

OpenapiAdapter, from @frontmcp/adapters, reads an OpenAPI 3 description of a REST API and gives your app one tool per operation. When the model calls getTicket, the adapter builds the HTTP request the spec describes, sends it with the credentials you configure, and hands back the API's answer. Use it to put an API you already have in front of a model without writing a tool for each endpoint; write tools yourself, with this.fetch(), when the model needs a few well-shaped calls rather than the whole API. For an API too large to list as tools, see Skilled OpenAPI. Wrapping an OpenAPI Service teaches the adapter step by step. The Pet Store from OpenAPI example wraps a whole API and makes it fit for a model.

@App({ adapters: [OpenapiAdapter.init({ name, baseUrl, spec | url, ...options })] })

Reference

OpenapiAdapter.init(options)

Install the package with npm install @frontmcp/adapters and list the result of init() in the adapters of an @App. A plugin can bring adapters too, in its own adapters option, and they add their tools to the app the plugin is on. In @FrontMcp({ adapters }), the adapter's tools are served from every app (Registering an adapter); before 1.9, @FrontMcp ignored adapters.

desk.app.ts
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import spec from "./desk-openapi.json";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://api.desk.example/v1",
      spec,
      staticAuth: { jwt: process.env.DESK_API_TOKEN },
    }),
  ],
})
export class DeskApp {}

It's also the default export of @frontmcp/adapters/openapi. See more examples below.

Options

Required:

OptionTypeDescription
namestringNames this adapter. It must be unique among the OpenapiAdapter.init() calls of the whole process, not only of one server: a second call with the same name throws (see Troubleshooting). It's also the prefix a tool gets when its name clashes with another tool's, and the adapter's logger is adapter:<name>.
baseUrlstringWhere the API is, like https://api.desk.example/v1. Each operation's path is appended to it. It wins over the spec's servers. It must be http: or https:; any other value fails every call with Invalid base URL: ….
spec or urlobject or stringThe OpenAPI 3.0 or 3.1 document: spec as an object (an imported JSON file, or a literal), or url, an http: or https: address to download it from when the server starts. Exactly one. See loading the spec from a URL.

Calling the API:

OptionTypeDefaultDescription
additionalHeadersRecord<string, string>Headers sent with every request. They replace headers of the same name that the adapter built, accept included. A security scheme's header set here counts as that scheme's credential (see how credentials are chosen).
headersMapper(ctx, headers) => HeadersCalled for every request with the request's context (ctx.authInfo, ctx.requestId, ctx.traceContext, …) and the headers built so far. Headers in the Headers object it returns are set on the request. Runs after additionalHeaders, so it can override them. A security scheme's header it sets counts as that scheme's credential.
bodyMapper(ctx, body) => objectCalled with the context and the JSON body, for operations that send one; the body is replaced by what it returns. Not called when the request has no body.
requestTimeoutMsnumber30000Milliseconds a request may take. After that it's aborted and the call fails with Request timeout after …ms for tool '…'.
maxRequestSizenumber10485760 (10 MB)Largest JSON body, in bytes, the adapter sends. A larger one fails the call before anything is sent.
maxResponseSizenumber10485760 (10 MB)Largest response, in bytes, the adapter reads. A larger one fails the call.

Credentials, described in how credentials are chosen:

OptionTypeDescription
staticAuthPartial<SecurityContext>Fixed credentials for every call, like { jwt: process.env.DESK_API_TOKEN }, whoever the caller is. With authProviderMapper, they fill in what its functions don't return.
authProviderMapperRecord<string, (ctx) => string | undefined>One function per security scheme in the spec (components.securitySchemes), returning that scheme's credential for this call, or undefined for none. Every scheme the spec uses must have one, or another source: staticAuth, securitySchemesInInput, a header from additionalHeaders or headersMapper, or passthroughCallerToken for an HTTP bearer scheme. Otherwise the server doesn't start.
securityResolver(tool, ctx) => SecurityContext | Promise<SecurityContext>Returns the credentials for one call to one tool. It wins over the other options.
securitySchemesInInputstring[]Schemes whose credential the model provides, as a required tool argument named after the scheme. The adapter sends it where the scheme says, unless the options above give that scheme a credential, which wins. The other schemes still come from the options above. Rated HIGH risk.
passthroughCallerTokenbooleanDefault false. Send the MCP client's own token (ctx.authInfo.token) to the API as a bearer token, when neither authProviderMapper nor staticAuth gave a credential. It fills HTTP bearer schemes only: an operation secured by an API key still needs its key. Only for an API that accepts the tokens your server accepts. Not used with securityResolver.

Which operations become tools, and how they look:

OptionTypeDescription
generateOptionsOpenApiGenerateOptionsWhich operations to include, how to name them, and how schemas are built. See generateOptions.
descriptionMode"summaryOnly" | "descriptionOnly" | "combined" | "full"How a tool's description is made from the operation. Default "summaryOnly". See how operations become tools.
toolTransforms{ global?, perTool?, generator? }Rename a tool, change its description, add annotations, tags or examples, or hide it from tools/list. See toolTransforms.
inputTransforms{ global?, perTool?, generator? }Remove arguments from a tool's input and fill them in on the server. See inputTransforms.
schemaTransforms{ input?, output? }Functions that return a new input or output JSON Schema for a tool. See schemaTransforms.
outputSchema{ mode?, descriptionFormat?, descriptionFormatter? }Keep the response schema as the tool's outputSchema, move it into the description, or both. See outputSchema.
dataTransforms{ preToolTransforms?, postToolTransforms? }Change output schemas and descriptions when tools are built, and the API's data before the model gets it. See dataTransforms.

Loading and watching the spec:

OptionTypeDescription
loadOptionsLoadOptionsHow the spec is loaded and checked. See loadOptions.
pollingSpecPollerOptions & { enabled: boolean }Fetch url again every so often and rebuild the tools when the spec changes. Only with url. See polling.
loggerFrontMcpLoggerA logger for the adapter. Inside a server FrontMCP replaces it with its own, adapter:<name>, before the tools are built.

generateOptions

The adapter passes these fields on to the generator (mcp-from-openapi) that turns operations into tools:

FieldTypeDefaultDescription
includeOperationsstring[]Only these operationIds. Operations without an operationId are left out too.
excludeOperationsstring[]Leave these operationIds out.
filterFn(op) => booleanCalled for each operation, with the operation object plus its path and method (lowercase). Return false to leave it out.
namingStrategy.toolNameGenerator(path, method, operationId?, operation?) => stringoperationId, else <method>_<path>Names each tool.
namingStrategy.conflictResolver(name, location, index) => stringthe location in front: pathId, queryId, bodyIdRenames an input whose name is used in two places, like a path id and a query id.
includeDeprecatedbooleanfalseAlso turn operations marked deprecated: true into tools.
preferredStatusCodesnumber[][200, 201, 202, 204]Which response's schema describes success, in order of preference.
includeAllResponsesbooleantrueDescribe every documented response in the output schema (a oneOf), not only the preferred one.
includeSecurityInInputboolean | string[]falseAdd each security scheme (or the ones listed) to the tool's input, as a required string argument named after the scheme, so the model supplies credentials. They're sent like securitySchemesInInput's, and a list means the same as securitySchemesInInput: every scheme it leaves out needs another credential source, or the server doesn't start. (Before 1.8.5, a list's values were never sent.) The adapter logs a SECURITY WARNING and rates the setup HIGH risk.
maxSchemaDepthnumber10Nesting kept in input and output schemas; deeper levels are cut, with a note in the description.
includeExamplesbooleanfalseCopy parameter and media-type example/examples into the schemas.
resolveFormatsbooleanfalseTurn formats into constraints: format: "uuid" gets a pattern and the description UUID string (RFC 4122), int32 a minimum and maximum, email the description Email address (RFC 5322), and so on.
formatResolversRecord<string, (schema) => schema>Your own formats, like phone, or replacements for the built-in ones. Without resolveFormats, only these apply.

OpenApiGenerateOptions, exported by @frontmcp/adapters, is mcp-from-openapi's GenerateOptions, which has more fields, with method filters that take any case. The adapter passes every field on. These select operations, as Choosing which operations become tools shows:

FieldTypeKeeps
includeTags, excludeTagsstring[]Operations with at least one of these tags; or leaves out those with any of them.
includeMethods, excludeMethodsOpenApiHttpMethod[]Operations with these methods, or without them, in any case: "delete", "DELETE" or "Delete". A name that isn't an HTTP method makes init() throw. Before 1.9.3 the type took lower case only, so ["DELETE"] was a type error (TS2820), though it matched at run time.
includePaths, excludePathsstring[]Operations whose path matches one of these globs, or none of them. * matches within one segment and ** across segments: "/tickets" is that path only, "/tickets/*" is /tickets/{id}, and "/admin/**" is everything under /admin.
readOnlyOnlybooleanOnly operations that only read, as the generator judges from the method, like GET, unless the spec's x-frontmcp annotations say otherwise.

Three add to what tools/list shows, as how operations become tools describes:

FieldTypeDefaultDescription
inferAnnotationsbooleantrueGive each tool annotations guessed from its HTTP method. false leaves annotations to x-frontmcp and toolTransforms.
emitMetabooleanfalseAdd _meta["dev.agentfront.openapi/operation"], the operation's path, method, operationId, specTitle and specVersion, to each tool.
inheritDocumentIconsbooleanfalseGive tools without icons of their own the spec's info["x-logo"] as their icon.

The others, descriptionStrategy, appendResponseSummary, maxProperties, maxDescriptionLength, stripExamples, target, maxToolNameLength and emitTypeSignatures, change how tools are built: see mcp-from-openapi's documentation.

Changed in 1.9: the adapter passed on only the fields in the first table, so the others were accepted and had no effect. Changed in 1.9.2: the annotations, title, icons and _meta the generator makes reach the tool, so every tool now lists inferred annotations and its summary as its title.

loadOptions

FieldTypeDefaultDescription
validatebooleantrueCheck the document before building tools. An invalid one stops the server: Invalid OpenAPI document.
dereferencebooleantrueResolve $refs.
refResolution{ allowedProtocols?, allowedHosts?, blockedHosts?, allowInternalIPs? }{ allowedProtocols: [] }Which $refs outside the document may be fetched, and which hosts a url may be loaded from. See below.
headersRecord<string, string>Headers for fetching url, like a token for a private spec. Only with url.
timeoutnumber30000Milliseconds to fetch url. Only with url.
followRedirectsbooleanfalseFollow redirects when fetching url, checking each hop like the first. Only with url.

References inside the document (#/components/...) are always resolved. A $ref to another file or URL isn't fetched by default: FrontMCP sets allowedProtocols to [], and the $ref stays in the tool's schema unresolved. Set refResolution to allow some: allowedProtocols: ["https"] with allowedHosts: ["schemas.desk.example"], say. Addresses on private networks, loopback and cloud metadata endpoints are refused unless allowInternalIPs: true, for $refs and for url alike.

LoadOptions has two more fields, and the adapter passes on every field. overlays takes one OpenAPI Overlay document or a list of them, applied in order before the spec is checked: a way to rewrite summaries, or add x-frontmcp, in a file of your own that survives the next version of the spec (see Naming and describing the tools). secureDefaults: true turns off redirects and external $refs, which the adapter already does by default.

toolTransforms

global applies to every tool, perTool to the tool with that name (the name before any rename), and generator is called with each tool and returns a transform or undefined. For one tool, perTool and generator override global's name, description, hideFromDiscovery and ui; annotations are merged, and tags and examples are added together.

FieldTypeDescription
namestring | (name, tool) => stringThe tool's new name.
descriptionstring | (description, tool) => stringThe tool's new description. The function gets the current one.
annotationsToolAnnotationsreadOnlyHint, destructiveHint, idempotentHint, openWorldHint, title: merged over the annotations inferred from the method and those the spec's x-frontmcp set.
hideFromDiscoverybooleanLeave the tool out of tools/list. It can still be called by name.
tags, examples, uiAs the @Tool options of the same names.

A transform's tool argument is the generated tool: tool.name, tool.description, tool.inputSchema, and tool.metadata, with method (lowercase), path, operationId, operationSummary, operationDescription and tags.

inputTransforms

global is a list for every tool, perTool maps a tool name to a list, and generator returns a list for a tool. Each entry is { inputKey, inject }:

  • inputKey is removed from the tool's input schema (and from required), so the model never sees it.
  • inject({ ctx, env, tool }) runs on every call, with the request's context, process.env and the tool, and its result becomes that argument. A value the model sends for the argument anyway is dropped before the argument check, and the injected one is sent. (In 1.9.2 such a call was refused with Unrecognized key.) Returning undefined leaves the argument out, which fails the call for a parameter the spec requires (see Troubleshooting). It may be async, and must answer within 5 seconds or the call fails with Input transform for '…' failed: Transform timeout after 5000ms.
  • __proto__, constructor and prototype aren't allowed as inputKey: the server doesn't start.

schemaTransforms

input and output each take global, perTool and generator: functions (schema, { tool, adapterOptions }) => schema that return the tool's new input or output JSON Schema. For one tool only one applies: generator's, else perTool's, else global's. An output function may return undefined to drop the output schema. The schema you return is what clients see; arguments are still sent as the spec describes.

outputSchema

FieldTypeDefaultDescription
mode"definition" | "description" | "both""definition"Where the response schema goes: the tool's outputSchema, the end of its description (and no outputSchema), or both.
descriptionFormat"summary" | "jsonSchema""summary"In the description: a ## Returns list of fields with their types, or an ## Output Schema block with the JSON Schema.
descriptionFormatter(schema, { tool, adapterOptions, originalDescription }) => string | Promise<string>Your own text, appended to the description instead.

The schema in the description is the API's response body; the tool's outputSchema, when there is one, is the result envelope around it.

dataTransforms

  • preToolTransforms (global, perTool, generator) runs when tools are built: transformSchema(outputSchema, ctx) returns a new output schema, or undefined to drop it, and transformDescription(description, outputSchema, ctx) returns a new description.
  • postToolTransforms (global, perTool, generator) runs on every call: transform(data, ctx) returns what the model gets as data, and an optional filter(ctx) returning false skips it. ctx has status, ok, tool, ctx (the request context) and adapterOptions. For one tool only one transform runs: generator's, else perTool's, else global's. If transform throws, the error is logged and the model gets the data unchanged.

polling

Only with url: polling with spec throws Polling requires URL-based options (use 'url' instead of 'spec'). The fields are those of OpenApiSpecPoller plus enabled: true. See picking up spec changes.

Returns

A record for adapters, not the adapter: { ...options, provide: Symbol.for("adapter:OpenapiAdapter:<name>"), useValue: <the OpenapiAdapter> }. init() constructs the adapter at once, when your module is imported. The spec is read, and the tools built, when the server starts. To call the adapter's own methods, like stopPolling(), use record.useValue.

How operations become tools

From the spec
NameThe operationId. Without one: the method and path, like get_stats for GET /stats or post_tickets_By_id_comments for POST /tickets/{id}/comments.
TitleThe operation's summary. An operation without one has no title.
DescriptiondescriptionMode: summaryOnly (the default) uses summary, else description, else METHOD path; descriptionOnly prefers description; combined is summary, a blank line, then description; full adds Operation: <operationId> and METHOD path.
Input schemaOne object, with a property for each path, query, header and cookie parameter and each property of the JSON request body. Each property keeps its schema, with an x-parameter-location saying where it goes. Header parameters also get x-mcp-header (see header parameters). required lists the required parameters and body properties, and additionalProperties is false.
Output schema{ status, ok, data, error }, with status and ok required and data being the success response's schema, or a oneOf of every documented response. A response without a body schema, like a 204, is described as data: { type: "null" }.
AnnotationsAll four hints, guessed from the method (inferAnnotations): GET is readOnlyHint and idempotentHint; PUT and DELETE are destructiveHint and idempotentHint; POST and PATCH are destructiveHint. openWorldHint is false. The spec's x-frontmcp, then toolTransforms, are merged over them.
IconsThe operation's x-frontmcp.icons, a list of { src, mimeType?, sizes? }. With inheritDocumentIcons, the spec's info["x-logo"] for the others.
_metaThe operation's x-frontmcp.meta. With emitMeta, also "dev.agentfront.openapi/operation": { path, method, operationId, specTitle, specVersion }.
Left outOperations marked deprecated (see includeDeprecated) and those your generateOptions filter out.

Two operations that end up with the same name stop the server: Tool name collision: "…" produced by 2 operations: …. When an adapter's tool has the same name as a tool from another adapter or app, FrontMCP lists both with their owner's name in front: desk:getStatus and billing:getStatus, or <app id>:getStatus for the app's own tool.

The x-frontmcp extension

An operation in the spec can carry an x-frontmcp object, which the adapter copies onto the tool:

FieldBecomes
annotationsThe tool's annotations: title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint.
hideFromDiscoveryThe tool's hideFromDiscovery.
tags, examples, cache, codecallThe @Tool options of the same names, for the plugins that read them (Cache, CodeCall).
iconsThe tool's icons, a list of { src, mimeType?, sizes? }.
metaEntries for the tool's _meta, like { "desk.example/team": "support" }.

A field it doesn't know is dropped, with a warning in the log: Unknown field 'priority' in x-frontmcp (will be ignored). Before 1.9.3 icons and meta got that warning too, though the tool got them. toolTransforms applies after x-frontmcp, so it wins.

Arguments are checked against the spec

Before anything is sent, a call's arguments are checked against the tool's input schema: the one clients see in tools/list, after any schemaTransforms. A call that doesn't match is refused with INVALID_INPUT, and the API receives nothing. The message names each argument and what's wrong with it, separated by ; :

The model sendsThe call fails with
No id, which is requiredInvalid arguments for tool 'getTicket': id: Invalid input: expected string, received undefined
A value outside an enumInvalid arguments for tool 'listTickets': status: Invalid option: expected one of "open"|"closed"
A key the operation doesn't haveInvalid arguments for tool 'listTickets': input: Unrecognized key: "page"
A number above maximumInvalid arguments for tool 'listTickets': limit: Too big: expected number to be <=50
A string for an integerInvalid arguments for tool 'listTickets': limit: Invalid input: expected number, received string

Ranges, lengths and pattern are checked the same way, in path, query, header and cookie parameters and in body fields. Values aren't converted: "5" isn't a number. What isn't checked:

  • format. "yesterday" passes as a date-time, and is sent.
  • oneOf is read as anyOf: a value that matches more than one branch passes.
  • Credentials in the input, from securitySchemesInInput or includeSecurityInInput: the check doesn't require them, so the server's own credential can fill the scheme. The credential check decides.
  • A required parameter with a default. The check lets the model leave it out, and the adapter sends the default. Changed in 1.9.3: the default wasn't sent, and the call failed when the request was built, Required query parameter 'format' (input: 'format') is missing for operation 'exportTickets', as a TOOL_EXECUTION_ERROR.

Changed in 1.9.2: arguments weren't checked, so a wrong value reached the API, and only a missing required parameter failed, when the request was built. Handling what the API answers shows the check.

What a call sends

For each call, in this order:

  1. The arguments are checked against the input schema: see above. An argument that inputTransforms fills in isn't part of it: a value the model sent for one is dropped first.
  2. inputTransforms fill in their arguments.
  3. Credentials are resolved: see how credentials are chosen. Credentials the model sent in the tool's input fill the schemes the server gave none.
  4. The request is built from the arguments. Path parameters are URL-encoded into the path, query parameters go in the query string (an array becomes comma-separated), header and cookie parameters go in headers, and body properties make a JSON body. The URL is baseUrl plus the path. Headers start with accept: application/json and the credentials' headers. A required parameter the model left out is sent with the spec's default; an optional one is left out, default or not, so the API applies its own. A required parameter that still has no value fails here, which happens when an inputTransforms inject returns undefined for it: Required body parameter 'channel' (input: 'channel') is missing for operation 'createTicket'. A header value with a line break fails with INVALID_HEADER_VALUE.
  5. additionalHeaders are set, then headersMapper runs.
  6. The credential is checked, on the request as it will be sent: an operation that requires credentials is refused unless the request carries one for at least one of its schemes. See how credentials are chosen.
  7. bodyMapper runs. A body gets content-type: application/json unless a header already set one.
  8. The body is checked against maxRequestSize.
  9. The request is sent with the global fetch, with the operation's method, a requestTimeoutMs timeout, and redirect: "manual": redirects aren't followed.

What the model gets back

The API answersThe model gets
2xxA result whose structuredContent, and text, is { status, ok: true, data }. data is the parsed JSON when the response's content-type includes application/json, else the text: "" for a 204.
4xx, 5xxAn error result (isError: true) whose text is {"status":404,"error":"…","data":…}, with _meta: { status, errorCode: "OPENAPI_ERROR" }. error is the body's message field, or the body when it's text, or HTTP <status> error.
3xxAn error result: {"status":302,"error":"Upstream returned a redirect (302); not followed to protect injected credentials."}, _meta.errorCode OPENAPI_REDIRECT_NOT_FOLLOWED.
Nothing in time, a response over maxResponseSize, a network failureThe call fails with TOOL_EXECUTION_ERROR: Request timeout after 30000ms for tool 'getTicket', Response size (… bytes) exceeds maximum allowed (… bytes), or the network error. In production FrontMCP hides these messages from the client.

postToolTransforms changes data before the model gets it, for errors too.

How credentials are chosen

The spec says which operations need credentials, with security and components.securitySchemes. For each call the adapter builds a SecurityContext from the first option you set:

  1. securityResolver(tool, ctx): whatever it returns.
  2. authProviderMapper: for each scheme the operation uses, the matching function's string becomes the credential its type needs (jwt for HTTP bearer, apiKey, basic, oauth2Token). A function that returns undefined or null gives that scheme nothing. staticAuth, when set, fills in every credential no function returned. If there's still no credential and passthroughCallerToken is true, the caller's own token (ctx.authInfo.token) is used as the bearer token.
  3. staticAuth: the fixed credentials.
  4. passthroughCallerToken: true: the caller's own token, as the bearer token.
  5. None of these: no credentials.

The caller's token is never sent unless you set passthroughCallerToken, and then only for HTTP bearer schemes. It was issued for your server, not for the API, and forwarding it is the token passthrough the MCP specification forbids.

Credentials the model sends in the tool's input, for the schemes in securitySchemesInInput or an includeSecurityInInput list (or every scheme, with includeSecurityInInput: true), are added for the schemes these options left empty. A credential from the server always wins, so a prompt that talks the model into sending another key can't replace the server's. A value with a line break or another control character fails the call with INVALID_HEADER_VALUE.

Then each of the operation's schemes takes its value from the context and puts it where the scheme says:

Scheme in the specFrom the SecurityContextSent as
http, scheme: bearerjwtAuthorization: Bearer <jwt>
http, scheme: basicbasic: username:password, already base64-encodedAuthorization: Basic <basic>
apiKey (in: header, query or cookie)apiKeys[<the key's name>], else customHeaders[<name>], else apiKeythe named header, query parameter or cookie
oauth2, openIdConnectoauth2TokenAuthorization: Bearer <oauth2Token>

A scheme with nothing to send is left out. The context also has digest, cookies (sent as cookies), clientCertificate, customResolver and signing fields; see SecurityContext in mcp-from-openapi.

Then, after additionalHeaders and headersMapper, the request as it will be sent must carry a credential for at least one of the operation's schemes, or the call fails before anything is sent: Authentication required for tool 'getTicket': no credential for its security schemes., followed by the schemes (Required security schemes: DeskToken (http)) and the options that would supply one. What counts for each scheme:

SchemeCounts as its credential
apiKey, in: headerA non-empty header with the key's name, like X-Reports-Key
apiKey, in: cookieA cookie with the key's name, in Cookie
apiKey, in: queryThe query parameter
http, scheme: bearer; oauth2; openIdConnectAuthorization: Bearer <something>
http, scheme: basicAuthorization: Basic <something>

So a key that additionalHeaders or headersMapper sets counts as much as one from staticAuth, while Authorization: Token abc doesn't count for a bearer scheme. Every scheme an operation lists counts as required, even when its security also allows {} (no credentials).

When the server starts, the adapter logs a Security Analysis with a risk rating: LOW for securityResolver or authProviderMapper; MEDIUM for staticAuth, or a spec with security and no credential option, which also logs a SECURITY WARNING that those operations fail unless additionalHeaders or headersMapper sets their credential; HIGH for securitySchemesInInput and includeSecurityInInput, and for passthroughCallerToken with an HTTP bearer scheme unless securityResolver or staticAuth keeps the caller's token from being used. With authProviderMapper, a scheme the spec uses that has no function stops the server, Invalid security configuration. Missing auth provider mappings for security schemes: …, unless another source covers it: staticAuth, securitySchemesInInput, additionalHeaders with the scheme's header (an Authorization header only for the scheme its first word names), headersMapper for a scheme sent in a header or cookie, or passthroughCallerToken for an HTTP bearer scheme.

Other exports

@frontmcp/adapters also exports the pieces the adapter is made of, for tools of your own:

ExportWhat it does
forceJwtSecurity(spec, options?)Returns a copy of the spec in which every operation, or those in options.operations, requires a scheme it adds: a bearer scheme named BearerAuth by default, or schemeType: "apiKey" (apiKeyIn, apiKeyName, default header X-API-Key) or "basic", named schemeName. For a spec that doesn't declare the security its API needs.
removeSecurityFromOperations(spec, operations?)Returns a copy of the spec in which those operations, or all, require no credentials.
buildRequest(tool, input, security, baseUrl)Builds { url, headers, body } for a generated tool, as in step 4 above.
parseResponse(response, { maxResponseSize? })Reads a Response into { status, ok, data, error? }, as the adapter does.
applyAdditionalHeaders(headers, additional)Sets each header on a Headers object.
createSecurityContextFromAuth(tool, ctx, options), resolveToolSecurity(tool, ctx, options)Build the SecurityContext, and the headers and query parameters it resolves to, as described above.
extractSecuritySchemes(tools), validateSecurityConfiguration(tools, options)The scheme names tools use, and the startup check with its risk rating.
OpenApiSpecPollerWatches a spec URL for changes. See below.
OpenAPIFetchErrorThrown by the poller when the spec URL answers with an error status. Code OPENAPI_FETCH_FAILED.
FRONTMCP_EXTENSION_KEY"x-frontmcp".
OpenApiGenerateOptions, OpenApiHttpMethodTypes: what generateOptions takes, and a method for includeMethods and excludeMethods, like "delete", "DELETE" or "Delete". New in 1.9.3.

OpenApiSpecPoller

The adapter's polling uses this class, which you can also use on its own:

const poller = new OpenApiSpecPoller(url, options?, callbacks?);
OptionTypeDefaultDescription
intervalMsnumber60000Milliseconds between polls.
fetchTimeoutMsnumber10000Milliseconds one fetch may take.
changeDetection"auto" | "etag" | "content-hash""auto"auto and etag send If-None-Match/If-Modified-Since from the last response, so an unchanged spec can answer 304; content-hash always downloads it. All three compare a SHA-256 of the body.
retry{ maxRetries?, initialDelayMs?, maxDelayMs?, backoffMultiplier? }{ 3, 1000, 10000, 2 }Retries within one poll, waiting longer each time.
unhealthyThresholdnumber3Failed polls in a row before onUnhealthy.
headersRecord<string, string>Sent with every poll.
ssrf{ allowedHosts, blockedHosts, allowInternalIPs }private addresses blockedWhich hosts may be polled. The adapter sets it from loadOptions.refResolution.
followRedirectsbooleanfalseFollow redirects, checking each hop.
CallbackCalled
onChanged(spec, hash)With the new spec text, when its hash differs from the last one. The first poll always counts as a change.
onUnchanged()When the spec is the same, or the server answered 304.
onError(error)When a poll fails after its retries, like an OpenAPIFetchError for a 500.
onUnhealthy(failures), onRecovered()After unhealthyThreshold failures in a row, and at the first success after that.

start() polls at once and then every intervalMs; stop() and dispose() stop; poll() polls once; getStats() returns { hash, consecutiveFailures, health: "unknown" | "healthy" | "unhealthy", isRunning }.

Caveats

  • Adapter names are remembered for the whole process. Restarting a server in the same process, as tests and hot reload do, fails with Duplicate adapter name, even though the first server is gone. Create the init() record once, at module level, and reuse it.
  • Arguments are checked against the input schema before anything is sent, except format, and oneOf is read as anyOf. See Arguments are checked against the spec.
  • init() builds the adapter when your module is imported, and the spec is read when the server starts. A spec that can't be loaded or doesn't validate stops the server.
  • init({ name, inject, useFactory }) builds the options from providers, when the server starts: useFactory gets the providers inject returns, and returns the options, all but name, which is the one given to init(). It may be async. A factory that returns something else stops the server with Invalid adapter '…'. Expected useFactory to return the adapter's options object. Changed in 1.9: a server with such a record didn't start, and a second error escaped as an unhandled rejection, which ended a Node process that didn't handle it.
  • In an ES module project, @frontmcp/adapters loads and runs as it does in CommonJS.

Usage

Turning an OpenAPI spec into tools

Give the adapter a name, the API's address and its spec. Each operation becomes a tool, named by its operationId, whose input has one property per parameter and body field:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec })],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Open the Capabilities tab to see the three tools as a client lists them. The model gets the API's status and body as they are; the sections below change what it sees.

Handling what the API answers

A 4xx or 5xx answer becomes an error result that still carries the API's status and body, so the model can tell a missing ticket from an outage. Arguments that don't match the spec never get that far:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec, requestTimeoutMs: 200 })],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A refused call is an INVALID_INPUT error whose message says which argument is wrong and why, so the model can correct it and call again; postToolTransforms doesn't run, since there's no answer. Arguments are checked against the spec lists what the check covers. The last two tests show what happens to a spec's default. The required format has one, so the check accepts a call without it, and the adapter sends format=csv. The optional limit isn't sent when the model leaves it out: the API applies its own default. Before 1.9.3 the adapter didn't send the default, and a call without format failed with TOOL_EXECUTION_ERROR.

Choosing which operations become tools

Most APIs have operations the model shouldn't call. Name the ones to keep, or decide from the operation:

Selecting operations

Example 1 of 3

By operationId

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      generateOptions: { includeOperations: ["listTickets", "getTicket"] },
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

includeOperations names operations by operationId, so GET /admin/stats, which has none, is left out; before 1.9.2 it got through as get_admin_stats. filterFn sees every operation. The tag, method and path filters, and readOnlyOnly, see every operation too, and can be combined: an operation must pass each one. Changed in 1.9: they were accepted and had no effect.

Naming and describing the tools

An operationId is often a poor tool name, and a summary a thin description. toolTransforms rewrites them, corrects the annotations the adapter guesses from each method, and can hide a tool from tools/list:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      descriptionMode: "combined",
      toolTransforms: {
        perTool: {
          getTicket: { name: "get_ticket" },
          listTickets: { name: "list_tickets", description: (d) => `${d} Use status to filter.` },
          // Closing a closed ticket changes nothing
          closeTicket: { name: "close_ticket", annotations: { idempotentHint: true } },
          deleteTicket: { name: "delete_ticket", hideFromDiscovery: true },
        },
      },
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

perTool is keyed by the name the tool had before the transform. Annotations are built in layers: the guess from the method, then the spec's x-frontmcp, then toolTransforms. closeTicket is a POST, so it's guessed destructive and not idempotent; its x-frontmcp says closing destroys nothing, and perTool that closing twice changes nothing. Each tool's title is its summary, and getTicket's x-frontmcp gives it icons and a _meta entry. To change the spec's words without editing the spec file, which may be regenerated, apply an overlay with loadOptions.overlays, as the overlay test does.

A hideFromDiscovery tool answers anyone who knows its name, so hiding isn't a way to protect an operation: leave it out with generateOptions instead, or require authorities.

Sending the API's credentials

When one account on the API serves every caller, give its credentials as staticAuth. The adapter puts each one where the spec's security scheme says:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      // In a real server: process.env.DESK_API_TOKEN and process.env.DESK_REPORTS_KEY
      staticAuth: { jwt: "desk-service-token", apiKey: "reports-key-1" },
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

When the API has an account per user or per tenant, pick the credential from the caller. ctx.authInfo.user holds the claims of the caller's token (see authentication modes). This server accepts tokens from an identity provider, and each tenant has its own API token; the tests call as two users, with tokens signed ahead of time:

Open
import "./desk.example";
import { App, FrontMcp } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

// In a real server these come from a secret store
const deskTokens: Record<string, string> = { acme: "desk-token-acme" };

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      securityResolver: (tool, ctx) => {
        // The claims of the caller's token; tenant is a claim this provider adds
        const tenant = (ctx.authInfo?.user as { tenant?: string } | undefined)?.tenant;
        return tenant && deskTokens[tenant] ? { jwt: deskTokens[tenant] } : {};
      },
    }),
  ],
})
class DeskApp {}

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [DeskApp],
  auth: {
    mode: "transparent" as const,
    provider: "https://auth.example.com",
    expectedAudience: "https://desk.example.com",
    allowAnonymous: true,
    anonymousScopes: ["tickets:read"],
    // The provider's public key, so the example runs offline. Usually FrontMCP fetches it.
    providerConfig: { jwks: { keys: [{ kty: "EC", crv: "P-256", kid: "desk-1", alg: "ES256", use: "sig", x: "1CjT9Do3B6PgOMVsIY0Amz_l1QbuUfoNLcq3E1OFip0", y: "4Je6s1T1RHT7obF4lowUe-63m0pat0TZfmnRxLfDCE8" }] } },
  },
};

@FrontMcp(config)
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

securityResolver returns {} when it has nothing for the caller, and the call fails without reaching the API. authProviderMapper works the same way, one function per scheme: a function that returns undefined gives the call no credential, and it fails. Two options give the adapter somewhere else to look: staticAuth, a shared account for the callers the mapper has nothing for, and passthroughCallerToken, the caller's own token:

When the mapper has nothing for the caller

Example 1 of 3

Refused

Open
import "./desk.example";
import { App, FrontMcp } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

const deskTokens: Record<string, string> = { acme: "desk-token-acme" };

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      // Sam has no tenant, so this returns undefined for him
      authProviderMapper: { DeskToken: (ctx) => deskTokens[(ctx.authInfo?.user as { tenant?: string } | undefined)?.tenant ?? ""] },
    }),
  ],
})
class DeskApp {}

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [DeskApp],
  auth: {
    mode: "transparent" as const,
    provider: "https://auth.example.com",
    expectedAudience: "https://desk.example.com",
    allowAnonymous: true,
    anonymousScopes: ["tickets:read"],
    providerConfig: { jwks: { keys: [{ kty: "EC", crv: "P-256", kid: "desk-1", alg: "ES256", use: "sig", x: "1CjT9Do3B6PgOMVsIY0Amz_l1QbuUfoNLcq3E1OFip0", y: "4Je6s1T1RHT7obF4lowUe-63m0pat0TZfmnRxLfDCE8" }] } },
  },
};

@FrontMcp(config)
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Use securityResolver or authProviderMapper for per-caller credentials, and staticAuth for a service account. passthroughCallerToken only fills HTTP bearer schemes, with the caller's token as jwt. An API key gets nothing from it, so ReportsKey needs a source of its own: without one the server doesn't start, and an API-key call for a caller the mapper has nothing for is refused, with nothing sent.

A credential can also come from a header you set, or from the model. The check before each call looks at the request as it will be sent, so a scheme's header that additionalHeaders or headersMapper sets counts as its credential, and the scheme needs no authProviderMapper function. Credentials the model sends, for schemes in securitySchemesInInput, are sent too, unless the server has one of its own:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      // In a real server: process.env.DESK_API_TOKEN and process.env.DESK_REPORTS_KEY
      authProviderMapper: { DeskToken: () => "desk-service-token" },
      additionalHeaders: { "X-Reports-Key": "reports-key-1" }, // ReportsKey's credential: no mapper function needed
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A header you set is sent for every operation, secured or not, so an API that doesn't expect X-Reports-Key on /tickets still gets it. headersMapper can set a per-caller credential, like a tenant's key, the same way. A credential the model provides is one it can be talked into sending, from whatever it has read, which is why securitySchemesInInput is rated HIGH: prefer a source on the server.

Filling in inputs on the server

Some parameters shouldn't be the model's choice: a tenant id, a request id for tracing, who created a record. inputTransforms removes an argument from the tool's input and computes it on every call; headersMapper and bodyMapper add headers and body fields the spec doesn't declare:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      inputTransforms: {
        global: [{ inputKey: "X-Request-Id", inject: ({ ctx }) => ctx.requestId }],
        perTool: { createTicket: [{ inputKey: "channel", inject: () => "mcp" }] },
      },
      headersMapper: (ctx, headers) => {
        headers.set("x-trace-id", ctx.traceContext.traceId);
        return headers;
      },
      bodyMapper: (ctx, body) => ({ ...body, receivedAt: "2026-09-27T09:00:00Z" }),
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

inputTransforms also works for arguments the spec requires: here both X-Request-Id and channel are required, and the model is asked for neither. They're the server's: a value the model sends for one anyway is dropped before the argument check, and the injected value is sent. In 1.9.2 such a call was refused with Unrecognized key: "channel". inject gets the request's context, so a transform can read the caller's claims from ctx.authInfo.user too.

Header parameters

A header parameter the model does fill in is an argument like the others, marked x-mcp-header in the input schema. Under the 2026-07-28 protocol that marker means the client must send the value twice: in the arguments, and as an Mcp-Param-<name> HTTP header, so proxies can route on it without reading the body. A call without the header is refused before the tool runs:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

@App({
  id: "desk",
  name: "Desk",
  adapters: [OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec })],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A 2026-07-28 client is expected to read x-mcp-header from tools/list and add the header. The Playground's test client doesn't, so the second test gets the error such a client would. Credentials that securitySchemesInInput or includeSecurityInInput put in the input are marked x-mcp-header too, with the scheme's header name (Authorization for a bearer token). When the value isn't the model's to choose, remove the parameter with inputTransforms, and the question doesn't arise.

Reshaping what the model gets back

APIs return fields the model doesn't need, or shouldn't see. postToolTransforms changes data on every call; outputSchema can move the response's description into the tool's description, for clients that ignore outputSchema:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";

type Ticket = { id: string; title: string; status: string; internal_notes?: string; assignee_email?: string };
const publicFields = ({ id, title, status }: Ticket) => ({ id, title, status });

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://desk.example/v1",
      spec,
      outputSchema: { mode: "description" },
      dataTransforms: {
        postToolTransforms: {
          perTool: {
            getTicket: { transform: (data) => publicFields(data as Ticket), filter: ({ ok }) => ok },
            listTickets: { transform: (data) => ({ count: (data as Ticket[]).length, tickets: (data as Ticket[]).map(publicFields) }) },
          },
        },
      },
    }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The description still describes the API's ticket, not what the transform returns; when a transform changes the shape, say so with toolTransforms or preToolTransforms.transformDescription.

Two APIs in one app

Each adapter needs its own name. When both specs have an operation with the same operationId, FrontMCP lists both tools with the adapter's name in front:

Open
import "./apis.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { specFor } from "./specs";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec: specFor("Desk") }),
    OpenapiAdapter.init({ name: "billing", baseUrl: "https://billing.example/v2", spec: specFor("Billing") }),
  ],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The prefix appears only while the names clash: remove one adapter and the other's tool is getStatus again. To keep names stable, give them a prefix yourself: toolTransforms: { global: { name: (n) => "billing_" + n } }.

Adding security the spec doesn't declare

Specs often leave out security, even when the API wants a token. forceJwtSecurity() returns a copy that requires a bearer token (or an API key, or basic credentials) on every operation, and removeSecurityFromOperations() takes it off again where the API is public:

Open
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter, forceJwtSecurity, removeSecurityFromOperations } from "@frontmcp/adapters";
import { spec } from "./spec";

const secured = removeSecurityFromOperations(forceJwtSecurity(spec as any), ["getHealth"]);

@App({
  id: "desk",
  name: "Desk",
  adapters: [OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec: secured, staticAuth: { jwt: "desk-service-token" } })],
})
export class DeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Loading the spec from a URL

With url instead of spec, the adapter downloads the spec when the server starts. The Playground can't show it: in Node the download uses Node's own HTTP client, which checks the address it connects to, not fetch.

desk.app.ts
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";

@App({
  id: "desk",
  name: "Desk",
  adapters: [
    OpenapiAdapter.init({
      name: "desk",
      baseUrl: "https://api.desk.example/v1",
      url: "https://api.desk.example/openapi.json",
      loadOptions: {
        headers: { authorization: `Bearer ${process.env.DESK_SPEC_TOKEN}` },
        timeout: 10_000,
      },
      staticAuth: { jwt: process.env.DESK_API_TOKEN },
    }),
  ],
})
export class DeskApp {}
  • If the spec can't be downloaded or doesn't validate, the server doesn't start.
  • The spec's host must resolve to a public address. http://127.0.0.1:4010/openapi.json, a spec served by your own mock server, stops the server with Host "127.0.0.1" maps to a blocked internal address unless you set loadOptions: { refResolution: { allowInternalIPs: true } }.
  • A redirect isn't followed unless loadOptions.followRedirects is true.
  • url must be an http: or https: address. A file path, which its type's comment suggests, fails with Invalid spec URL: ./openapi.json. To load a local file, import the JSON and pass it as spec.

Picking up spec changes

With polling, the adapter downloads the spec again every intervalMs and rebuilds its tools when it changed, so a new operation becomes a tool without a restart:

OpenapiAdapter.init({
  name: "desk",
  baseUrl: "https://api.desk.example/v1",
  url: "https://api.desk.example/openapi.json",
  loadOptions: { headers: { authorization: `Bearer ${process.env.DESK_SPEC_TOKEN}` } },
  polling: {
    enabled: true,
    intervalMs: 60_000,
    // Polls don't send loadOptions.headers
    headers: { authorization: `Bearer ${process.env.DESK_SPEC_TOKEN}` },
  },
});

Checked in Node against a local server:

  • FrontMCP starts polling once the adapter's first tools are registered. The first poll always counts as a change, so the tools are built twice at startup.
  • When the spec changes, the adapter downloads it again (with loadOptions.headers), rebuilds every tool, and replaces the app's tools from this adapter; the next tools/list has the new ones. Rebuilds run one at a time. If a rebuild fails, the old tools stay and the error is logged.
  • With changeDetection: "auto" (the default) and a server that sends an ETag, an unchanged spec costs a 304.
  • Poll errors are logged as warnings, and unhealthyThreshold failures in a row as an error; the tools keep working.
  • Nothing stops the poller when the server shuts down. Keep the record and call record.useValue.stopPolling() on shutdown.

To watch a spec without an adapter, for a deploy check say, use OpenApiSpecPoller directly:

import { OpenApiSpecPoller } from "@frontmcp/adapters";

const poller = new OpenApiSpecPoller(
  "https://api.desk.example/openapi.json",
  { intervalMs: 300_000, headers: { authorization: `Bearer ${process.env.DESK_SPEC_TOKEN}` } },
  {
    onChanged: (spec, hash) => console.log(`Desk API spec changed: ${hash.slice(0, 12)}`),
    onUnhealthy: (failures) => console.error(`Desk API spec unreachable ${failures} times in a row`),
  },
);
poller.start();

Troubleshooting

Duplicate adapter name 'x' for OpenapiAdapter

Two OpenapiAdapter.init() calls used the same name in one process. Give each adapter its own name. If you only have one, init() ran twice: a server restarted in the same process (tests, hot reload), or a function that builds the config was called again. Call init() once at module level and reuse the record. The Playground starts each run with fresh names, so it doesn't hit this.

Adapter OpenapiAdapter.init() requires a non-empty 'name' option

name is missing or empty. init() with no argument, or with init({}), reaches this error too. (Before 1.8.7, init() with no argument failed with Cannot read properties of undefined (reading 'name').)

Invalid security configuration. Missing auth provider mappings for security schemes: …

You set authProviderMapper, and the spec uses a security scheme it has no function for, and no other source covers. The server doesn't start. Add a function for each scheme listed (it gets the request's context: (ctx) => …), set staticAuth for the schemes one account covers, set the scheme's header with additionalHeaders or headersMapper, or put the scheme in securitySchemesInInput if the model should provide it. passthroughCallerToken: true covers HTTP bearer schemes only, and the message suggests it only for those: an API key isn't filled by the caller's token.

Authentication required for tool '…': no credential for its security schemes.

The operation requires credentials, and the request, as it would have been sent, carries none for any of its schemes, listed on the next line (Required security schemes: ReportsKey (apiKey)). Nothing was sent. Set staticAuth, return credentials for this caller from securityResolver or authProviderMapper, or set the scheme's header with additionalHeaders or headersMapper. For a caller you have no API credentials for, this is the error they get, which is what you want. Check what counts for the scheme in how credentials are chosen: an Authorization header must start with Bearer (or Basic), and passthroughCallerToken fills only HTTP bearer schemes. The adapter doesn't fall back to the caller's own token: if the API accepts your server's tokens, set passthroughCallerToken: true, and read its pitfall first.

Tool name collision: "…" produced by 2 operations

Two operations of one spec got the same tool name, usually from a toolTransforms rename or a toolNameGenerator. The server doesn't start. Rename one with toolTransforms.perTool.

Header mismatch: Mcp-Param-… header is required for argument '…'

JSON-RPC error -32020. The tool has a header parameter, and a 2026-07-28 client sent its value in the arguments without the Mcp-Param- header. Update the client, or fill the parameter in on the server with inputTransforms.

Invalid arguments for tool '…': …

Code INVALID_INPUT. The model's arguments don't match the operation's schema in the spec, and nothing was sent; the message names each argument and what's wrong with it. A model reads it and calls again. If it keeps sending the same wrong value, say what's allowed in the description, or check the spec: an enum or a range that's stricter than the API refuses values the API would take. On FrontMCP 1.9.2, Unrecognized key for an argument you fill in with inputTransforms meant the model sent it anyway; 1.9.3 drops the model's value. See Arguments are checked against the spec.

Required query parameter '…' (input: '…') is missing for operation '…'

A TOOL_EXECUTION_ERROR, or Required body parameter '…' for a field of the body. The spec requires the parameter, an inputTransforms entry fills it in, and its inject returned undefined, so there was nothing to send. The spec's default doesn't apply to an argument a transform fills in. Return a value from inject. On FrontMCP 1.9.2 a required parameter with a default that the model left out failed here too: update, and the default is sent. (Before 1.9.2, every missing required parameter failed here.)

The API receives values that aren't in the spec

Only values the argument check doesn't cover get through: a string that doesn't match its format, and a value that matches more than one branch of a oneOf. On FrontMCP before 1.9.2, arguments weren't checked at all: update.

generateOptions.excludeMethods lists "…", which is not an HTTP method

OpenapiAdapter.init() throws for a name in includeMethods or excludeMethods that isn't get, post, put, patch, delete, head, options or trace, in any case.

Unknown field '…' in x-frontmcp (will be ignored)

An operation's x-frontmcp has a field the adapter doesn't know, and the tool doesn't get it. The x-frontmcp extension lists the fields it knows. On FrontMCP 1.9.2 icons and meta got this warning too, though the tool listed them: the warning could be left alone.

The API receives the MCP client's token

The adapter has passthroughCallerToken: true, the operation uses an HTTP bearer scheme, and neither authProviderMapper nor staticAuth gave that call a credential. Without the option, the caller's token is never sent. See how credentials are chosen.

Invalid OpenAPI document

The spec didn't validate. A Swagger 2.0 document fails with Missing required field: openapi: convert it to OpenAPI 3 first. To build tools from a spec that's valid enough but fails the check, set loadOptions: { validate: false }.

A $ref stays unresolved in a tool's schema

It points outside the document, and external references aren't fetched by default. Allow its host with loadOptions.refResolution, or bundle the spec into one file.

Host "…" maps to a blocked internal address

The spec url (or an external $ref) is on a private network or loopback. Set loadOptions: { refResolution: { allowInternalIPs: true } } if you trust it, or pass the spec as spec.

Polling requires URL-based options (use 'url' instead of 'spec').

polling needs url: there's nothing to poll with an in-memory spec.

Invalid adapter '…'. Expected useFactory to return the adapter's options object.

An adapter registered with init({ name, inject, useFactory }) has a factory that returned something other than the options: undefined, say, from a function that forgot to return. The server doesn't start. See Caveats. Cannot read properties of undefined (reading 'name') for the same record is FrontMCP before 1.9.0, where useFactory didn't work: update, or build the options at module level and pass them to init() directly.

Options in generateOptions do nothing

On FrontMCP before 1.9.0, includeTags, readOnlyOnly, descriptionStrategy, maxToolNameLength and the other fields listed under generateOptions weren't passed on, and before 1.9.2 inferAnnotations, emitMeta and icons didn't reach the tools: update. Then check the values: paths are globs, where * stays within one segment, and includeOperations leaves out operations without an operationId.