Most agent codebases contain a file nobody enjoys maintaining. It holds forty tool definitions, each one a hand-written JSON schema describing an endpoint that already has a schema somewhere else. The API team ships a new required field, the spec updates, the docs update, and the agent keeps sending the old payload until someone notices the 400s.
You already have a machine-readable description of every endpoint. It is the OpenAPI document. The job is to turn that into tool definitions the model can call, and to keep the two in sync automatically instead of by memory.
This guide covers how OpenAPI operations map to tool schemas, what the generator has to fix along the way, how to trim a 200-endpoint spec down to something a model can reason about, and how to test that the generated tools behave. If you are earlier in the stack, our post on whether you still need an API tool when agents write the code sets the wider context.
Apidog matters here because the spec has to be correct before anything generated from it can be. A tool definition inherits every gap in the document it came from.
The cost of hand-written tool definitions
Hand-writing tools feels fine at five endpoints. It stops being fine somewhere around twenty, for three reasons.
Definitions drift. The spec is generated from code or maintained by the API team. The tool file is maintained by whoever built the agent. Nothing connects them, so they diverge quietly, and the first symptom is an agent that “suddenly” stopped working.
Descriptions get thin. When a person writes forty schemas by hand, the last twenty get one-line descriptions. Models pick tools by reading those descriptions, so thin text directly degrades tool selection. Our post on tool schema design for agents goes deeper into why the wording carries so much weight.
Errors are invisible until runtime. A hand-written schema that says a field is a string when the API wants an integer produces a 422 the first time the agent tries it, in production, on a real task.
Generating from the spec fixes all three at once. There is one source of truth, descriptions come from the same text your docs use, and types come from the same schema the server validates against.
How an OpenAPI operation becomes a tool
The mapping is more direct than it looks. Take a single operation:
paths:
/orders/{orderId}/refund:
post:
operationId: refundOrder
summary: Refund an order
description: >
Issues a full or partial refund against a completed order.
Refunds are irreversible. Partial refunds require an amount
no greater than the remaining refundable balance.
parameters:
- name: orderId
in: path
required: true
schema: { type: string }
description: The order to refund.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [reason]
properties:
amount:
type: integer
description: Amount in cents. Omit for a full refund.
reason:
type: string
enum: [duplicate, fraudulent, requested_by_customer]
The tool definition that comes out of it:
{
"name": "refundOrder",
"description": "Issues a full or partial refund against a completed order. Refunds are irreversible. Partial refunds require an amount no greater than the remaining refundable balance.",
"input_schema": {
"type": "object",
"required": ["orderId", "reason"],
"properties": {
"orderId": { "type": "string", "description": "The order to refund." },
"amount": { "type": "integer", "description": "Amount in cents. Omit for a full refund." },
"reason": { "type": "string", "enum": ["duplicate", "fraudulent", "requested_by_customer"] }
}
}
}
Four rules do most of the work:
operationIdbecomes the tool name. If an operation has nooperationId, generate a stable one from method plus path, and then go add it to the spec.- Path, query, and body parameters flatten into one properties object. The model does not care where a value rides on the wire. Your executor does, so keep a side table that records which parameter goes where.
summaryplusdescriptionbecomes the tool description. Both, joined. The summary alone is usually too terse to guide selection.- Required arrays merge. A required path parameter and a required body field both land in the same
requiredlist.
The executor is the other half, and it is small:
def execute(tool_name, args, spec_index, http):
op = spec_index[tool_name] # method, path template, param locations
path = op.path
query, body = {}, {}
for name, value in args.items():
location = op.locations[name] # "path" | "query" | "header" | "body"
if location == "path":
path = path.replace("{" + name + "}", str(value))
elif location == "query":
query[name] = value
elif location == "body":
body[name] = value
return http.request(op.method, path, params=query, json=body or None)
That is the entire bridge. Everything else is cleanup on the way through.
What the generator has to fix
A naive dump of the spec into tool schemas produces tools that models handle badly. Five adjustments matter.
Resolve $ref pointers. Most tool-calling APIs accept a subset of JSON Schema and will not follow references into a components section. Inline them. Watch for recursive schemas, which inlining will expand forever; cut the recursion at a fixed depth and describe the deeper structure in prose.
Drop unsupported keywords. oneOf, allOf, discriminator, and nullable are common in specs and poorly supported by tool schemas. Collapse allOf by merging properties. For oneOf, either pick the dominant variant or split the operation into two tools, one per shape. That second option usually produces better tool selection anyway.
Flatten deep nesting. A body three levels deep is hard for a model to fill correctly. If your create-order payload nests customer.address.postal_code, consider a flatter tool surface and reassemble the nested shape in the executor.
Prune response schemas. Tool definitions describe inputs. The full response schema does not belong in the definition, and including it wastes context. What the response looks like matters when the result comes back, and that is a separate problem covered in our post on keeping API responses inside the agent’s context window.
Carry the safety flags. Write operations should be marked so your executor can route them through an approval gate. If your spec uses an extension such as x-agent-requires-approval, read it and honor it. Pair this with the patterns in our AI agent guardrails guide.
Do not give the model all 200 endpoints
The biggest practical problem is not conversion. It is volume. A mature API has hundreds of operations, and pasting all of them into the tool list produces two failures at once: the context fills with schemas before the task starts, and selection accuracy drops because the model is choosing among near-identical options.
Three ways to cut it down, roughly in order of how well they work.
Filter by tag. OpenAPI operations carry tags, and tags usually map to product areas. An agent that handles refunds needs the orders and payments tags, not admin or analytics. This is a one-line filter and it typically removes most of the surface.
Curate an allowlist. Write down the operations this agent is allowed to call, by operationId, and generate only those. This doubles as a security control, since an agent that has no tool for an endpoint cannot call it by accident. Our post on stopping agents from nuking your API argues for exactly this kind of narrow surface.
Retrieve tools on demand. For very large APIs, index the operations and select a handful per turn based on the task. This adds a retrieval step and its own failure modes, so reach for it only after filtering and curation stop being enough.
There is also the protocol route. The Model Context Protocol standardizes how a server exposes tools to a client, and an MCP server backed by your OpenAPI document gives you one integration point instead of one per framework. Our explainer on what MCP is covers the model, and building an MCP server with Apidog covers the build.

The spec has to be right first
Generation moves the quality problem upstream. A vague description in your OpenAPI document becomes a vague tool description, and the model picks the wrong endpoint. An optional field that the server actually requires becomes a tool the agent calls incorrectly on the first try.
So audit the spec through an agent’s eyes before you generate anything:
- Every operation has an
operationId, and it reads like a verb plus a noun. - Every operation has a description that says what it does, what it changes, and when not to use it. “Deletes a user” is not enough. “Permanently deletes a user and all their sessions. Cannot be undone. Use deactivateUser to disable access temporarily.” is.
- Every parameter has a description with units and format.
amountis ambiguous. “Amount in cents, minimum 50” is not. - Enums are declared rather than described in prose, so the model gets a closed set instead of guessing.
- Required is accurate. Specs drift toward marking everything optional, which pushes validation failures into runtime.
This is ordinary spec hygiene, and it pays off twice, because the same text drives your published docs. In Apidog the spec, the docs, the mock server, and the tests come from one project, so tightening a description improves all of them at once. Our guide on managing API versioning in Apidog covers the other half of keeping generated tools honest over time.
Share the tool set, do not copy it
A generated tool set is configuration, and configuration that lives in one developer’s checkout drifts the same way hand-written schemas do. The filter list, the allowlist, and the pinned spec version should be shared artifacts, versioned next to the spec they came from.
Some platforms make this the default unit. In Sharkly, an Agent is a saved working configuration rather than a one-off prompt: its instructions, Runtime, Skills, repositories, and run settings travel with it and can be shared across a Space, so a working tool setup becomes something a team reuses instead of something each person rebuilds. The runtime underneath is still Claude Code, Codex, or whatever you already run. What changes is that the configuration around it stops being local.

Testing generated tools
Generated tools fail in ways hand-written ones do not, so test the generation as well as the calls.
Start with a schema round-trip check. For every generated tool, build a valid example from the schema and send it. Anything that returns 400 or 422 means the tool schema and the server disagree, and the spec is the thing to fix.
Then test selection. Write a small set of task prompts with a known correct tool, run them, and record which tool the model picked. This is a cheap regression suite that catches the day someone renames an operation or shortens a description. Because the output is non-deterministic, assert on the tool name rather than on exact arguments, along the lines of our guide to testing non-deterministic agents.
Finally, run the agent against mocks before anything live. A mock server generated from the same spec gives you realistic responses without side effects, and it lets you inject the 500s and timeouts that your retry logic is supposed to handle.
Where this leaves you
The spec is the contract, and the tool list should be a projection of it, not a parallel copy maintained by hand. Generate the tools, filter them hard, keep the descriptions honest, and test both the shapes and the selection.
Start by exporting your OpenAPI document and counting the operations that have no description. That number is how much work stands between you and agent tools you can trust. Download Apidog if you want the spec, mocks, and tests in one place while you fix it.
Frequently asked questions
Can I generate tools from a Swagger 2.0 document? Yes, but convert it to OpenAPI 3.x first. The 2.0 body model differs enough that generators handle it inconsistently, and 3.x is what current tooling targets. The OpenAPI Specification repository documents the differences.
How many tools can a model handle at once? Accuracy starts degrading well before the technical limit, and the practical ceiling is usually a few dozen. Treat any list past that as a signal to filter by tag or curate an allowlist rather than as a limit to test.
Should tool names match operationId exactly? Yes, when the operationId is readable. It gives you a direct lookup from tool call back to spec operation, which makes tracing and debugging much easier. Rename in the spec if the name is bad, not in the generator.
What about GraphQL APIs? The same idea applies with a different source: introspect the schema and generate a tool per query or mutation. The volume problem is worse because a GraphQL schema exposes more surface, so filtering matters even more.
Do I still need to write any tools by hand? A few. Composite tools that chain several calls into one action, and tools that wrap something other than HTTP, still get written manually. The point is that the routine one-endpoint wrappers stop being handwork.
How do I stop the agent from calling write endpoints during testing? Generate a read-only tool set for test runs by filtering on HTTP method, and point the agent at a mock for anything that writes. Our post on why agents should hit mocks, not production covers the setup.



