OpenAPI Toolset
Turn any OpenAPI 3 or Swagger 2 spec into callable agent tools with one import
Point an agent at a REST API’s spec and every operation becomes a tool it can call — no per-endpoint wrapper functions, no external MCP process.
Quick Start
- Load a spec and start the agent
Every operation in the spec becomes a tool the agent can call directly.
python import os from praisonaiagents import Agent from praisonaiagents.tools.openapi_toolset import OpenAPIToolset toolset = OpenAPIToolset( spec_url="https://api.example.com/openapi.json", auth={"type": "bearer", "token": os.environ["API_TOKEN"]}, ) agent = Agent(name="ops", tools=toolset.get_tools()) agent.start("List the last 5 incidents and open a ticket for the top one") - Install httpx
httpxis needed at call time.bash pip install httpxYAML specs also need
PyYAML(already a dependency).
How It Works
get_tools() returns one callable OpenAPIOperation per spec operation. When the agent calls one, it builds the request, attaches auth headers, sends it with httpx, and returns the parsed JSON.
Loading a Spec
Provide the spec exactly one of three ways — otherwise a ValueError is raised.
Configuration Options
All constructor arguments are keyword-only.
| Option | Type | Default | Description |
|---|---|---|---|
spec_dict | Optional[Dict[str, Any]] | None | Parsed spec (JSON/YAML already loaded) |
spec_str | Optional[str] | None | Raw spec text (JSON or YAML) |
spec_url | Optional[str] | None | URL of a JSON or YAML spec |
base_url | Optional[str] | inferred | Overrides servers[0].url / host+basePath. Required only when the spec has no server info |
auth | Optional[Dict[str, Any]] | None | See the auth table below |
tool_filter | Optional[Callable[[str, str, str], bool]] | None | Called with (name, method, path); return True to keep the operation |
tool_name_prefix | Optional[str] | None | Prepended to every generated tool name |
header_provider | Optional[Callable[[], Dict[str, str]]] | None | Called per request; adds/overrides headers (use for rotating tokens) |
timeout | int | 30 | Per-request timeout, in seconds |
Exactly one of spec_dict, spec_str, spec_url is required.
Methods
| Method | Returns | Description |
|---|---|---|
get_tools() | List[OpenAPIOperation] | One callable per operation, ready for Agent(tools=...) |
OpenAPIOperation
Each tool returned by get_tools() is an OpenAPIOperation.
| Member | Type | Description | |
|---|---|---|---|
operation(**kwargs) | `dict \ | str` | Parsed JSON, else response text; on failure returns "{name} failed: {error}" |
build_request(**kwargs) | Dict[str, Any] | Pure argument-binding (no HTTP) — preview or test the assembled request | |
to_openai_tool() | Dict[str, Any] | Function-calling dict, via the same helper MCP tools use |
Attributes: name, description, method, path, base_url, parameters, body_schema, body_param, input_schema, auth, header_provider, timeout.
Auth Shapes
The auth dict is keyed by type.
auth["type"] | Required keys | Header emitted |
|---|---|---|
"bearer" | token | Authorization: Bearer <token> |
"api_key" | key, optional name (default X-API-Key) | <name>: <key> |
"basic" | value (already base64-encoded user:pass) | Authorization: Basic <value> |
Safety Behaviour
The toolset refuses unsafe requests and never fetches arbitrary URLs while parsing.
| Behaviour | What happens |
|---|---|
http:// + auth | Raises ValueError — credentials are never sent in the clear. Pass an https:// base_url or fix the spec |
| Swagger 2 schemes | When both http and https are listed, https wins (not schemes[0]) |
Remote $ref | Ignored — parsing a spec never fetches external URLs. Self-referential schemas are cycle-broken to {"type": "object"} |
| Path parameters | Every value is quote(value, safe="") before substitution, so ../../admin or https://other/ cannot alter host, path, query or fragment |
| Origin pin (host) | If an absolute path in the spec, or any argument path, would send the request to a host other than the configured base_url, the tool returns "{name} failed: {name}: refusing to send credentialed request to {other_origin}; configured origin is {configured_origin}". Credentials never leave the configured origin |
| Origin pin (scheme) | A same-host http:// value in the spec is refused the same way when the configured base_url is https:// — no silent HTTPS→HTTP downgrade |
| SSRF guard | Every outgoing request URL and the initial spec_url fetch run through is_safe_http_url. Loopback (127.0.0.1, ::1), private ranges (RFC1918), link-local (169.254.0.0/16, including cloud metadata 169.254.169.254), multicast and unspecified addresses are refused. A refused call returns "{name} failed: refusing unsafe URL '{url}'"; a refused spec_url raises ValueError at construction. Both httpx clients pass follow_redirects=False, so a redirect to an internal host cannot slip past. Set ALLOW_LOCAL_CRAWL=true to opt out for local development |
Strict body (declared properties) | Only properties declared in the requestBody schema are sent. Model hallucinations and framework-injected kwargs (idempotency_key, run_id, …) are dropped, so additionalProperties: false schemas stay clean |
Composed body (allOf / oneOf / anyOf) | Properties from every branch are advertised and accepted. oneOf / anyOf take the union — the superset of what the tool may accept. Only allOf contributes to the request’s required-field list; a field required by one branch of a oneOf is not required of the request as a whole. Nested composition and self-referential $refs are handled without hangs |
Free-form body (bare {"type": "object"} or additionalProperties: true) | Arguments the model supplies are forwarded verbatim. A closed empty schema (additionalProperties: false with no properties, directly or in an allOf branch) is honoured instead — nothing is sent |
Swagger 2 in: body | The whole named argument becomes the JSON body verbatim |
Relative servers[0].url | A remotely loaded spec with servers: [{ url: "/v1" }] resolves against the spec_url. With spec_dict= or spec_str= there is no URL to resolve against — the tool returns "{name} failed: {name}: no host to send to -- the spec's server URL is relative ('/v1') and could not be resolved. Pass an absolute base_url= to OpenAPIToolset." instead of an opaque transport error |
| Tool names | operationId is sanitised; when absent, name is method + "_" + sanitised path — stable across runs |
| Errors return, not raise | A network/HTTP failure comes back as "{name} failed: {exc}" so the model can react instead of ending the turn. The ValueErrors raised inside build_request() (the two origin/hostless refusals above) are caught by __call__() and returned the same way |
header_provider faults | If the provider raises, the request continues without its headers (a warning is logged) |
Framework-injected kwargs are stripped on all paths — free-form, declared, and composed — so durable-execution metadata never leaks into a strict API.
Body Shapes
Every requestBody schema falls into one of three modes, decided in this order.
Framework-injected kwargs are dropped in every mode.
Common Patterns
Restrict the tool surface to read-only operations:
from praisonaiagents.tools.openapi_toolset import OpenAPIToolset
toolset = OpenAPIToolset(
spec_url="https://api.example.com/openapi.json",
tool_filter=lambda name, method, path: method.lower() == "get",
)Namespace a spec when combining several on one agent:
stripe = OpenAPIToolset(
spec_url="https://api.stripe.com/openapi.json",
tool_name_prefix="stripe_",
)Rotate auth per request:
def fetch_token() -> str:
return "..." # your token-refresh logic
toolset = OpenAPIToolset(
spec_url="https://api.example.com/openapi.json",
header_provider=lambda: {"Authorization": f"Bearer {fetch_token()}"},
)Preview a request without calling the API:
tools = OpenAPIToolset(spec_url="https://api.example.com/openapi.json").get_tools()
request = tools[0].build_request(id="123")
print(request) # {"method": ..., "url": ..., "headers": ...}Call an operation whose body uses allOf — no special configuration:
from praisonaiagents import Agent
from praisonaiagents.tools.openapi_toolset import OpenAPIToolset
spec = {
"openapi": "3.0.0",
"servers": [{"url": "https://api.example.com"}],
"paths": {"/pets": {"post": {
"operationId": "createPet",
"requestBody": {"content": {"application/json": {"schema": {
"allOf": [
{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]},
{"type": "object", "properties": {"age": {"type": "integer"}}},
],
}}}},
}}},
}
agent = Agent(name="pets", tools=OpenAPIToolset(spec_dict=spec).get_tools())
agent.start("Create a pet called Rex, age 3")Both name and age are advertised to the model and forwarded to the API — a composed spec works with no extra wiring.
Force a base URL when the spec has a relative servers: entry:
from praisonaiagents.tools.openapi_toolset import OpenAPIToolset
toolset = OpenAPIToolset(
spec_dict=my_spec, # servers: [{ url: "/v1" }]
base_url="https://api.example.com", # required — no spec_url to resolve against
auth={"type": "bearer", "token": "..."},
)OpenAPI Toolset vs MCP
Both expose external tools to an agent; reach for the toolset when the API already has a spec.
| OpenAPI Toolset | MCP OpenAPI server | |
|---|---|---|
| Extra process | None | npx subprocess |
| Language runtime | Python only | Node.js required |
tool_filter | Built in | Not available |
tool_name_prefix | Built in | Not available |
| Agent view | Routes through build_openai_tool_dict — identical to MCP tools | Identical |
A workaround existed (MCP("npx -y @ivotoby/openapi-mcp-server ...")); the toolset removes the extra process and the npx dependency, and adds first-class tool_filter and tool_name_prefix.
Best Practices
Always use https when sending auth
The toolset refuses to attach credentials to an http:// endpoint. If the spec declares a cleartext server, pass an https:// base_url explicitly. A same-host scheme downgrade — a spec supplying http:// for an origin you configured as https:// — is refused too, so a token never travels in the clear.
Filter to the operations the agent actually needs
A large spec can generate dozens of tools. Use tool_filter to keep only read-only or task-relevant operations so the model isn’t overwhelmed.
Prefix tools when combining specs
Two specs may both define get_users. Set a distinct tool_name_prefix per toolset so names stay unique on one agent.
Trust the origin pin when pointing at a spec you don't control
When you point the toolset at a spec URL you do not control, the spec — not just the model — can try to relocate a call. The toolset pins every credentialed request to your configured origin’s host and scheme, so an absolute path in the spec cannot silently send your token somewhere else. Nothing to do — this is on by default; the tool returns a refusal message the model can see.
Set ALLOW_LOCAL_CRAWL=true only in local dev
OpenAPI tools pointed at loopback or private hosts are refused by default because an operation URL that resolves to 169.254.169.254 can read cloud metadata. Set ALLOW_LOCAL_CRAWL=true in your shell only when you explicitly want the agent to call a local API.
Preview requests in tests with build_request
build_request(**args) binds arguments to URL, headers and body without any HTTP call — assert the assembled request in a unit test.