Fundamentals

Capabilities

A capability is one thing your harness can do: a typed TypeScript route that says how a call reaches it, what every call is checked against, and the steps that do the work. The greet tool you called in the quickstarts is one. This page takes that shape apart.

One folder per capability

Each capability normally lives in its own folder under capabilities/, in a file named route.ts that default-exports it (a file can also export a capability together with the small caller that feeds it, as shown below):

capabilities
└── invoice-status
    ├── route.ts            # the capability, and the only file other code imports
    └── route.test.ts

craft start discovers every route.ts under capabilities/, so nothing registers a capability by hand. Name the folder after the capability's id; the id set in .id() is what identifies it at runtime, not the path. Project structure has the whole layout, including tests, helpers and domain folders.

Anatomy of a capability

// capabilities/invoice-status/route.ts
import { craft, direct, http, log } from "@routecraft/routecraft";
import { mcp } from "@routecraft/ai";
import { z } from "zod";

export const InvoiceStatusInput = z.object({
  invoiceId: z.string().min(1).describe("The invoice number, such as INV-2041."),
});
export type InvoiceStatusInput = z.infer<typeof InvoiceStatusInput>;

export default craft()
  .id("invoice-status")
  .title("Invoice status")
  .description("Look up whether an invoice is paid, open or overdue")
  .input({ body: InvoiceStatusInput })
  .authorize({ roles: ["finance"] })
  .from(direct(), mcp())
  .enrich(
    http<InvoiceStatusInput, { status: string; dueDate: string }>({
      method: "GET",
      url: (ex) => `https://billing.example.com/invoices/${ex.body.invoiceId}`,
    }),
  )
  .transform((result) => result.body)
  .to(log());

Everything before .from() describes the capability. Everything after it is the work, run in the order written.

  • .id() names the capability. It is the MCP tool name, the endpoint other capabilities call with direct("invoice-status"), and the name in logs and telemetry. Without it the builder generates a random id on every start, so the require-named-route lint rule makes it an error.
  • .title() and .description() are what a caller reads to decide whether to call it. An agent sees them as the tool's title and description, so write them for that reader.
  • .input() is the schema every call is validated against before any step runs, whichever way the call arrived. A call that does not match is refused, and the schema's .describe() text reaches the agent as field documentation.
  • .authorize() decides who may call it. Here the caller must carry a verified identity with the finance role. That identity comes from a door that checks a credential, such as MCP over HTTP behind a token on the team harness; over stdio on a laptop nothing verifies the caller, so this capability refuses every call there. Credentials and identity explains where identity comes from.
  • .from() lists the sources: how a call gets in. direct() lets other capabilities and the CLI call it in-process; mcp() makes it a tool an agent can call. One capability can stand behind several doors at once, with the same checks on each.
  • The steps do the work: here, fetch the invoice and answer with its body. The final body is what the caller gets back.

Sources: doors and triggers

A source decides how a capability starts. A door answers someone who asks, and returns the final body to them:

.from(direct(), mcp(), http({ path: "/invoices/status", method: "POST" }))

A trigger starts the capability when something happens, with nobody waiting for an answer:

.from(cron("0 8 * * 1-5"))                // on a schedule
.from(timer({ interval: 60_000 }))        // every minute
.from(simple({ report: "daily-summary" })) // once, when the harness starts

A trigger does not bring the input a door-facing capability expects. Give it a small capability of its own that builds that input and calls the other one through direct(), so the logic is still written once. Doors and triggers lists every way in.

Steps

Between .from() and the end, each step receives the exchange the previous one produced: .transform() reshapes the body, .filter() drops what should not continue, .enrich() pulls data in, .tap() runs a side effect without changing anything, and .to() hands the exchange to a destination. Operations covers them all, and The Exchange covers what they act on.

Prefer one .to() per capability, for the primary output. Side effects such as an audit record or a metric go in .tap(), which runs them in the background without holding up the caller. The single-to-per-route lint rule warns when a capability has more than one .to().

Several capabilities in one file

A file may hold more than one capability, which suits a capability and the small caller that feeds it. Build each with its own craft() and default-export them as an array. This is the form the scaffold uses:

// capabilities/hello-world/route.ts
import { craft, direct, log, simple } from "@routecraft/routecraft";
import { z } from "zod";

const GreetInput = z.object({ name: z.string() });
type GreetInput = z.infer<typeof GreetInput>;

const greet = craft()
  .id("greet")
  .input({ body: GreetInput })
  .from(direct())
  .transform((body) => `Hello, ${body.name}!`)
  .to(log());

const helloWorld = craft()
  .id("hello-world")
  .from(simple({ name: "Ada" }))
  .to(direct<GreetInput, string>("greet"));

export default [greet, helloWorld];

A single craft() chain can also hold several capabilities, each opened by its own .id(), but prefer the array: each capability keeps its own variable and its own body type, and one can be read, tested and moved on its own. Every id must be unique across the project.

Calling another capability

One capability calls another with direct("<id>") and imports only the types the callee exports from its route.ts. Composing capabilities covers chains, fan-out and routing chosen at runtime.


The Exchange

The body and headers every step of a capability acts on.

Operations

Every kind of step, and the wrappers that change how one runs.

Composing capabilities

Connect capabilities with direct(): chains, fan-out and dynamic routing.

Securing capabilities

Put authentication in front of the doors that reach your capabilities.

Previous
Talk from your editor