Architecture overview

How the pieces fit, how a request flows, and how the packages layer. If you've read the concepts, this ties them together.

A polished, standalone version of the system diagram below lives at diagrams/system-architecture.html — open it in a browser.

The system at a glance

Everything is a binding in one Context. The servers are bindings too; they discover your code by tag at startup and expose it over their transport. The same Zod schemas feed validation, OpenAPI, and MCP contracts.

Architecture diagram — text source below
diagram source (mermaid)
graph TD
  subgraph Container["Application — one DI Context"]
    direction TB
    Schemas["Zod schemas (shared)"]
    Ctrls["controllers.* [controller]"]
    Tools["tool classes [mcpServer]"]
    Svcs["services.* / providers / config"]
    Ctrls -. @inject .-> Svcs
    Tools -. @inject .-> Svcs
    Ctrls -. carry .-> Schemas
    Tools -. carry .-> Schemas
  end
  Container --> RS["RestServer<br/>(servers.RestServer)"]
  Container --> MS["MCPServer<br/>(servers.MCPServer)"]
  RS -->|findByTag controller| Ctrls
  MS -->|findByTag mcpServer| Tools
  RS --> HTTP["HTTP + /openapi.json"]
  HTTP --> SUI["/explorer (Swagger UI)"]
  MS --> STDIO["stdio transport"]
  MS --> INS["/mcp-inspector"]
  Schemas --> OAS["OpenAPI 3.1 (z.toJSONSchema)"]
  Schemas --> MCPC["MCP input/output schema"]
  OAS --> HTTP
  MCPC --> MS

The shape to remember: one container in the middle; servers on the edges discovering bindings by tag; schemas radiating out to every contract.

How a REST request flows

Two standalone diagrams cover the middleware layer: the structure — the group-sorted cascade (cors → parseBody → middleware) that fronts every route — at diagrams/middleware-chain.html, and a live request trace through the resolved order (POST /mcp ①→⑦, plus the OPTIONS-preflight short-circuit) at diagrams/mcp-request-lifecycle.html. Open them in a browser.

Architecture diagram — text source below
diagram source (mermaid)
sequenceDiagram
  participant C as Client
  participant MW as Middleware chain
  participant RS as RestServer.dispatch
  participant Z as Zod (route bundle)
  participant DI as resolveInjectedArguments
  participant H as Your handler
  C->>MW: HTTP request
  MW->>RS: (after CORS / rate-limit / tracing)
  RS->>Z: validate path/query/headers/body
  alt invalid
    Z-->>C: 422 / 400 + ZodError.issues
  else valid
    RS->>DI: resolve @inject args (slot 1+)
    DI->>H: input bundle (slot 0) + deps
    H-->>RS: return value
    RS->>Z: validate against `response` (log on mismatch)
    RS-->>C: status + JSON (sendResult)
  end

An MCP tool call follows the analogous path inside MCPServer.dispatchTool: parse input → weave injects → apply method → validate output.

How discovery works

No router file, no central switch. Servers find your code by querying the container at start time.

Architecture diagram — text source below
diagram source (mermaid)
graph LR
  Dec["@api + @mcpServer class"] -->|"app.restController (one call)"| Bind["one binding + tags<br/>(controller · extensionFor MCP_SERVERS)"]
  Start["app.start()"] --> SrvR["RestServer"]
  Start --> SrvM["MCPServer"]
  SrvR -->|"findByTag('controller')"| Bind
  SrvM -->|"findByTag('mcpServer')"| Bind
  SrvR -->|read decorator metadata| Routes["routes + Zod bundles"]
  SrvM -->|reflect @tool/@resource/@prompt| Reg["SDK registrations"]

This is why "add a feature" = "add a binding": the discovery step picks it up with zero wiring.

Package layering

The DI foundation is the base; servers, integrations, and the agent runtime build up from it. Every package has its own README.md with exports and a usage snippet. An arrow means "depends on."

Architecture diagram — text source below
diagram source (mermaid)
graph BT
  subgraph foundation["DI foundation (ports of @loopback/*)"]
    metadata["metadata"]; context["context"]; core["core"]
  end
  subgraph crosscut["cross-cutting"]
    config["config"]; security["security"]; testlab["testlab"]
  end
  subgraph http["HTTP / REST / OpenAPI"]
    httpserver["http-server"]; express["express"]; openapi["openapi"]
    rest["rest"]; restexp["rest-explorer"]; client["client (standalone)"]
  end
  subgraph auth["auth stack"]
    authn["authentication"]; authjwt["authentication-jwt"]
    authoauth2["authentication-oauth2"]; authz["authorization"]
  end
  subgraph pay["metering + payments"]
    metering["metering"]; payments["payments"]
  end
  subgraph mcpfam["MCP"]
    mcp["mcp"]; mcphttp["mcp-http"]
    mcpclient["mcp-client"]; mcphost["mcp-host"]; mcpconnect["mcp-connect"]
  end
  subgraph obs["observability + edge"]
    health["extension-health"]; metrics["extension-metrics"]
    ratelimit["extension-rate-limit"]
  end
  subgraph ui["console / explorers"]
    theme["console-theme"]; ctxexp["context-explorer"]
    schemaexp["schema-explorer"]; mcpins["mcp-inspector"]; console["console"]
  end
  context --> metadata
  core --> context
  config --> core
  security --> core
  httpserver --> core
  express --> httpserver
  openapi --> core
  rest --> express
  rest --> openapi
  rest --> authn
  rest --> authz
  authn --> security
  authjwt --> authn
  authoauth2 --> authn
  authz --> security
  metering --> rest
  metering --> mcp
  payments --> metering
  payments --> mcp
  mcp --> core
  mcphttp --> mcp
  mcphost --> mcpclient
  mcpconnect --> mcpclient
  restexp --> rest
  health --> rest
  metrics --> rest
  ratelimit --> rest
  ctxexp --> rest
  schemaexp --> rest
  schemaexp --> mcp
  mcpins --> mcp
  mcpins --> mcpconnect
  console --> mcpins
  console --> ctxexp
  console --> schemaexp

Notes:

Agent-facing runtime pieces

The packages in this repo stop at framework and substrate boundaries. They give agent applications the core primitives for tools, durable work, gatewaying, and plugins, but they do not currently ship a higher-level turn loop or orchestration runtime.

Architecture diagram — text source below
diagram source (mermaid)
graph BT
  subgraph contracts["tool and API contracts"]
    openapi["openapi"]; mcp["mcp"]; rest["rest"]
  end
  subgraph runtime["agent-facing substrates"]
    messaging["messaging"]; bullmq["messaging-bullmq"]
    mcphost["mcp-host"]; mcpclient["mcp-client"]; plugin["plugin"]
    files["files"]; filess3["files-s3"]
  end
  subgraph platform["platform concerns"]
    auth["authentication / authorization"]
    metering["metering / payments"]
    otel["extension-otel"]
  end
  core(["core / context"])
  openapi --> core
  rest --> openapi
  mcp --> core
  messaging --> core
  bullmq --> messaging
  files --> core
  filess3 --> files
  rest --> files
  mcphost --> mcpclient
  plugin --> core
  auth --> core
  metering --> rest
  metering --> mcp
  otel --> rest
  otel --> mcp
  otel --> messaging

Where the boundaries are the same artifact

The thing that distinguishes this framework: a single Zod schema is the validator, the z.infer type, the OpenAPI parameter/response, the MCP input/output, and the rendered docs — simultaneously. Changing it changes all of them in one edit, and disagreements surface as a TypeScript error at the decorator, a startup throw, or a failing test — three localized signals instead of one vague one.

That property, not any single feature, is the framework's bet. The full argument is in the boundary-coherence design thesis.

Next