Wrapping a REST API as an Agent Tool
Wrapping REST means building a thin, validated host function that exposes one agent-facing action while your code owns auth, timeouts, error mapping, and response shaping.
Search across all documentation pages
Wrapping REST means building a thin, validated host function that exposes one agent-facing action while your code owns auth, timeouts, error mapping, and response shaping.
You do not hand the model a general HTTP client. You hand it a named capability.
Pick a domain action, design a strict JSON schema, implement a handler that calls REST with scoped credentials, return compact structured results, and register the tool in your agent runtime.
list_open_incidents, not call_any_path).additionalProperties: false.ok, data fields, error_code, retryable).Wrap GET /v1/incidents?status=&limit= as list_incidents.
import json
import os
from typing import Any, Literal
import httpx
from pydantic import BaseModel, Field, ValidationError
BASE = os.environ["INCIDENTS_API_BASE"] # e.g. https://api.internal.example
TOKEN = os.environ["INCIDENTS_READ_TOKEN"]
class ListIncidentsArgs(BaseModel):
status: Literal["open", "acked", "resolved"] = "open"
limit: int = Field(default=10, ge=1, le=50)
team: str | None = Field(default=None, max_length=64)
TOOL_SPEC = {
"type": "function",
"function": {
"name": "list_incidents",
"description": (
"List recent incidents filtered by status. "
"Use to survey workload before acknowledging or escalating. "
"Do not use for creating or resolving incidents."
),
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["open", "acked", "resolved"],
"description": "Incident lifecycle filter.",
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"description": "Max rows to return (default 10).",
},
"team": {
"type": "string",
"description": "Optional team slug filter.",
},
},
"additionalProperties": False,
},
},
}
def list_incidents(arguments_json: str) -> str:
try:
args = ListIncidentsArgs.model_validate_json(arguments_json or "{}")
except ValidationError as e:
return json.dumps({
"ok": False,
"error_code": "INVALID_ARG",
"message": e.errors(),
"retryable": False,
})
params: dict[str, Any] = {"status": args.status, "limit": args.limit}
if args.team:
params["team"] = args.team
try:
with httpx.Client(timeout=httpx.Timeout(10.0, connect=5.0)) as client:
r = client.get(
f"{BASE}/v1/incidents",
params=params,
headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"},
)
except httpx.TimeoutException:
return json.dumps({"ok": False, "error_code": "TIMEOUT", "retryable": True})
except httpx.HTTPError:
return json.dumps({"ok": False, "error_code": "NETWORK", "retryable": True})
if r.status_code == 401 or r.status_code == 403:
return json.dumps({"ok": False, "error_code": "AUTH", "message": "Not allowed", "retryable": False})
if r.status_code == 429:
return json.dumps({"ok": False, "error_code": "RATE_LIMIT", "retryable": True})
if r.status_code >= 500:
return json.dumps({"ok": False, "error_code": "UPSTREAM", "retryable": True})
if r.status_code >= 400:
return json.dumps({"ok": False, "error_code": "BAD_REQUEST", "message": r.text[:300], "retryable": False})
payload = r.json()
items = payload.get("items") or payload.get("data") or []
slim = [
{
"id": i.get("id"),
"title": (i.get("title") or "")[:120],
"severity": i.get("severity"),
"status": i.get("status"),
"opened_at": i.get("opened_at"),
}
for i in items[: args.limit]
]
return json.dumps({"ok": True, "count": len(slim), "incidents": slim})Register TOOL_SPEC with your agent framework and route list_incidents to this handler.
| REST concept | Agent tool choice |
|---|---|
| Resource collection GET | list_* with filters + limit |
| Resource GET by id | get_* requiring id from list/search |
| POST create | create_* with idempotency key |
| PATCH/PUT | update_* with explicit fields only |
| DELETE | Separate tool + approval for high impact |
| Batch / search DSL | Fixed query templates, not free strings |
Collapse multi-call UI flows when the agent should not invent intermediate states (for example one refund_order tool that calls validate + refund endpoints server-side).
| Method | Default retry in tool host |
|---|---|
| GET / HEAD | Safe with backoff on 429/5xx/timeout |
| POST with Idempotency-Key | Safe if backend honors the key |
| POST without key | Do not auto-retry |
| PATCH/DELETE | Retry only if documented idempotent |
Surface retryable to the model, but enforce max attempts in the host so the loop cannot hammer an outage.
Log tool name, arg hashes (or redacted args), latency, status, upstream request id, and agent run id.
Never log full secrets or unnecessary PII.
{ok: true, empty: true} style result.| Approach | Pros | Cons |
|---|---|---|
| Hand-written tool per action | Safest, clearest | More code |
| OpenAPI → generated tools | Fast coverage | Often too wide / noisy |
| MCP server over curated tools | Multi-client reuse | Still need curation |
Generic http_request tool | Flexible | High abuse and SSRF risk |
| GraphQL free-form tool | Flexible reads | Over-fetch and auth holes |
Usually one primary action. Internal fan-out is fine if the agent sees one semantic step.
Yes for large sets. Return next_cursor and require it on the next call instead of huge pages.
Start with connect 2-5s and total 10-30s depending on the API. Long jobs should return a job id and a status tool.
Store OAuth tokens server-side per user/session. The tool uses the session principal; the model never sees the refresh token.
Most tool APIs expect a finished string result. Buffer with a size cap, or write large artifacts to object storage and return a pointer.
Translate to JSON in the handler. Keep the model on one structured format.
Prefer one tool name with environment-specific base URLs and credentials in config, plus hard network isolation between envs.
Return the new resource id, version/etag, and a short status. That enables follow-up gets without re-querying by fuzzy text.
Related: Custom Tools Basics
Related: What Makes an Internal API a Good Agent Tool
Related: Versioning and Deprecating Agent Tools Without Breaking Agents
Related: Scoping API Keys and Credentials Passed to an Agent
Related: Custom Tools Best Practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026