.vendo/tools.json, turning your API into a set
of guarded tools. Extraction runs during build and development
so the tool surface never drifts from your API.
Zero config: npx vendo init runs the extractors and hooks vendo sync into
predev/prebuild. No key, no adapter, no tools option. createVendo()
reads .vendo/tools.json off disk on its own, and re-reads it per generation.
The rules
.vendo/tools.json is generated. Only tools in that file exist.
- Never hand-edit
.vendo/tools.json. It is regenerated. Durable corrections go in.vendo/overrides.json, which wins field by field and is never touched by sync. - Never invent a tool. If an endpoint was not extracted, fix the source it
is extracted from (or its OpenAPI spec) and re-run
npx vendo sync. Do not add entries by hand. - Unclassifiable routes are extracted
disabledwith a note. Enable them deliberately inoverrides.jsonafter review, never blanket-enable.
What makes a good tool
- A task-oriented description, written for the model: what the tool does for the user and when to reach for it, not a restatement of the route path.
- An honest risk label.
readauto-runs;writeanddestructiveare policy-gated, and the default policy makesdestructiveask first. Raise risk in overrides freely. confirmEach: trueon actions that need a person (payments, sends, exports) so policy asks before every run.
--ai / --no-ai skips the question either way). Whatever it
leaves wrong is yours to correct in .vendo/overrides.json. Two guards apply
regardless of the pass. Every proposal carries a verbatim quote from your
handler, and a second pass checks that quote against the real source. And a
hardening applies itself, while a loosening waits for you: lower risk, a woken
tool, or a cleared confirmEach lands only after you read the quote and say
yes.
How extraction works
Tool names use letters, numbers,_, and -, with a maximum of 64
characters. Use _ for namespaces. Each descriptor includes JSON Schema input
and a read, write, or destructive risk label.
OpenAPI operations
Vendo sync extracts OpenAPI operations from your spec into.vendo/tools.json,
using the shared naming and risk-label rules above. See Extraction
reference for where OpenAPI sits in the extraction order.
host_ plus the operation’s operationId (or, when a spec
omits one, the method-and-path fallback shared with route tools). The input
schema comes from the operation’s path/query parameters plus its
application/json request body under a body property. The binding records
the operationId, method, and path so the runtime can execute without
re-reading the spec, plus baseUrl when the spec declares an absolute
servers[0].url; omitted, the call goes same-origin. Risk labels follow the
same rules as route tools: DELETE is always destructive, and the
destructive word list can lift any other method.
tRPC procedures
If your app mounts a tRPC router, sync extracts each procedure as its own tool. The extractor statically parses your router (no server runs) and followsrouter({...}), createTRPCRouter, and mergeRouters across nested and
cross-file routers. Both app router and pages router mounts are supported, and
each mount is detected independently.
_ as the namespace
separator. The binding records the procedure, its query or mutation type,
the mount path, and the transformer when one is in use.
At runtime Vendo speaks the tRPC HTTP envelope. Queries use
GET {mount}/{procedure}?input= and mutations POST with a JSON body. When
the router uses superjson, Vendo wraps and unwraps the payload automatically.
Auth is unchanged from route tools. Present calls forward the inbound cookie
and authorization headers on same-origin fetches, and away calls still require
the host’s actAs seam plus an app-bound grant.
Risk labels follow the procedure type. Queries are read only when the
procedure name reads like a fetch; anything else falls back to the conservative
default. Mutations default to write, and the destructive word list can lift
them to destructive, the same rules that apply to route tools. Subscriptions
and any procedure the extractor cannot classify are emitted as disabled with
a note so you can promote them explicitly through overrides.
Zod input schemas are interpreted into JSON Schema for common patterns
(objects, primitives, unions, enums, optionals). If the extractor cannot
recognize a validator, it fails closed to a permissive schema and adds a note
so the tool still surfaces without blocking your build.
The extractor loads the TypeScript compiler from your app’s own
node_modules. If TypeScript is not available, tRPC extraction is skipped with
a warning and no tRPC tools are emitted. Extraction never fails your build.
Next.js server actions
If your Next.js app declares server actions, sync extracts each exported action as its own tool. The extractor statically scans modules that begin with"use server" and any inline "use server" directives (no host code runs).
Every named export of a module-scoped "use server" file becomes a candidate
tool.
module#exportName is the tool identity. At runtime
Vendo maps the arguments object onto positional parameters and returns the
action’s return value on the JSON wire.
Input schemas
Vendo interprets input schemas where they can be resolved statically:- Zod validators declared with
z.infer<typeof Schema>,z.input<typeof Schema>, orz.output<typeof Schema>type annotations, including imported schemas resolved across files. createSafeAction(schema, handler)fromnext-safe-action, unwrapped to a singledataparameter shaped by the schema.- Plain TypeScript primitives, object literals, and unions on parameter annotations.
Wrappers
The extractor unwraps two recognized patterns when the export remains an importable callable:cache(fn)from React.createSafeAction(schema, handler)fromnext-safe-action.
Risk labels
Server actions default towrite. A read-shaped name never earns read,
because a server action is a mutation seam. The destructive word list
(delete, remove, cancel, send, invite, and similar verbs) can lift
an action to destructive, the same rules that apply to route and tRPC
tools.
Some exports are always emitted as disabled. See Disabled
cases for the full list.
The extractor loads the TypeScript compiler from your app’s own
node_modules. If TypeScript is not available, server-action extraction is
skipped with a warning and no server-action tools are emitted; extraction
never fails your build. See Server-action registration
map for how init wires the detected actions
into createVendo.
Routes
Route scanning reads Next.js route conventions: App Routerroute.ts
handlers and pages/api files. Hosts on Express, Fastify, or Hono get route
tools from an OpenAPI spec or a tRPC router today — plain routes on those
frameworks are not yet scanned.
{ query, variables } envelope that route dispatch cannot construct — so the
route yields no tool rather than one that fails on every call.
Detection reads the route file and any local module it re-exports its handler
from, so export { GET, POST } from "./graphql-server" is skipped too. A route
that instead imports a GraphQL server and wraps it in its own exported
handler is not recognized; set "disabled": true on that tool in
overrides.json.
Durable overrides
grant-invalidated policy decision. See Tools and
safety for the surfaced notice and event shape.
Overrides also apply to compound tools, which
live in the same file’s compounds array. A compound entry inside
tools.json is rejected. Compounds belong to the authored file.
Present route calls forward the inbound request’s cookie and authorization
headers on a same-origin fetch. Away calls require both an app-bound grant and
the host’s actAs seam. See actAs presets for
provider-specific wiring.
Extraction reference
Extractor order
Extractors run in a fixed order: OpenAPI first, then tRPC, then server actions, then the route scan. Each extractor decides whether it applies to your app. The ones that do not detect anything are skipped. When the tRPC extractor emits tools for a mount, any catch-all route under that mount (for example/api/trpc/[trpc]) is dropped from the route scan so you do not see
duplicate coverage of the same surface.
Disabled cases
Next.js server actions. The extractor fails closed and emitsdisabled: true with a note in two cases:
- Inline server actions. A
"use server"directive inside a component-scoped function is real surface, but the function is not importable by module path. Hoist the action into a module-level"use server"file to promote it into a working tool. - Unclassifiable exports. When the export is not a callable Vendo can
bind (for example, a re-exported object or a value with no stable
signature), it is emitted disabled with
risk: "destructive"so you can promote it explicitly through overrides.
Server-action registration map
vendo init generates an actions registration map beside your handler route
and wires it into createVendo({ serverActions }). The map imports every
detected action module keyed by "<module>#<exportName>", so Vendo can
dispatch calls through the map at runtime.
Init writes those two files exactly once — on the run that creates them. From
then on they are yours to edit, and init never rewrites a file that already
exists. An existing map is compared only by the keys it registers, so your
formatting, your comments, your aliases and your own extra entries all survive:
when an action is missing, a later vendo init prints just the entries to add
(plus the serverActions line for a route that predates your actions), and
vendo doctor fails E-WIRE-009 until you apply
them. Re-running init on an unchanged surface prints nothing.
Actions disabled in .vendo/overrides.json are left out of all of this — the
runtime never dispatches them, so neither init nor doctor asks you to register
them. A route that passes a serverActions map it composes itself is left
alone entirely, and no generated map is created for it.
When the map lacks an entry for an action, or the registered value is not a
function, execution fails closed with a clear not-implemented error and no
work is performed. Server actions are present-only: away calls and MCP venues
fail closed because there is no HTTP seam to attach actAs credentials to.