N AgentNava
AgentNava · Build

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_searchSearch 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}}' },
}]
Never a literal 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 agent never sees the token

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.

what does not work yet

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 decide

The three sources are { openapi }, { postman } and { curl }. fromOpenApiUrl(url) fetches the document for you.

it runs on your machine

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-missing warning 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 include to 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.