# AI Agent Readiness

Every compiled Quickback Worker advertises itself to AI agents and MCP clients — no user wiring required. One config flag controls the whole discovery surface.

## What the compiler generates

When `agents` is enabled (the default), the Worker serves:

| Route | What it is |
|-------|-----------|
| `GET /robots.txt` | AI-bot policy with the [Cloudflare Content Signals](https://developers.cloudflare.com/bots/concepts/content-signals/) preamble and explicit `Allow:` rules for GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, Applebot-Extended, Bytespider, MistralAI, Cohere, DuckAssist. |
| `GET /llms.txt` | Agent-facing summary of the API — project name, authentication, resource list, endpoint conventions — derived from your own config. |
| `GET /.well-known/oauth-protected-resource` | [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) metadata declaring this Worker as a protected resource. |
| `GET /.well-known/oauth-authorization-server` | [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) authorization-server metadata. When the `oauthProvider` plugin is enabled it **delegates to Better Auth's real OAuth 2.1 surface** (`/oauth2/authorize`, `/oauth2/token`, `/oauth2/register` dynamic client registration, PKCE); otherwise it advertises only the truthful token-mint subset. |
| `GET /.well-known/api-catalog` | [RFC 9727](https://datatracker.ietf.org/doc/html/rfc9727) catalog linking at `/openapi.json`. |
| `GET /.well-known/mcp.json` | MCP Server Card — discoverable metadata for the MCP server below. |
| `POST /mcp` | Auto-generated MCP-over-HTTP server. One tool per OpenAPI operation, JWT auth forwarded through the same middleware as the REST API. |

## Default behavior

```ts title="quickback.config.ts"
import { defineConfig } from '@quickback/compiler';

export default defineConfig({
  name: 'my-api',
  // …
  // agents: true is the default — no need to write it.
});
```

Run `quickback compile` and every endpoint above ships in the Worker.

## Opting out

```ts title="quickback.config.ts"
export default defineConfig({
  // …
  agents: false,  // disables everything
});
```

## Fine-grained control

Pass an object to toggle individual surfaces:

```ts title="quickback.config.ts"
export default defineConfig({
  // …
  agents: {
    robotsTxt: true,        // default
    llmsTxt: true,          // default
    mcp: true,              // default
    oauthDiscovery: true,   // default
    apiCatalog: true,       // default
    allowAiCrawlers: true,  // default — flip to false to Disallow in robots.txt
  },
});
```

`mcp` also accepts an object for an authenticated-only MCP surface:

```ts title="quickback.config.ts"
export default defineConfig({
  // …
  agents: {
    mcp: { requireAuth: true },  // /mcp challenges anonymous callers → interactive OAuth
  },
});
```

See [Interactive OAuth](#interactive-oauth-requireauth) below.

## The MCP server

### Tools

Every operation in your generated OpenAPI spec becomes one MCP tool. The tool name is the operation's `operationId` (e.g. `listCustomers`, `createInvoice`, `sendInvoice`). Each tool's `inputSchema` is a JSON Schema combining the request body with every parameter in the operation's **mounted** path — including the `:params` contributed by a namespace prefix (e.g. `/scope/:scopeId/...`), so a standalone or namespace action is always callable, not just record-based ones. Nested `z.object` inputs are preserved as nested objects rather than flattened, so the model sees each field's real type.

`tools/call` re-enters the same Hono app via `app.request(...)` — so **every existing middleware runs exactly as it would for a direct REST call**: authentication, firewall, access control, guards, masking. No bypass.

### Access-filtered `tools/list`

`tools/list` only returns tools the calling identity could actually invoke. Each generated tool entry carries the operation's access metadata (`roles`, `userRole`, `or` / `and` combinators) and `tools/list` filters against `ctx` before responding:

- **Unauthenticated callers** see only tools whose access declares the `PUBLIC` pseudo-role, or operations that have no security requirement at all (e.g. sign-in / sign-up).
- **Authenticated callers** see tools whose role gate intersects their `ctx.roles` or `ctx.userRole`. Platform-operator endpoints don't appear for callers outside the required user roles.
- **Function-form access** (escape hatch) is opaque at list time — surfaced to any authenticated caller, then evaluated for real on `tools/call`.
- **Record predicates** are dropped from the list-time check (no record yet) and applied normally on the actual call.

This is recon-mitigation, not a security boundary. Hiding a tool from the list never weakens the underlying gate — `tools/call` still runs the full middleware chain and rejects unauthorized invocations the same way a direct REST call would.

The unauthenticated `GET /mcp` discovery payload returns server info only; it does **not** enumerate tool names. Inventory is exclusively available via the access-filtered `tools/list`.

### Request limits

Unless `requireAuth` is set, `/mcp` accepts anonymous POSTs, so both the body
and the JSON-RPC batch array are attacker-controlled. Two limits apply:

| Limit | Value | Behaviour when exceeded |
| --- | --- | --- |
| Request body | 1 MiB | `413` with JSON-RPC error `-32600` |
| Batch elements | 50 | `413` with `-32600`; split the batch |

The body is read through a byte counter rather than buffered whole, so an
oversized request is cut off mid-stream instead of being materialized first.
A size rejection is deliberately distinct from `-32700` *Parse error* — a
client told "parse error" would retry the same oversized body.

Batches are dispatched in waves of 8 rather than all at once. Every element
becomes its own internal request through the full middleware chain, so peak
concurrency stays flat regardless of how large a batch is submitted.

### Authentication

MCP clients pass their credentials through the standard `Authorization: Bearer <jwt>` header. The generated MCP handler forwards the header verbatim into the internal request, and the same JWT middleware that guards `/api/v1/*` validates it.

Mint a JWT from an authenticated session:

```bash
curl -X POST https://my-api.example.com/api/v1/token \
  -H "Cookie: better-auth.session_token=..." \
  -H "Content-Type: application/json"
```

### Interactive OAuth (`requireAuth`)

Statically pasting a Bearer token works, but it isn't how an MCP connector wants to authenticate — it expects to **launch a sign-in flow**. By default `/mcp` serves an anonymous session (the access-filtered public tool list), and a connector that never sees an auth challenge never offers to sign in. Set `agents.mcp.requireAuth: true` to turn `/mcp` into an authenticated-only endpoint:

```ts title="quickback.config.ts"
export default defineConfig({
  // …
  agents: { mcp: { requireAuth: true } },
});
```

This does three things:

1. **Force-enables the `oauthProvider` + `bearer` Better Auth plugins** — so `/oauth2/authorize`, `/oauth2/token`, and `/oauth2/register` ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) dynamic client registration) exist for a connector to discover, with PKCE.
2. **Challenges unauthenticated `/mcp` requests** with `401` and a `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"` header ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) §5.1). This is the signal MCP connectors use to start OAuth.
3. **Makes the root `/.well-known/oauth-authorization-server` doc delegate to Better Auth's real metadata**, so discovery resolves to working `/oauth2/*` endpoints instead of a hand-rolled stub.

The full handshake a connector runs:

```text
POST /mcp  (no token)
  → 401 + WWW-Authenticate: resource_metadata="…/.well-known/oauth-protected-resource"
GET  /.well-known/oauth-protected-resource   → authorization_servers: [<origin>]
GET  /.well-known/oauth-authorization-server → authorize/token/register + PKCE
POST /oauth2/register                        → dynamic client registration
GET  /oauth2/authorize?code_challenge=…      → redirect to your Account login → consent
POST /oauth2/token  (code + code_verifier)   → access token
POST /mcp  (Bearer access token)             → authenticated tool list
```

The login and consent screens are served by your [Account SPA](/ui/account) (`ACCOUNT_URL/login`, `ACCOUNT_URL/consent`), so users sign in with whatever methods you already enabled (Google, GitHub, email, passkey). No connector-specific configuration is required — adding the server URL is enough.

> `requireAuth` removes anonymous public-tool access over `/mcp`. Use it when the MCP surface is meant for authenticated users; leave it off (the default) for servers that expose genuinely public tools.


### Split authorization server / resource server (multi-app SSO)

By default a Worker is both the authorization server (AS) and the resource server — it issues *and* verifies its own OAuth tokens. For multi-app SSO you can run **one canonical AS** and **many resource servers** (each hosting its own `/mcp`) that share a single auth database (the same D1, e.g. via `crossSubDomainCookies` on a shared parent domain). Two config fields wire it up — both under `auth.config.oauth`:

**On each resource server** — point discovery at the external AS and verify its tokens:

```ts
auth: {
  config: {
    oauth: {
      enabled: true,
      issuerUrl: "https://auth.example.com/auth/v1",  // the canonical AS
    },
  },
},
agents: { mcp: { requireAuth: true } },
```

`issuerUrl` makes `/.well-known/oauth-protected-resource` advertise the AS (not this Worker), and the generated middleware verifies incoming `Bearer` access tokens against the **shared JWKS** (`iss` must equal `issuerUrl`, `aud` must equal this Worker's `/mcp`, plus `exp` + live-session binding). Because the auth DB is shared, the resource server reads the AS's signing keys and the session row directly — no cross-Worker HTTP call.

**On the authorization server** — register the resource audiences it may mint tokens for ([RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) resource indicators):

```ts
auth: {
  config: {
    oauth: {
      enabled: true,
      audiences: [
        "https://crm.example.com/mcp",
        "https://billing.example.com/mcp",
      ],
    },
  },
},
```

`audiences` is an array — list every federated resource server. The AS's own origin (and `/mcp`) is always included automatically, so you only list the *other* servers; a token requested with `resource=https://crm.example.com/mcp` is then minted with that audience instead of rejected.

> `agents.mcp.requireAuth` is the documented switch for an authenticated MCP server, and it now emits the full OAuth token-verification path on its own — a resource server needs only `requireAuth` + `oauth.issuerUrl`, not a separate `oauth.enabled` step.


### Connecting clients

**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "my-api": {
      "url": "https://my-api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer <jwt>"
      }
    }
  }
}
```

**Cursor** — use the same config shape under `.cursor/mcp.json`.

**Quick sanity check with curl:**

```bash
# List available tools
curl -X POST https://my-api.example.com/mcp \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# Call a tool
curl -X POST https://my-api.example.com/mcp \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"listCustomers","arguments":{"limit":10}}}'
```

### Transport

The server speaks the **Streamable HTTP** MCP transport — JSON-RPC 2.0 over `POST`. A plain `GET /mcp` returns the server info document for sanity checks.

The MCP route does **not** emit its own CORS headers. The Worker's existing `cors()` middleware applies uniformly, so browser-origin access is governed by the same allowlist as the rest of the API (`auth.trustedOrigins` + the deploy origin). Server-to-server clients (Claude Desktop, Cursor, MCP Inspector) connect with a Bearer token and don't need browser CORS at all. If you want a browser-based MCP client (ChatGPT, Claude.ai) to connect cross-origin, add its origin to `auth.trustedOrigins` like any other web client.

## Verification

After you deploy, scan both the REST and MCP surfaces:

```bash
# Agent-readiness scan (external)
open https://isitagentready.com/?url=https://my-api.example.com

# MCP Inspector (official tool)
npx @modelcontextprotocol/inspector https://my-api.example.com/mcp
```
