Making Agent API Calls 10x Faster with Cached Definitions
When developers give an AI coding assistant (like Cursor, Claude Code, Antigravity, or Cline) access to web APIs, the default approach is almost always the same: give the agent a bash tool and let it run curl or a CLI tool in a subshell.
I built an open-source tool called apicat (invoked on the command line as apic) to simplify how APIs are managed and called. Instead of writing shell scripts or memorizing complex curl flags, you define your endpoints in a clean YAML configuration file (~/.apicat or ./apicat.yaml). The CLI tool takes care of authentication tokens, headers, variable interpolation, and response formatting. While apic works well for human developers in a terminal, having an autonomous AI agent spawn shell processes to execute it revealed three deeper problems: slow execution, wasted context tokens, and lost socket reuse.
The technique explored here leverages that declarative YAML API definition format from apicat, but pairs it with a fast in-memory Model Context Protocol (MCP) server for LLMs rather than spawning CLI subprocesses.
I ran a set of benchmarks on real API queries against Fireworks AI and OpenRouter to compare three execution models:
- Unassisted shell execution: The agent reads documentation or local files and constructs
curlcommands. - CLI subprocess wrappers: The agent runs a local CLI command like
apic <service.endpoint>. - In-memory MCP server: The agent sends structured calls to a compiled Rust Model Context Protocol (MCP) server that keeps connections open in memory.
The difference in speed and token efficiency was substantial. MCP ran 10x faster, used one turn instead of ten, and 96.5% less tokens.
Architecture: apicat and native MCP
In apicat, we combined declarative YAML definitions with an in-memory Rust MCP server.
1. Declarative YAML definitions (apicat.yaml)
Endpoints are defined in a clean, structured YAML file:
fireworks.costs:
url: https://api.fireworks.ai/v1/accounts/chb1856-7irji5gdx5b0/usageCosts:query
method: POST
headers:
Authorization: "Bearer $!FIREWORKS_API_KEY"
Content-Type: application/json
body: |
{
"start_time": "$START_TIME",
"end_time": "$END_TIME",
"scope": 1,
"group_by": ["MODEL"]
}
help: Retrieve rated dollar costs and model spend breakdown.
2. Automatic JSON Schema generation
When the MCP server starts, it reads ~/.apicat and ./apicat.yaml, converting required ($!VAR) and optional ($VAR) variables into native JSON Schema tool definitions:
{
"name": "fireworks_costs",
"description": "Retrieve rated dollar costs and model spend breakdown.",
"inputSchema": {
"type": "object",
"properties": {
"START_TIME": { "type": "string", "description": "ISO timestamp" },
"END_TIME": { "type": "string", "description": "ISO timestamp" }
},
"required": ["START_TIME", "END_TIME"]
}
}
The model receives exact parameter types upfront, preventing hallucinated flag names, documentation scraping turns, or shell escaping mistakes.
Two bottlenecks when agents call APIs
When an agent interacts with external APIs, it faces two separate challenges:
┌──────────────────────────────────────────────────────────┐
│ 1. Schema Discovery: "What are the endpoints & schemas?" │
└────────────────────────────┬─────────────────────────────┘
│ Declarative apicat.yaml
▼
┌──────────────────────────────────────────────────────────┐
│ 2. Runtime Execution: "How do bytes reach the socket?" │
└────────────────────────────┬─────────────────────────────┘
│ In-Memory Rust MCP Server
▼
[ Upstream Web APIs ]
1. Schema discovery
Large models do not know your project's specific infrastructure:
- What is your custom account ID or quota path?
- Does this endpoint use
Authorization: Bearer <token>or a custom header? - Are timestamp filters formatted as ISO-8601 strings or Unix timestamps?
Without a defined schema, the agent enters an exploratory loop: searching the web, reading thousands of tokens of documentation, guessing parameters, and fixing 400 Bad Request responses.
2. Runtime execution
Even when the agent knows the syntax, running through a shell adds overhead:
- Subprocess startup: Spawning a subshell, booting a runtime like Node.js or Python, and parsing configuration files from disk on every invocation.
- No connection reuse: Every
curlcommand is an isolated process. It performs fresh DNS resolution and a fresh TLS 1.3 handshake on every request. - Shell escaping hazards: Nested JSON payloads with single and double quotes inside terminal strings frequently cause escaping errors.
Real-world autonomous agent benchmark
To measure this on an actual task, I gave an agent the following prompt:
"Perform an AI infrastructure cost and quota audit: Check Fireworks AI monthly spend and RPM limit, check OpenRouter API key daily limit, usage, and lifetime spend, and summarize quota health."
I tested the agent under two setups:
- Unassisted CLI mode: The agent located endpoint definitions in the workspace, constructed CLI commands, and parsed the output.
- In-memory MCP mode: The agent used a compiled Rust MCP server with pre-loaded schemas.
Results
| Metric | Unassisted Agent (CLI & Search) | In-Memory Rust MCP | Difference |
|---|---|---|---|
| Total wall-clock latency | 21.0 seconds | ~2.5 seconds | 6.0x faster |
| LLM reasoning turns | 10 turns | 1 turn | 10x fewer turns |
| Context token footprint | ~18,400 tokens | ~650 tokens | 96.5% reduction |
| Exploratory probing steps | 5 tool calls (which, cat, --help) |
0 tool calls | Zero trial and error |
The unassisted agent spent most of its time in multi-turn reasoning:
Unassisted trajectory (10 steps, 21.0s):
find_by_name -> which apic -> view_file apicat.yaml -> run apicat --help ->
run apic fireworks.balance -> run apic openrouter.key -> run apic openrouter.balance ->
which apic -> run apic fireworks.costs -> send_message report
In-memory MCP trajectory (1 step, 2.5s):
fireworks_balance() + openrouter_key() + fireworks_costs() in parallel -> send_message report
The main source of delay was not the network request itself. It was the LLM inference time across ten separate turns. Eliminating the exploration phase saved over 18 seconds on a single three-endpoint audit.
Batch execution latency: raw curl vs. CLI vs. in-memory MCP
Next, I measured raw execution speed across four API calls without model inference in the loop:
catfact.getFact(Public GET endpoint)openrouter.key(Authenticated GET endpoint)fireworks.costs(Authenticated POST with JSON body)fireworks.balance(Authenticated GET endpoint)
Measured latency
| Endpoint | Raw curl (Subprocess) |
apic CLI (Node.js) |
In-Memory Rust MCP | MCP vs. Raw curl |
|---|---|---|---|---|
catfact.getFact |
157.8 ms | 390.5 ms | 117.6 ms | 1.34x faster (+40 ms) |
openrouter.key |
188.3 ms | 358.4 ms | 131.1 ms | 1.44x faster (+57 ms) |
fireworks.costs |
1,198.7 ms | 1,242.4 ms | 783.1 ms | 1.53x faster (+416 ms) |
fireworks.balance |
2,748.1 ms | 1,394.6 ms | 1,742.0 ms | 1.58x faster (+1,006 ms) |
| Total batch (4 calls) | 4,292.9 ms | 3,385.8 ms | 2,773.8 ms | 1.55x faster (+1.52s saved) |
Connection pooling and Keep-Alive
The main technical factor behind lower latency in repeated calls is HTTP connection reuse (Keep-Alive).
When an agent runs curl or a CLI command, the operating system creates a process that immediately terminates after printing stdout. Every subsequent request must create a new TCP connection, resolve DNS, and complete an SSL/TLS handshake.
With an in-memory MCP server, the process stays running. The HTTP client keeps an active connection pool across calls:
- Cold socket (
openrouter.key, new TLS handshake): 4,066 ms - Warm socket (
openrouter.key, reused socket): 468 ms (8.7x speedup)
Repeated calls to the same provider reuse the existing encrypted connection.
Runtime comparison: Rust vs. Bun vs. Python
I also compared three different implementations of the MCP server:
- Native Rust: Release binary using
ureqandserde_json - Bun / TypeScript:
Bun.spawnwith native streams - Python 3.14: Standard library
jsonandsubprocess
| Metric | Rust (Native Release) | Bun (TypeScript) | Python 3.14 (Stdlib) |
|---|---|---|---|
| Cold start and handshake (Spawn, Init, tools/list) | 4.36 ms | 37.24 ms | 42.79 ms |
| Raw JSON-RPC IPC latency (Stdio Ping) | 0.018 ms (18 µs) | 0.058 ms (58 µs) | 0.028 ms (28 µs) |
| Memory footprint (RSS) | 1.77 MB | 22.83 MB | 17.45 MB |
| Runtime dependencies | None (Static binary) | Requires Bun or Node | Requires Python |
The compiled Rust binary starts in 4.3 milliseconds and uses 1.77 MB of memory, making it practical to leave running during an editing session.
Takeaways
- Avoid shell-based API calls in agents: Running
curlin a subshell creates escaping bugs, wastes context tokens, and loses connection pooling. - Use declarative API catalogs: Defining endpoints in YAML files like
apicat.yamlremoves the need for agents to search and scrape documentation. - Keep tools in memory: An in-memory MCP server with socket pooling turns multi-step terminal investigations into deterministic, single-turn tool calls.
The source code and benchmarks are available in the apicat repository.