# Add Agentback to a product

You are a coding agent. Your user wants AI agents that use their product (an MCP server, a CLI, or an HTTP API), and people who use its web app, to be able to report problems they run into. Agentback collects those reports in an inbox the user reads at https://agentback.app/app.

Follow these steps in order. Keep the change small and in the style of the existing code.

## 1. Get the project key

You need the project's public key. It starts with `af_`. If the user already gave you one, use it and go to Step 2. Otherwise connect to the user's Agentback account; this works even if they have no account yet.

Start a connection. Suggest the product's name (for example from package.json or the README) as `project_name`, and name yourself in `agent.name`:

```sh
curl -X POST https://agentback.app/api/agent/connect \
  -H 'Content-Type: application/json' \
  -d '{"project_name":"My product","agent":{"name":"Claude Code"}}'
```

```json
{
  "device_code": "af_dc_...",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://agentback.app/connect",
  "verification_uri_complete": "https://agentback.app/connect?code=BCDF-GHJK",
  "expires_in": 900,
  "interval": 5
}
```

Show the user `verification_uri_complete` and `user_code`, and ask them to open the link. If you can open a browser for them, do. They sign up or sign in, check that the code matches, pick or create the project, and approve. Keep `device_code` to yourself.

Meanwhile poll every `interval` seconds until `expires_in` runs out:

```sh
curl -X POST https://agentback.app/api/agent/connect/token \
  -H 'Content-Type: application/json' \
  -d '{"device_code":"af_dc_..."}'
```

| Response                                                                               | Meaning                                                    |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| 400 `{"error":"authorization_pending"}`                                                | The user has not approved yet. Keep polling.               |
| 400 `{"error":"expired_token"}`                                                        | The code expired. Start a new connection.                  |
| 403 `{"error":"access_denied"}`                                                        | The user denied it. Stop and ask them what they want.      |
| 429                                                                                    | You are polling too fast. Wait a minute, then poll slower. |
| 200 `{"project_key":"af_...","project_name":"...","origin":"...","connect_url":"..."}` | Connected. Use `project_key` from here on.                 |

The user keeps the project's server-only secrets; you only ever receive the public key. Tell the user they can watch reports arrive at `connect_url`.

The project key is write-only: it can submit reports but cannot read them, so it is safe to commit and ship inside a CLI or a locally installed MCP server.

The project also has two server-only secrets. Never ask the user to paste them into this chat, and never write them into source code, prompts, client code, or anything that ships to end users. Read them from environment variables:

| Value           | Prefix   | Environment variable        | Where it may live                        |
| --------------- | -------- | --------------------------- | ---------------------------------------- |
| Project key     | `af_`    | (can be a constant)         | Anywhere, including shipped code         |
| Secret key      | `af_sk_` | `AGENTBACK_SECRET_KEY`      | Your backend or a hosted MCP server only |
| Identity secret | `af_id_` | `AGENTBACK_IDENTITY_SECRET` | Your backend only, to sign account IDs   |

## 2. Find where the product is used

Look through the repository and list every surface agents or people use. A product can have more than one; integrate each.

| Surface    | Signs                                                                                      | Go to   |
| ---------- | ------------------------------------------------------------------------------------------ | ------- |
| MCP server | `@modelcontextprotocol/sdk` or `@modelcontextprotocol/server`, `McpServer`, `registerTool` | Step 3a |
| CLI        | a `bin` entry in package.json, commander, yargs, or a hand-written argv parser             | Step 3b |
| HTTP API   | a backend that agents call, in any language                                                | Step 3c |
| Web app    | pages people sign in to or browse: a React, Vue or Svelte app, Next.js, or plain HTML      | Step 3d |

Steps 3a and 3b use the `agentback` npm package, which needs Node.js 20 or later (it also runs on Cloudflare Workers, Deno and Bun). For a product in another language, use Step 3c.

Tell the user which surfaces you found and which you are about to integrate.

## 3a. MCP server

```sh
npm install agentback
```

The integration also needs `@modelcontextprotocol/sdk` 1.23+ (or `@modelcontextprotocol/server` 2.x) and `zod` 4.2+, which an MCP server usually has already. Use the repository's package manager.

Decide where the server runs:

- **On users' machines** (stdio, installed with npx and similar): use the project key.
- **On the user's own servers** (hosted Streamable HTTP or SSE): use the secret key from `AGENTBACK_SECRET_KEY`.

Add this where the server registers its other tools. If the server is created per session or per request, add it in the same place so every instance gets it.

```ts
import { Agentback } from 'agentback';
import { attachToMcp } from 'agentback/mcp';

// On users' machines:
const agentback = new Agentback({ projectKey: 'af_...' });
// On your own servers instead:
// const agentback = new Agentback({ secretKey: process.env.AGENTBACK_SECRET_KEY });

// Registers a send_feedback tool whose description tells agents when to use it.
attachToMcp(server, agentback, { product: 'Product name' });
```

`server` is the `McpServer` instance. Use the product's real name for `product`. Reports carry source `mcp`, the connected MCP client's name and version, and the server's own version from the `McpServer` constructor (pass `version` to override it). Agents say how their task ended (`outcome`: blocked, worked around, or friction), can name the tool that had the problem, say what they expected and what happened instead, and suggest what would have helped. Tool names agents copy with an MCP client prefix such as `mcp__myserver__search` are stored as `search`. If the server already has a tool called `send_feedback`, pass `toolName` to pick another name.

## 3b. CLI

```sh
npm install agentback
```

Add a `feedback` subcommand. The CLI ships to users, so use the project key.

```ts
import { Agentback } from 'agentback';
import { feedbackHint, runFeedbackCommand } from 'agentback/cli';

const agentback = new Agentback({ projectKey: 'af_...' });

// mycli feedback "The response is missing a published date" --category bug --outcome worked_around
if (argv[0] === 'feedback')
  process.exitCode = await runFeedbackCommand(agentback, argv.slice(1), {
    program: 'mycli',
    version: pkg.version, // the CLI's version, attached to every report
  });
```

`runFeedbackCommand` parses its own options and prints its own help, so with commander hand it the raw arguments:

```ts
program
  .command('feedback')
  .description('Report a problem with mycli to its developers')
  .helpOption(false)
  .allowUnknownOption()
  .argument('[args...]')
  .action(async (args: string[]) => {
    process.exitCode = await runFeedbackCommand(agentback, args, {
      program: 'mycli',
    });
  });
```

The command accepts `--outcome blocked|worked_around|friction` (required: how the task ended) and `[--category bug|feature_request|docs|other] [--suggestion text] [--intent text] [--expected text] [--actual text] [--tool command] [--request-id id] [--model name] [--json] <message>`, and `-` reads the message from stdin. Pass the CLI's version as `version` so reports say which release they are about; read it the way the CLI already does for `--version`.

Then, where the CLI prints errors, print the hint on stderr so agents learn they can report the problem:

```ts
console.error(feedbackHint('mycli'));
```

Replace `mycli` with the CLI's real command name everywhere.

## 3c. HTTP API, or any language

Agents can post to the project's public address with no secret:

```sh
curl -X POST 'https://agentback.app/f/af_...' \
  -H 'Content-Type: application/json' \
  -d '{"message":"The response is missing a published date","category":"bug","outcome":"worked_around","agent":{"name":"My agent"}}'
```

Agents only use this address if they are told about it. The same address answers a GET with instructions for agents in Markdown: when to report, the fields, and what each response means. So one line is enough. Add it to the product's agent-facing documentation: the README section agents read, `llms.txt`, a skill, an `AGENTS.md`, or the system prompt the product ships. Replace the product name and address with the project's own.

```text
If Product name gets in the way of your task, report the problem to its developers. Read https://agentback.app/f/af_... for how; it takes one POST request.
```

If agents already call the product's own API with their user's credentials, an alternative is a feedback endpoint on that API that forwards to Agentback with the secret key. The backend can then attach the account and details of the failed call, which the public address does not accept:

```ts
// Node.js backend
import { Agentback } from 'agentback';
const agentback = new Agentback({
  secretKey: process.env.AGENTBACK_SECRET_KEY,
});

await agentback.send(
  { message, intent, category, request_id },
  {
    source: 'api',
    account: tenantId, // an opaque ID, never an email
    context: { endpoint: '/search', status_code: 200, duration_ms: 320 },
  },
);
```

In other languages, send the same JSON to `POST https://agentback.app/feedback` with `Authorization: Bearer $AGENTBACK_SECRET_KEY`, adding `source`, `account` and `context` next to the report fields. Validate the agent's input before forwarding, and do not report success to the agent when forwarding fails.

## 3d. Web app

People using the web app get a Feedback button and a small panel. It uses the project key, which is safe in page source. Add the script tag once, to the layout every page shares (`index.html`, the root layout, or the base template), just before `</body>`:

```html
<script
  src="https://agentback.app/widget.js"
  data-project="af_..."
  data-product="Product name"
  async
></script>
```

If the app has a bundler and you prefer npm, call this once on the client instead (never during server rendering):

```ts
import { createFeedbackWidget } from 'agentback/web';

createFeedbackWidget({ projectKey: 'af_...', product: 'Product name' });
```

Reports carry source `web` and the page's origin and path. The panel follows the visitor's light or dark setting, uses the page's font, and is orange by default. Leave these defaults unless the user asks, or the app's own design clearly calls for them:

| Attribute (npm option)          | Use                                                                                                         |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `data-color` (`color`)          | `neutral`, `blue`, `violet` or a hex colour to match the brand                                              |
| `data-color-dark`               | A different button colour in dark mode (npm: `color: { light, dark }`)                                      |
| `data-text-color` (`textColor`) | The button's text colour as a hex; `data-text-color-dark` for dark mode. Defaults to whichever reads better |
| `data-launcher="false"`         | Hide the floating button when the app has a menu or header to open it from                                  |
| `data-agentback-open`           | Put on the app's own Feedback menu item or button; clicking it opens the panel next to it                   |
| `data-theme` (`theme`)          | `light` or `dark` when the app has its own theme switch; call `Agentback.setTheme()` on change              |
| `data-categories`               | Relabel, reorder or hide the types, e.g. `bug:Report a bug,feature_request:Suggestion` or `false`           |
| `data-locale` (`locale`)        | `en` or `zh`; defaults to the page's `lang`                                                                 |

## 4. Identify accounts (optional)

Reports are anonymous unless they carry an account. Ask the user whether they want to know which of their users sent each report. If not, skip this step.

Use an opaque account ID, never an email.

- **Secret-key clients** (hosted MCP server, backend): pass the signed-in account directly, for example `attachToMcp(server, agentback, { product, identify: () => ({ account: session.accountId }) })`.
- **Project-key clients** (local MCP server, CLI, web app): a public key cannot vouch for an account, so the user's backend signs the account ID with the identity secret and the client passes the signature along. A good place is the response the CLI, MCP server or web app already gets when the user logs in.

```ts
// On the backend:
import { signAccount } from 'agentback';
const signature = await signAccount(
  process.env.AGENTBACK_IDENTITY_SECRET,
  accountId,
);

// In the MCP server or CLI:
attachToMcp(server, agentback, {
  product: 'Product name',
  identify: () => ({ account: accountId, signature }),
});
runFeedbackCommand(agentback, args, {
  program: 'mycli',
  identify: () => ({ account: config.accountId, signature: config.signature }),
});

// In the web app, once the user is signed in (or data-account and
// data-signature on the script tag in server-rendered pages):
window.Agentback?.identify({ account: accountId, signature });
// With npm: the object createFeedbackWidget returned has the same identify().
```

The signature is a hex HMAC-SHA256 of the trimmed account ID keyed with the identity secret, so a backend in another language can compute it without the package. A wrong signature is rejected with 401. Rotating the project's keys replaces the identity secret and invalidates existing signatures.

If the user does not have a backend, skip signing; reports stay anonymous.

## 5. Verify

Send one real test report through the surface you integrated:

- MCP: run the server and call the `send_feedback` tool, for example with the MCP Inspector. Or ask an agent connected to it to do something the server's tools cannot do; it should report that before it finishes.
- CLI: run `mycli feedback "Test report from the Agentback setup" --category other --outcome friction`.
- HTTP: run the curl command from Step 3c.
- Web: open the app, click Feedback, and send a test report. If you cannot open a browser, ask the user to.

A successful submission returns HTTP 201 with `{ "id": "fb_...", "status": "new" }`. The project's Connect page (`connect_url` from Step 1) switches from "Waiting for the first report" to "Connected" when it arrives; ask the user to confirm it there. They can mark the test report Ignored afterwards. Run the repository's own tests, type checks and linters before you finish.

Finish by telling the user what you changed, which key each surface uses, and which environment variables they still need to set on their servers.

## Reference

| Field                   | Limit or accepted values                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| message                 | Required, nonempty, at most 4000 characters                                                         |
| intent                  | At most 2000 characters; what the agent was trying to do                                            |
| expected                | At most 2000 characters; what the agent expected to happen                                          |
| actual                  | At most 4000 characters; what happened instead, including the error text                            |
| tool                    | At most 128 characters; the tool, command or endpoint involved                                      |
| version                 | At most 64 characters; the product's version                                                        |
| category                | `bug`, `feature_request`, `docs`, `other` (default)                                                 |
| outcome                 | `blocked` (could not finish), `worked_around` (finished another way), `friction` (finished after retries or guessing) |
| suggestion              | At most 2000 characters; what would have helped                                                     |
| request_id              | At most 256 characters; the ID of the failed call                                                   |
| agent.name, agent.model | At most 128 characters each                                                                         |
| source                  | `direct`, `mcp`, `cli`, `web` on the public address; `api`, `mcp`, `cli`, `web` with the secret key |
| page                    | An http(s) address, at most 2048 characters; the page a person sent the report from                 |
| account                 | At most 256 characters; with `account_signature` on the public address                              |
| context                 | `endpoint`, `status_code`, `duration_ms`; secret key only                                           |

Unknown fields are rejected and bodies are limited to 64 KiB.

| Status | Meaning                                              |
| ------ | ---------------------------------------------------- |
| 201    | Stored                                               |
| 400    | Invalid JSON or fields; `details` lists each problem |
| 401    | Missing or invalid secret key or account signature   |
| 404    | Unknown project key                                  |
| 413    | Body larger than 64 KiB                              |
| 415    | Content-Type is not `application/json`               |
| 429    | Rate limited; retry after 60 seconds                 |
| 503    | Storage unavailable; the report was not stored       |

The SDK's `send()` throws `AgentbackError` on failure, with `retryable` set for 429, 5xx and network errors. Feedback is best effort: never let a failed report break the product's main task.

Package source and full README: https://github.com/superagents-lab/agentback-js
