Skip to content

Capabilities

A capability is a governed application operation derived from an oRPC procedure. It is the central abstraction of oRPC Agent — not "tool", which is only how one adapter represents a capability on one surface (ADR-002).

Why not just call procedures?

A procedure answers "what does this operation do?". Handing it to an agent raises questions the procedure alone cannot answer:

  • May this surface reach it at all? (a UI action is not automatically an MCP tool)
  • What does it do to the world — read, write, destroy, leave the system?
  • How sensitive is it, independent of side effects?
  • May it be retried? Must a human approve it? What may models see of its output?
  • Who asked, and where is that recorded?

A capability is a procedure plus those answers, made explicit and machine-readable.

Anatomy

text
capability "orders.refund"
├── identity            id (registry path), tags
├── contract            input schema, output schema      <- from the procedure
├── implementation      handler + oRPC middleware        <- from the procedure
├── description         model-facing usage text          \
├── exposure            per-surface allow map             \
├── classification      sideEffect + risk                  |  AgentMeta
├── execution policy    timeout, retry, idempotency        |  (.meta({ agent }))
├── approval policy     static gate or policy-driven      /
├── redaction           output / approvalInput hooks     /
└── adapter hints       tool names, MCP annotations     /

Everything above the line already exists in your oRPC codebase; oRPC Agent adds the metadata block and never forks the implementation (ADR-001).

Defining one

ts
import { os } from "@orpc/server";
import { agentProcedure } from "@orpc-agent/core";
import * as z from "zod";

const base = os.$context<AppContext>().use(requireSession);
const agentBase = agentProcedure(base);

export const refundOrder = agentBase
  .meta({
    agent: {
      description: "Refund an order, fully or partially. Check eligibility first.",
      expose: { aiSdk: true, direct: true },        // not on mcp
      sideEffect: "write",
      risk: "high",
      tags: ["orders", "money"],
      redact: { output: (o) => ({ ...(o as Refund), gatewayRef: undefined }) },
    },
  })
  .errors({ NOT_ELIGIBLE: { message: "Order is not eligible for refund." } })
  .input(z.object({ orderId: z.string(), amount: z.number().positive(), reason: z.string() }))
  .output(RefundResult)
  .handler(async ({ input, context, errors, signal }) => {
    if (!(await context.orders.isRefundable(input.orderId))) throw errors.NOT_ELIGIBLE();
    return context.payments.refund(input, { signal });
  });

The procedure remains a plain oRPC procedure: mount it in your router, call it from your UI, generate OpenAPI from it. The agent block only matters to createCapabilityRegistry and the runtime.

Tags are how a catalog gets asked for in pieces

tags is free-form on the capability, but it carries one defined job: it is what describe's scope matches on. An application that tags devices.* as devices and billing.* as billing lets each route ask for the group it needs, and the discovery policies of everything else never run.

ts
tags: ["orders", "money"]                                  // on the capability
runtime.describe("aiSdk", { actor, context, scope: { tags: ["orders"] } });

Two consequences worth knowing before you tag:

  • An untagged capability matches no tags scope. Adding scope to a host before tagging returns nothing rather than everything — deliberate, so a scope means what it says. Select untagged capabilities by ids.
  • A tag is not a permission. Scope shapes discovery only; the capability stays invocable by an authorized actor either way. Exposure and policies are the authority mechanisms (SI-2).

One capability, many surfaces

SurfaceRepresentationTrust profile
directruntime.invoke from your server codeYour code, your process
aiSdkAI SDK tool in your model loopYour process, model-chosen arguments
mcpMCP tool for external clientsExternal client, model-chosen arguments
workflowStep in a durable workflow (future adapter)Your infrastructure, replayed calls
testDeterministic test invocationCI

Same validation, same policies, same middleware, same audit trail on every surface — that uniformity is the product. Exposure differs per surface because trust differs per surface (ADR-004).

Granularity guidance

Good capabilities are narrow verbs over governed nouns:

  • orders.refund(orderId, amount, reason) — good: bounded, classifiable, approvable.
  • db.query(sql) — bad: unbounded power, unclassifiable side effects, unpoliceable (and a prompt-injection amplifier — see security/prompt-injection.md).
  • orders.manage(action, payload) — bad: one capability with five side-effect profiles; split it.

If you cannot assign a single honest sideEffect value, the capability is too broad.

Independent community project — not affiliated with or endorsed by the oRPC maintainers.