Converting OpenAPI Specs to Agent Tools

Before we dive in, let’s clarify what we mean by “agent tools”. Agent tools are local functions whose descriptions and parameters are exposed to an agent. The agent can then supply all the parameters to perform a “tool call”. Then, the agent framework (CrewAI, AWS Strands, Langchain, etc.) orchestrates running the tool function with those […]

Engineering Blog

Before we dive in, let’s clarify what we mean by “agent tools”. Agent tools are local functions whose descriptions and parameters are exposed to an agent. The agent can then supply all the parameters to perform a “tool call”. Then, the agent framework (CrewAI, AWS Strands, Langchain, etc.) orchestrates running the tool function with those agent supplied parameters. Many agent tools wrap remote APIs and can bring incredible new capabilities to your agent.

But there’s a scaling problem: you can only write and maintain so many full tool implementations locally before things get unwieldy. That’s where the Model Context Protocol (MCP) comes in. This protocol allows agents to connect to external tools that get executed on a remote server, rather than locally. The MCP server lists its tools, the agent consumes them, and everyone’s happy.

But here’s the catch—services that your agent needs may not have invested the time to develop an MCP server just yet.

Instead, they’ll give you what they already have: an OpenAPI spec or some documentation that describes how to use their APIs. This is what developers have used for 25+ years (15+ in the case of OpenAPI specs) to understand how to communicate with services.

So the question becomes: how do we bridge the gap between traditional APIs and local tools that an agent can call? That’s what this post is about.

The Context: Why Conversion of OpenAPI Specs to Tools Matters

Imagine you are developing a personal agent platform, where users can create agents for different workflows. You’re providing your end-users with the capability to manage their calendar and send emails via agents. Now, you aim to enhance your agent platform by offering flight reservations. However, the flight reservation distributors you’ve partnered with utilize a standard REST API with an OpenAPI specification, rather than an MCP server.

How are the agents on your agent platform going to book flights for your users?

That’s where converting OpenAPI specs directly into local tools for agents comes in.

This one capability dramatically expands an agent’s action space. Instead of being limited to MCP servers and a handful of built-in tools, an agent can now call all the APIs that use OpenAPI specs. In short, your agent platform just got a lot more useful.

And while it’s possible to convert OpenAPI specs into MCP servers, having agents work with OpenAPI specs directly eliminates the need to maintain and monitor an ever-growing number of MCP servers.

Now that we’ve asserted the value in giving agents the ability to work with OpenAPI specs, let’s see how this can be done in practice.

The High-Level Approach

At a high level, the process for turning OpenAPI specs into agent tools looks like this:

  1. Agent fetches one or more of the OpenAPI specs and stores it to a list of OpenAPI specs it wants to connect with (e.g. using a convert_to_tools() call)
  2. Each endpoint in the OpenAPI spec is converted into a tool definition compatible with your agent SDK
  3. Those tools are added to the agent’s toolkit.
  4. The agent is restarted (or hot-reloaded) with the new tools available.

Depending on the framework being used, the implementation details differ, but the core principle remains the same – “make the OpenAPI specs available as agent-understandable tools”

Technicals of OpenAPI Spec to Tool Conversion

OpenAPIs

Let’s focus on really understanding how the conversion process from OpenAPI endpoint to agent tool works (step 2 from the above checklist).

To demonstrate how OpenAPIs can be converted into agent tools, let’s use an OpenAPI spec from BuildShip, a platform for creating AI agent workflows. We’ll be using a specific Buildship tool called companyResearch— a service defined by an OpenAPI spec that enables agents to perform company research. Here’s its specification:

{
  "openapi": "3.0.0",
  "info": {
    "title": "Buildship Generated API",
    "version": "1.0.0",
    "description": "API for Buildship Managed tools"
  },
  "servers": [
    {
      "url": "https://ct7rdx.buildship.run"
    }
  ],
  "parameters": [
    {
      "name": "skyfire_kya_pay_token",
      "in": "header",
      "description": "The Skyfire KYA pay token",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "paths": {
    "/executeTool/U40tJouoY9wAaIhk8Z37/22e5a0a4-5ead-442b-ab12-4ece693ca2d9": {
      "post": {
        "summary": "companyResearcher",
        "description": "Get structured company information from an email address or domain. Accepts an email or domain, extracts the domain, scrapes the company website, and searches the web for verifiable details, returning a JSON summary with name, website, description, industry, location, size, and contact info.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "string"
                ],
                "properties": {
                  "emailOrDomain": {
                    "properties": {

                    },
                    "type": "string",
                    "title": "emailOrDomain"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "output": {
                      "title": "Output",
                      "properties": {

                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

The most important parts of any OpenAPI spec are the servers, parameters, paths, methods, and request body. Let’s break down each of these crucial parts of an OpenAPI spec:

  • Servers: Define base URLs for different API environments.
  • Parameters: Inputs for API operations, located in the path, query, header, or cookie.
  • Paths: Define API endpoints and the HTTP methods allowed on them.
  • Methods (Operations): Detail specific API operations for each path, including summaries, unique IDs, parameters, request bodies, and responses.
  • Request Body: Defines the structure and type of data sent to the server for operations like POST, PUT, or PATCH.

By integrating all of this information into a tool call, we can convert an OpenAPI specification into an agent tool.

Vercel Tools

Now, let’s examine what the Vercel AI SDK would expect a companyResearcher() tool to look like.

export const companyResearcherTool = {
  "companyResearcher": {
    description:
      "Get structured company information from an email address or domain. Accepts an email or domain, extracts the domain, scrapes the company website, and searches the web for verifiable details, returning a JSON summary with name, website, description, industry, location, size, and contact info.",
    parameters: jsonSchema({
      type: "object",
      properties: {
        emailOrDomain: {
          type: "string",
          description:
            "email or domain",
        },
      },
      required: ["emailOrDomain"],
      additionalProperties: false,
    }),
    execute: this.createExecuteFunction(path, method, parameters, operation),
  },
};

The tool has three main parts:

  • description: summary of what the tools does
  • parameters: the input schema the tool accepts
  • execute: the function that runs the operation

You can already see the resemblance to the original OpenAPI spec, but it’s no trivial matter to map between the two.

To handle this translation, we’ll create a Typescript class that will process the OpenAPI spec into the corresponding AI SDK tool call.

export class OpenAPIToTools {
  private spec: OpenAPIV3_1.Document;
  private baseUrl: string;
  private apiKey: string;
  private securitySchemes: Record<string, SecurityScheme>;
  private toolNames: Record<string, string>;

  constructor(spec: OpenAPIV3_1.Document, apiKey: string = "") {
    this.spec = spec;
    this.baseUrl = this.getBaseUrl();
    this.apiKey = apiKey;
    this.securitySchemes = this.extractSecuritySchemes();
    this.toolNames = this.extractToolNames();
  }

The class defines a couple of private variables (the OpenAPI spec, baseUrl, apiKey if provided, securitySchemes, and toolNames) and in its constructor initializes these private variables by parsing the OpenAPI spec (constructor helper function code). The security schemes represent the types of authentication (api key / bearer token authentication / Oauth) required by the API server.

In this case, authentication is handled more simply: instead of a traditional API key, we pass a skyfire_kya_pay_token custom header in the request. This token encodes identity and payment information that the Buildship server can validate and charge against, circumventing the need for a typical Buildship API key.

Once the private class members and constructor are set up, we define a public method generateTools(). This method iterates through all the paths, methods, and operations defined in the OpenAPI spec and outputs a dictionary of AI SDK-compatible tool definitions (the full source code can be found at the end).

public generateTools(): Tools {
    const tools: Tools = {};

    // Iterate through all paths and methods of the spec
    Object.entries(this.spec.paths || {}).forEach(([path, pathItem]) => {
      if (!pathItem) return;

      // Handle each HTTP method (get, post, etc.)
      const methods = ['get', 'post', 'put', 'delete', 'patch'] as const;

      methods.forEach(method => {
        const operation = pathItem[method];
        if (!operation) return;

        // Generate the tool names from the path, method, operation
        const toolName = this.generateToolName(path, method, operation);
        const parameters = [
          ...(pathItem.parameters || []),
          ...(operation.parameters || []),
        ] as OpenAPIV3_1.ParameterObject[];

        // Get request body
        const requestBody = operation.requestBody;

        // Create tool description
        let description = operation.summary || operation.description || `${method.toUpperCase()} ${path}`;
        description += " Requires skyfire_kya_pay_token parameter for authentication.";

        // Get tool schema in format requested by Vercel AI SDK
        const toolSchema = this.convertParametersToJsonSchema(parameters, requestBody as OpenAPIV3_1.RequestBodyObject);

        // Return dictionary of tool names mapping to tool definition
        tools[toolName] = {
          description,
          parameters: toolSchema,
          execute: this.createExecuteFunction(path, method, parameters, operation),
        };
      });
    });

    return tools;
  }

At the end of this function, we end up with a Vercel AI SDK-ready tool dictionary. Each tool contains a description of what the tool does, the parameters that the tool takes as input, and an execution function that calls the endpoint using the correct method and request format.

That execute function, createExecuteFunction(),  is the glue that makes the tool actually work. It takes the path, method, parameters, and operations from the OpenAPI spec and builds a callable function the SDK can use to perform the real API request.

The function created by createExecuteFunction() will accept the API request body arguments as input parameters.

In the specific case of this Buildship tool, the only request body argument needed is the emailOrDomain,which is an email or domain of the company in question.

Here is how createExecuteFunction works:

  private createExecuteFunction(
    path: string,
    method: string,
    parameters: OpenAPIV3_1.ParameterObject[],
    operation: OpenAPIV3_1.OperationObject
  ) {
    return async (args: Record<string, any>) => {
      const toolName = this.generateToolName(path, method, operation);

      try {
        let url = this.baseUrl + path;
        const queryParams = new URLSearchParams();

        // Get security headers based on operation if any at all
        const headers = this.getSecurityHeaders(operation);

        // Handle skyfire_kya_pay_token parameter - add to headers
        if (args.skyfire_kya_pay_token) {
          headers['skyfire_kya_pay_token'] = args.skyfire_kya_pay_token;
        }

        // Handle parameters -- separate out 3 types
        parameters.forEach(param => {
          const paramName = param.name;
          const paramIn = param.in;

          if (paramName in args) {
            if (paramIn === 'path') {
              url = url.replace(`{${paramName}}`, String(args[paramName]));
            } else if (paramIn === 'query') {
              queryParams.append(paramName, String(args[paramName]));
            } else if (paramIn === 'header') {
              headers[paramName] = String(args[paramName]);
            }
          }
        });

        if (queryParams.toString()) {
          url += `?${queryParams.toString()}`;
        }

        // Handle request body for POST/PUT/PATCH methods
        let body: string | undefined;
        if (['post', 'put', 'patch'].includes(method.toLowerCase())) {
          // Extract request body parameters (exclude path, query, header params and headers)
          const bodyParams: Record<string, any> = {};
          const paramNames = parameters.map(p => p.name);

          Object.entries(args).forEach(([key, value]) => {
            if (!paramNames.includes(key) && key !== 'skyfire_kya_pay_token' && value !== undefined) {
              bodyParams[key] = value;
            }
          });

          if (Object.keys(bodyParams).length > 0) {
            body = JSON.stringify(bodyParams);
          }
        }

        // Make the fetch API request 
        const response = await fetch(url, {
          method: method.toUpperCase(),
          headers,
          body,
        });

        if (!response.ok) {
          let errorMessage: string;
          let fullErrorData: any = null;

          try {
            const errorData = await response.json();
            fullErrorData = errorData;
            errorMessage = errorData.error || response.statusText;
          } catch {
            errorMessage = response.statusText;
          }

          // Enhanced error logging with full details
          logHttpError(`HTTP error in ${toolName}:`, response, {
            url: url.toString(),
            method: method.toUpperCase(),
            headers: headers,
            body: body
          }, fullErrorData);

          throw new Error(`HTTP error! status: ${response.status}, message: ${errorMessage}`);
        }

        const contentType = response.headers.get('content-type');
        let responseData;
        if (contentType?.includes('application/json')) {
          responseData = await response.json();
        } else {
          responseData = await response.text();
        }
        
        // return API result in format expected by tool call
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(responseData, null, 2),
            },
          ],
        };

      } catch (error) {
        logError(`Error in tool execution (${toolName}):`, error, 'toolExecution', {
          toolName: toolName
        });

        return {
          content: [
            {
              type: "text",
              text: `Error: ${getErrorMessage(error)}`,
            },
          ],
        };
      }
    };
  }

createExecuteFunction is essentially the function factory responsible for creating the execution functions enclosed at the end of each AI SDK tool definition.

So, putting this all together, when we call the converter class on an OpenAPI spec, it is parsing through the spec, creating Vercel AI tool definitions, and creating the execution functions for those tools which actually run the assembled API calls.

    const converter = new OpenAPIToTools(
      postSpecResponse,
      agentContext.openApiSpecs[j].authHeader // optional api key / auth header
    );
    const openApiTools = converter.generateTools(); // contains post_comanyReseacher

Now, with a simple invocation of our generateTools() method, the OpenAPI has been transformed into tools ready for our agent to call. We’ve gone from an original API call of [Method] [base url]/[path] to a simple agent tool call.

Before 
POST https://ct7rdx.buildship.run/executeTool/U40tJouoY9wAaIhk8Z37/22e5a0a4-5ead-442b-ab12-4ece693ca2d

After
agent tool
post_companyResearch(emailOrDomain: str) AI SDK tool

Now that we’ve covered the technical details of how to programmatically convert an OpenAPI spec into a tool call, let’s look at how an agent can actually use it. Below is a full example of a Vercel AI SDK agent that integrates OpenAPI specifications to extend its capabilities.

Using OpenAPIs with a Vercel AI SDK Agent

Step one of building this agent requires a tool like convert-openapi-spec-to-agent-tool that the agent will call when it wants to convert an OpenAPI spec it’s discovered. This tool collects the OpenAPI spec URL and service name as part of the tool call arguments.

export const convertOpenApiSpecToAgentTool = {
    "convert-openapi-spec-to-agent-tool": {
        description: "Gets the OpenAPI spec URL prompted by the user. Stop execution after this tool",
        parameters: jsonSchema({
        type: "object",
        properties: {
            openApiSpecUrl: {
            type: "string",
            description: "URL for OpenAPI spec - ends in a .json",
            },
            serviceName: {
            type: "string",
            description: "Name of the service corresponding to the OpenAPI spec",
            }
        },
        required: ["openApiSpecUrl", "serviceName"],
        additionalProperties: false,
        }),
        execute: async () => {
        return {
          content: [
            {
              text:  "Converting OpenAPI spec to tools...",
            },
          ],
        };
        },
    },

However, despite its name, this tool is not actually responsible for converting anything. We can clearly see that it only returns a string “Converting OpenAPI spec to tools…” in the execution function. This tool is more of a placeholder to signal to the agent that it needs to shutdown, while also conveniently storing the OpenAPI spec url in the tool call inputs.

The magic of spec to tool conversion will have to take place while the agent is asleep in between restarts. This shutdown mechanism is necessary because, at least in the Vercel AI SDK, agent tools are fixed during runtime. You cannot add more tools to your agent’s toolkit as it is running and as a result need to restart your agent with the new tools. The same static toolset problem can be found across many other agentic frameworks, so this approach serves as an extensible and practical solution until framework toolsets become more mutable.

To enforce the shutdown we instruct the agent to turn off after making the convert-openapi-spec-to-agent-tool tool call in the agent’s system prompt.

<terminate>
You can execute multiple convert-openapi-spec-to-agent-tool calls in sequence, but after all OpenAPI conversions are complete, stop processing.
</terminate>

So after making the convert-openapi-spec-to-agent-tool tool call, the agent will stop its execution.

Then, we iterate through all the tool calls the agent made and we check to see whether it made a call with convert-openapi-spec-to-agent-tool before ending execution. If so, we extract out the OpenAPI spec that was passed as the tool argument and append it to a list of OpenAPIs to connect to.

Then, using the converter class we talked about at length above, the OpenAPIs are turned into tools which are then added into the agent toolkit before the agent restarts.

 let allTools = { 
    ...convertOpenApiSpecToAgentTool
  };

  // iterate through all openapi specs
  for (let j = 0; j < agentContext.openApiSpecs?.length; j++) {
    const postSpec = await fetch(agentContext.openApiSpecs[j].url);
    const postSpecResponse = await postSpec.json();

    const converter = new OpenAPIToTools(
      postSpecResponse,
      agentContext.openApiSpecs[j].authHeader || "" // instantiate the spec converter
    );
    const openApiTools = converter.generateTools(); // extract the tools from spec
    console.log(
      `✅ Processed OpenAPI spec ${j + 1}, generated ${
        Object.keys(openApiTools).length
      } tools`
    );
    allTools = { ...allTools, ...openApiTools }; // add them to toolkit
    }

Now, upon rebooting, your agent can use these OpenAPI specs as if they were native tools it had access to all along. Here is the complete OpenAPIToTools class implementation for formalizing an OpenAPI endpoint into a Vercel AI SDK tool if you’d like to take a closer look.

An Alternative Approach: Two-Tool Pattern

If you don’t want to start/stop your agent or create and load new tools for each OpenAPI spec, you can still take advantage of OpenAPI specs with a two-tool pattern:

  1. Tool A: This tool fetches the OpenAPI spec with a simple GET request. The OpenAPI spec is returned from the tool, which loads the spec into the agent’s context window.
  2. Tool B: This tool builds and executes an API call. Given headers, a path, a method, and parameters (supplied by the agent in the tool call arguments), this generic tool builds the HTTP request and executes it.

In this approach, the developer is giving the agent full responsibility for reading the OpenAI spec and writing the correct API call to make. It’s less structured than actually generating a tool, but still opens up the entire API to the agent.

Despite being less structured, the two tool approach can scale better because not every API has an OpenAPI spec. Documentation can take all forms from webpages to Markdown to Postman collections and more. An agent that can fetch and read documentation and then execute the API call correctly is all we are really trying to accomplish here. “Correctly” is a key word here. The approach of letting the agent formulate its own request has a large drawback wherein the agent can malform its request, leading to inconsistency between agent runs. This approach also requires loading the entire OpenAPI spec (or at least its general structure) into the agent’s context window, so that the agent can later correctly write a call to the API. Depending on the size of the OpenAPI spec, this could be a very costly operation, and it could make more sense to run code that will process the spec and turn it into specific tools for the agent to call instead.

Since the agents we showed above demanded reliability and consistent behavior across runs, we chose to restart them with the transformed tools—avoiding the uncertainty of having the agent issue raw API calls itself.

The two general approaches to utilizing external OpenAPI specs both have their benefits and drawbacks. The two tool approach is highly scalable across different types of APIs and different frameworks, while the code to convert OpenAPI specs to agent tools needs to be tailored to each agent framework. However, the two tool approach also introduces more uncertainty in making reliable API calls and adds overhead into the LLM model context, whereas the tools generated by converting the OpenAPI spec are bound to work predictably once tested.

Ultimately, as with almost everything, the correct approach depends on the tradeoffs you need to make for your specific agentic use case. Godspeed developers.

Lessons Learned

Through experimenting with this approach across frameworks, we’ve learnt a few important things:

  • OpenAPI specs are messy. Some are missing servers, some misuse the required keyword, some declare impossible schemas. Build resilience into the way you process OpenAPI specs.
  • Authentication needs handling. Many APIs require tokens, headers, or OAuth flows. Your OpenAPI spec converter should give agents a way to pass custom headers or API keys.
  • Tool naming matters. Clear, short names improve agent reliability. Some LLM models and agent frameworks cannot handle tool names that are too long.

The Takeaway

Converting OpenAPI specs into agent tools is not just a neat hack—it’s a major unlock.

It means your agent is no longer limited to the sliver of services that have MCP servers. It can work with the APIs that power nearly every service on the internet. Plus, integrating Skyfire means the system can also incorporate payment and identity credentials for authenticated service calls or to programmatically pay for API usage.

Yes, MCP is definitely preferable when available, but until every company adopts it, OpenAPI conversion is a pragmatic stopgap. Google, recognizing this fact, has already included an OpenAPI integration into their Agent Development Kit. This simplifies the process of using OpenAPI specs to a couple lines of code and it is likely other agent frameworks will follow suit, allowing agents to interface with both OpenAPIs and MCPs seamlessly.

The real win: you don’t need to wait for the ecosystem to catch up and for more companies to release MCPs to build useful agents! Your agent can start calling APIs today using the approaches outlined in this post!

At Skyfire, we’re actively testing these integrations across Strands, Vercel AI SDK, and more. If you’ve built your own OpenAPI-to-tool converters—or run into weird spec quirks—we’d love to hear about it.

AI merchant service

Agent Tool Calling Chaos when MCP Server Tools have Optional or Nullable Parameters

When building MCP servers, you quickly realize something: the devil really is in the details. One issue we’ve encountered is how differently LLMs and agent frameworks handle optional and nullable parameters in MCP server tools. At first glance, this might sound like a subtle implementation detail. But as we learned—through multiple iterations across OpenAI, Anthropic, […]
Read more

Join Our Community of Innovators

Stay updated with the latest insights and trends in AI payments and identity solutions.