AgentBack is an AI-native API/MCP framework for the agent era, built with agents, for agents. It lets REST endpoints, MCP tools, generated docs, typed clients, tests, and runtime validation all share one Zod contract.
Your API has two audiences now: human developers and AI agents. Same operations, different surfaces — REST and a typed client for devs, MCP tools and machine-readable docs for agents.
Most stacks make you describe each operation four or five times: a Zod schema, an OpenAPI block, an MCP tool definition, a client type. One contract, copied everywhere. Add a field, edit all of them. Miss one, they drift — quietly, until an agent sends the body your docs promised and your validator throws.
AgentBack describes it once.
const OrderId = z.object({id: z.string()});
const Order = z.object({id: z.string(), status: z.enum(['open', 'shipped'])});
@get('/orders/{id}', {path: OrderId, response: Order}) // → REST + OpenAPI + typed client
async getOrder(input) { /* … */ }
@tool('get_order', {input: OrderId, output: Order}) // → MCP tool, same schemas
async getOrderTool(input) { /* … */ }One Zod schema becomes the validator, the z.infer type, the OpenAPI 3.1
contract, the MCP schema, the typed client, and the docs. Change the schema;
every boundary follows.
Not "LoopBack/Express, but newer." It's the layer that keeps one operation
coherent for the developers who build on your API and the agents that call it —
on a DI core you can extend, on any host (Node, Fastify, Hono, Bun, Deno,
Workers) from one fetch handler.
One Zod schema → a REST/HTTP API for developers and an MCP surface for agents (tools, /llms.txt, a derived SKILL.md), served on any host from one fetch handler. · Interactive version →
The foundation is a modern ESM port of LoopBack 4's proven dependency-injection
core: a hierarchical Context of Bindings with @inject, providers,
interceptors, extension points, lifecycle observers, and tag-based discovery.
REST controllers and MCP tool classes are just bindings the respective servers
find by tag — so adding a route or a tool is adding a class, not editing a
router file, an OpenAPI document, and a tool manifest.
- ESM-only, Node 22.13+, TypeScript 7.0
Context+BindingDI with@inject, providers, interceptors, extensions, lifecycle observers- Zod-first schemas; decorators accept
ZodTypedirectly - OpenAPI 3.1.1 emission via Zod v4's native
z.toJSONSchema - MCP (Model Context Protocol) server with the same decorator style as REST
- Swagger UI at
/explorerand MCP Inspector at/mcp-inspector - AX artifacts:
/llms.txt+/llms-full.txtserved from the same route registry - Machine-actionable errors: stable codes, per-field issues, the violated schema, retryability, and remediation hints — same envelope on REST and MCP
- Safety primitives:
confirm:(payload-bound confirmation tokens) andidempotency:(idempotency-key replay) on routes and tools - Tool cost report: token-price the MCP tool surface before agents pay for it
- Per-call pricing:
@price('$0.001')meters a route or tool and (withinstallPriceGate) refuses unpaid calls with an x402/MPP challenge - Typed HTTP client without codegen when TypeScript consumers share schemas
- Policy, auth, metering, payments, messaging, and observability as DI components
- Actors: stateful entities at a stable
{type, id}address — serialized turns, idempotent retries, lease-free queries, and an optional event log - Built on the official
@modelcontextprotocol/sdk
Status: alpha. End-to-end examples and tests pass; the API surface is stabilizing but may still change between alpha releases.
The core product claim is one schema, every boundary. Compared with common Node/TypeScript service stacks, AgentBack optimizes for teams whose APIs are consumed by both applications and AI agents.
| Stack | Runtime contract | Service contract | Agent/tool contract |
|---|---|---|---|
| Express + raw Zod | Hand-wired Zod | Hand-written OpenAPI | Hand-written tool manifest |
| Fastify | JSON Schema/TypeBox | OpenAPI via @fastify/swagger |
Custom adapter |
| Hono | Zod (validator) | OpenAPI via @hono/zod-openapi |
Custom adapter |
| tRPC | Zod | TypeScript-only | Custom adapter |
| NestJS | class-validator | Swagger decorators | Custom adapter |
| FastAPI | Pydantic | OpenAPI from the same models | Custom adapter |
| AgentBack | Zod | OpenAPI from same Zod | MCP from same Zod |
Use it when you need HTTP APIs, MCP tools, docs, typed clients, policy checks, and usage rails to stay coherent as the system grows.
Fastify and Hono are transport runtimes, not competitors. They solve HTTP
plumbing; AgentBack solves the layer above — one Zod schema projected to REST,
OpenAPI, and MCP through a DI container. And it's host-portable: a
runtime-neutral RestServer.fetchHandler() runs the full pipeline (routing,
validation, DI, auth, streaming, uploads, MCP-over-HTTP) on Express, a native
Node listener, Fastify, Hono, Bun, Deno, and Workers — see
examples/hello-hosts and the
HTTP hosts guide.
Pick your host by class: RestApplication (a.k.a. ExpressRestApplication) for
the Node/Express host, or EdgeRestApplication for fetch/edge — the latter
installs no express/cors (the neutral middleware machinery lives in
@agentback/middleware). Ship either with agentback deploy vercel or
agentback deploy cloudflare (the @agentback/cli, with a nodejs_compat-aware
bundle-doctor preflight).
Full docs live in docs/ — a guided path from the core
ideas to building real apps:
- Blog — Design notes · Boundary architecture map
- Concepts — Dependency injection · Schema-first decorators · Components, servers & lifecycle
- Guides — Build a REST API · Build an MCP server · Hybrid app (REST + MCP) · Composition & extensibility · Testing · Secure MCP over HTTP · Deploy to production
- Architecture — Overview + diagrams · Boundary-coherence design thesis
The rest of this README is the one-page tour and code feel.
The DI framework is the foundation; everything else is built on it. The full
catalog — DI foundation, REST/MCP/clients, and platform components — is in
docs/packages.md; each package also ships its own
README.md under packages/.
Scaffold a new service (REST, MCP, or both) — no global install needed:
npm create agentback my-service # hybrid (REST + MCP)
pnpm create agentback my-service --template rest # REST only
pnpm create agentback my-api --template hybrid --drizzle --auth # with add-onsRun npm create agentback with no name for an interactive wizard. See
create-agentback for templates,
capability flags (--drizzle/--auth/--console), and host options.
Or work in this repo against the bundled examples:
pnpm install
pnpm build
pnpm -F hello-rest start # REST + Swagger UI
pnpm -F hello-mcp test # MCP over stdio (drives the server with a test client)
pnpm -F hello-hybrid start # REST + MCP from one process, both UIsFor a complete, real-world build beyond the in-repo hello-* examples, see
ninemindai/agentback-demo —
a Weather MCP server where one Zod schema set is served over stdio, authenticated
HTTP, and a dev console, backed by the free Open-Meteo API.
This repo ships an agent skill —
skills/agentback — that teaches a coding agent the
framework's decorator patterns, slot-0 input-bundle convention, DI container,
auth stack, and schema-sharing client, with task-scoped reference files the
agent loads on demand. If you're building an app with AgentBack and an
agent is doing the typing, install the skill first; it encodes the conventions
that aren't guessable from type signatures alone.
The easiest install is skills.sh, which supports Claude Code, Codex, Cursor, Copilot, Gemini, and 20+ other agents:
npx skills add ninemindai/agentback # pick agents interactively
npx skills add ninemindai/agentback -a claude-code -a codex -y
npx skills add ninemindai/agentback -g -y # user-global, all agentsOnce installed, the agent discovers it automatically; it activates when a task
mentions @agentback/* packages, the REST/MCP decorators, or hybrid
Zod-shared apps.
Agents without skill support — the skill is plain markdown with a YAML
description. Point the agent at it from your instructions file (e.g.
AGENTS.md):
When working with @agentback/\* packages, first read
skills/agentback/SKILL.md and the relevant file under
skills/agentback/references/ (REST, MCP tools, DI, auth, client sharing,
or composition) before writing code.The skill's references/ directory splits the
deep material by task — rest-and-openapi.md, mcp-tools.md,
dependency-injection.md, auth-and-rate-limiting.md,
schema-sharing-and-client.md, composition-and-operations.md — so agents
pull only the context the current task needs.
A full code walkthrough — the DI container, a REST service, and an MCP server, in
compiling TypeScript drawn from the packages. Expand it, or jump straight to the
runnable examples/ and the guides.
Show the walkthrough
import {Context, BindingScope, inject, injectable} from '@agentback/context';
import {Application} from '@agentback/core';
@injectable({scope: BindingScope.SINGLETON})
class Clock {
now() {
return new Date().toISOString();
}
}
@injectable()
class Greeter {
constructor(@inject('clock') private clock: Clock) {}
greet(name: string) {
return `Hello ${name} at ${this.clock.now()}`;
}
}
const app = new Application(); // Application IS a Context
app.bind('clock').toClass(Clock);
app.service(Greeter); // tag-based service binding
const greeter = await app.get<Greeter>('services.Greeter');
console.log(greeter.greet('world'));The same Context/Binding/@inject machinery is what the REST and
MCP servers below use under the hood — controllers and tool classes are
just tagged bindings the servers discover at start time.
import {z} from 'zod';
import {api, get, post} from '@agentback/openapi';
import {RestApplication} from '@agentback/rest';
import {inject} from '@agentback/context';
const HelloPath = z.object({name: z.string().min(1).max(64)});
const Greeting = z.object({greeting: z.string()});
const EchoIn = z.object({text: z.string().min(1).max(280)});
const EchoOut = z.object({echoed: z.string(), at: z.string()});
@api({basePath: '/greet'})
class GreetingController {
@get('/hello/{name}', {path: HelloPath, response: Greeting})
async hello(input: {path: z.infer<typeof HelloPath>}) {
return {greeting: `Hello, ${input.path.name}!`};
}
@post('/echo', {body: EchoIn, response: EchoOut})
async echo(
input: {body: z.infer<typeof EchoIn>},
@inject('clock') clock: {now(): string},
) {
return {echoed: input.body.text, at: clock.now()};
}
}
const app = new RestApplication();
app.bind('clock').to({now: () => new Date().toISOString()});
app.restController(GreetingController);
await app.start();
// GET /openapi.json -> OpenAPI 3.1.1
// POST /greet/echo with {"text":""} -> 422 with Zod issuesSchemas live once on each verb decorator. The handler receives a single
input object with body/path/query/headers keys derived via
z.infer — change a schema and the parameter type updates. @inject
weaves in at slot 1+; on routes with no input schemas, slot 0 is yours
too (e.g. @get('/whoami') async whoami(@inject(USER) user) { … }).
import {z} from 'zod';
import {mcpServer, tool, MCPComponent} from '@agentback/mcp';
const ForecastInput = z.object({
city: z.string(),
days: z.number().int().min(1).max(7),
});
const ForecastOutput = z.object({
city: z.string(),
forecast: z.string(),
});
@mcpServer()
class WeatherTools {
@tool('get_forecast', {
description: 'Returns the forecast for a city.',
input: ForecastInput,
output: ForecastOutput,
})
async getForecast(input: z.infer<typeof ForecastInput>) {
return {city: input.city, forecast: 'sunny'};
}
}
const app = new RestApplication();
app.component(MCPComponent); // registers MCPServer
app.service(WeatherTools); // tag flows from @mcpServer's @bind metadata
await app.start();
// stdio MCP transport active by default; mount mcp-inspector for a UIThe schemas live once, on the decorator. The parameter type is derived
via z.infer<typeof ForecastInput> — if you change the schema, TS
rewrites the parameter type. With output: declared, the return type
is also constrained at compile time and validated at runtime; the MCP
SDK is given the schema so structured-content clients can consume it
directly. Omit output: to keep the method's return type unconstrained.
The @mcpServer() decorator uses @bind({tags: {mcpServer: true}}) under the
hood. When you bind the class with app.service() (or app.controller()),
the framework reads that metadata via createServiceBinding and the
mcpServer tag is applied automatically — no manual .tag() call needed.
Beyond schemas, the framework ships the conventions agents need from an API:
@post('/deploy', {body: DeployIn, response: DeployOut, confirm: true})
async deploy(input: {body: z.infer<typeof DeployIn>}) { … }
// 1st call -> 409 {error: {code: 'confirmation_required', confirmationToken, hint}}
// identical retry with x-confirmation-token header -> executes.
// The token is single-use and bound to the exact payload — a confirmed
// call cannot differ from the proposed one. Same flow on MCP tools via
// @tool(..., {confirm: true}) and a `confirmationToken` input property.
@post('/charge', {body: ChargeIn, response: ChargeOut, idempotency: true})
async charge(input: {body: z.infer<typeof ChargeIn>}) { … }
// Replaying an idempotency-key header returns the original result without
// re-executing; concurrent duplicates share one execution; errors aren't cached.- Errors are machine-actionable. Every failure is
{error: {statusCode, code, message, issues?, schema?, retryable, hint?}}— a stablecode(never parsemessage), per-fieldissues, the violated section's JSON Schema inline, and a one-line remediation hint. MCP tool errors carry the same envelope as REST, so one parser self-corrects on both surfaces. - The API documents itself to agents.
app.start()serves/llms.txt(compact endpoint index) and/llms-full.txt(full inlined schemas) from the same registry as/openapi.json;generateAgentContext()renders an agent-context manual (CLAUDE.md shape) for the live API. Components append sections via theAX_SECTION_TAGbinding tag —installMcpHttpadds the MCP tool listing automatically. - Tool definitions are a context budget.
mcpServer.toolCostReport()token-prices everytools/listentry and totals what each connection costs a caller's context window;formatToolCostReport()flags over-budget tools.
Three packages were rewritten rather than ported wholesale because the upstream code carries baggage the fork doesn't want:
openapi— the upstream@loopback/openapi-v3is built aroundopenapi3-ts@2.xloose types and a JSON-Schema-via-@loopback/repository-json-schemapipeline. We emit OpenAPI 3.1.1 directly from Zod viaz.toJSONSchema({target: 'draft-2020-12'}), which aligns with OpenAPI 3.1's dialect.rest— the upstream@loopback/restis ~10k LoC of sequences, actions, and middleware composition. We have a minimal RestServer that handles routing, Zod request/body validation, error mapping, and serves the OpenAPI document.mcp-inspector— the official@modelcontextprotocol/inspectoris a CLI tool, not an embeddable UI. We ship a small DOM-built inspector that talks to the in-process MCPServer directly via a JSON API.
The DI foundation (metadata, context, core) and the
http-server / express integrations are faithful ESM ports — same
source as upstream LoopBack 4, with .js extensions on relative
imports, lodash → lodash-es, and p-event v6 named exports. The
DI semantics (binding scopes, @inject, providers, interceptors,
extension points, observers) match @loopback/context and
@loopback/core exactly; if you know LB4 DI, you already know this.
These are deliberately not in the framework — they reintroduce the upstream complexity the rewrite walked away from.
- LB4 sequences/actions (
findRoute → parseParams → invoke → send → rejectDI pipeline).RestServeris a monolithic dispatcher; per-route customization lives on the decorator, cross-cutting in middleware/interceptors, and deeper changes in subclassingRestServerand overridingdispatch/sendResult/sendError. @loopback/repositoryand theFilter<T>/Where<T>helpers.x-ts-typeinlining — replaced by Zod schemas attached to route metadata.@oas.deprecated/@oas.tags/@oas.visibilitydecorator namespace.
- Middleware composition —
@agentback/expressships the LB4 middleware machinery, andRestApplicationexposesapp.middleware(...)andapp.expressMiddleware(...). Registered middleware runs through the framework's chain before every route handler. - CORS — set
cors: true(or passCorsOptions) onRestServerConfig; thecorspackage is mounted globally. - MCP over Streamable HTTP —
@agentback/mcp-http'sinstallMcpHttp(app)exposes the in-process MCP server at/mcpso remote clients (Claude, Cursor) can reach the same tools; stdio remains the default. Supports OAuth 2.1 resource-server protection (auth:option +/.well-known/oauth-protected-resource), scope-based tool ACL (@tool(..., {scope})), and DNS-rebinding protection.
- MCP SSE (legacy) transport — Streamable HTTP is supported (including its server→client SSE stream with resumability); the deprecated standalone HTTP+SSE transport is not.
We use Vitest (2000+ tests across the workspace), not LB4's mocha/should ports. Tests run against built dist/ — pnpm build before pnpm test.
Contributions welcome — see CONTRIBUTING.md for setup, conventions, and the proposal process.
pnpm install # install all workspace deps
pnpm build # tsc -b across the workspace
pnpm test # vitest run — IMPORTANT: requires a prior `pnpm build`
pnpm -F <pkg> build # build a single packageTests run against built dist/. vitest.config.ts globs
packages/*/dist/__tests__/**/*.{test,spec,unit,integration,acceptance}.js,
so editing a .ts file requires pnpm build (or pnpm build:watch)
before pnpm test sees the change.
TypeScript project references keep incremental builds fast. ESLint flat config + Prettier match the LB4 conventions (single quotes, no bracket spacing, 80 col).
Schemas live on the decorator (@get/@post/...'s options object,
@tool's input/output). The handler always receives one
positional input bundle whose type is derived via z.infer. @inject
parameters live at slot 1+ when schemas are declared; when no schemas
are declared, slot 0 is free for @inject too. The slot-0 reservation
is enforced at decoration time, and URL placeholders are cross-checked
against the path: schema at app.start().
MIT.
