Tools
Three ways to let an agent reach something beyond its own files.
What every agent already has
Reading, writing and editing files, a shell, and search across its own drives. You do not switch those on. Beyond them, three ways to give an agent more.
Platform capabilities
Named, and switched on by listing them:
tools: ['web_search']web_search | Search the live web. We carry the keys; you do not bring your own |
A name we do not recognise is rejected when you create the agent.
MCP servers
Connect a server and its tools become available to the agent.
mcpServers: [{
name: 'linear',
url: 'https://mcp.linear.app/sse',
transport: 'sse', // or 'http'
headers: { Authorization: 'Bearer {{secrets.LINEAR_TOKEN}}' },
}]Put credentials in headers as {{secrets.NAME}}, naming a secret
the agent declared. A literal token would be stored in the version, and a version is
readable.
Your own endpoints
You describe the request; we make it. There is no code to deploy and nothing of yours runs in our runtime.
httpTools: [{
name: 'order_lookup',
description: 'Fetch one order by its number. Use before any refund decision.',
method: 'GET',
url: 'https://api.you.com/orders/{{params.id}}',
params: { id: { type: 'string', required: true } },
secrets: ['ORDERS_API_KEY'],
headers: { Authorization: 'Bearer {{secrets.ORDERS_API_KEY}}' },
}]{{params.x}} and {{secrets.X}} are resolved when the call is made
and never stored resolved.
description does the same job when does on a workflow: it is how the
agent decides this tool is the one. "Fetch one order by its number" is useful; "orders API" is
not.
Getting a bearer token to the call
Almost every API wants Authorization: Bearer <something>. Which
<something>, and where it comes from, depends on whose it is.
The model is given a tool's name, description and parameters, and nothing
else. It calls order_lookup({ id: '4471' }). We substitute
{{secrets.X}} into the url, headers and body after it has chosen, as the
request goes out. The value is never in a prompt, never in the transcript, and never in
anything the model could repeat back.
One credential for everyone
A service account, a partner key, anything that is the same for all your users. Store it once; every conversation uses it.
await ws.secrets.create({ name: 'ACME_TOKEN', value: process.env.ACME_TOKEN });
// in the tool
headers: { Authorization: 'Bearer {{secrets.ACME_TOKEN}}' }A different token per person, from your own OAuth
This is the common one and it is easy to over-think. Your application already did OAuth with your user, so it is holding their access token right now. Hand it over when you start the conversation. Nothing is stored, and the token is fresh because you just got it.
const conversation = await agent.start({
endUserId: ananya.id,
secrets: { ACME_TOKEN: ananyasAccessToken }, // this conversation only
});
await conversation.ask('what happened to my last order?');The tool does not change. It still says
Bearer {{secrets.ACME_TOKEN}}; the value it resolves to is this conversation's.
That is what lets one agent serve ten thousand of your customers: same configuration, a
different token each time.
The token expired
Refresh it your side and push the new one into the conversation that is already open.
await conversation.attach({ secrets: { ACME_TOKEN: refreshedToken } });A token that outlives the conversation
When the credential belongs to one of your users and should still be there tomorrow, store it against them and attach it. It is encrypted on arrival and never handed back, which is why you attach it by id rather than by value.
const ananya = ws.endUser(row.userId);
const key = await ananya.secrets.create({ name: 'ACME_TOKEN', value: token });
const conversation = await agent.start({ endUserId: ananya.id });
await conversation.attach(key); // EXPLICIT. Nothing is implied.Two rules we enforce and you cannot switch off: the agent must declare that secret name, so attaching by id is not a way around what an agent is allowed to read; and a secret belonging to one of your users can only be attached to a conversation started for that same person.
Every option above needs either a long-lived credential or your application running to hand us a fresh one. A scheduled run at 3am has neither: if the stored token was a one-hour OAuth token, it is long dead by then.
If your API issues short-lived machine-to-machine tokens, tell us: we do not yet fetch them for you. Today the ways through are a long-lived credential, or hosting an MCP server that holds the OAuth relationship itself. The same applies to mutual TLS and to request signing such as HMAC or AWS SigV4, which a fixed header cannot express.
Drafting tools from what you already have
Hand-writing one declaration per endpoint is a week of somebody's time, and a reason to
stall. If your team keeps an OpenAPI document, a Postman collection, or a curl command that
works, the SDK turns it into draft httpTools for you.
import { importTools } from '@cerebro-labs/agentnava-sdk';
const draft = importTools(
{ openapi: await (await fetch('https://api.you.com/openapi.json')).json() },
{ include: (c) => !c.url.includes('/admin/') }, // an agent has no business purging orders
);
console.log(draft.tools); // drafts, in the shape agents.create() takes
console.log(draft.secrets); // the credentials to create, with no values carried across
console.log(draft.warnings); // everything a human should decideThe three sources are { openapi }, { postman } and
{ curl }. fromOpenApiUrl(url) fetches the document for you.
Nothing about the source reaches AgentNava. That is deliberate rather than convenient: a
Postman export routinely contains a live token, and a file you never upload is a token we can
never receive. Any credential the import finds is replaced with a
{{secrets.NAME}} placeholder and reported in secrets, so you create
the real value yourself with ws.secrets.create(). Each drafted tool also
declares the secrets it uses, in its own secrets field, which is what
lets the runtime load them for the turn.
It gives you drafts, not finished tools
Read them before you ship them. Two things it will not do for you, and both are reported as warnings rather than quietly guessed:
- It cannot write the descriptions. None of the three formats contains the
one sentence a model reads to decide "this is the tool for this job". A summary written for a
developer browsing documentation is not that sentence. A bad description does not fail, it
makes the agent pick the wrong tool, so an operation with nothing usable comes back with a
description-missingwarning instead of a confident-looking stub. - It does not decide which endpoints an agent should have. A model chooses
markedly worse between forty tools than between eight, and your API almost certainly has
endpoints no agent should ever call. Use
includeto keep the ones you mean.
Nothing is created. importTools returns an object; you pass
draft.tools to ws.agents.create({ httpTools }) when you are happy with
it.