# Connecting MCP clients to Action Signal AI

The MCP server is at:
- **Production**: `https://www.actionsignal.ai/api/mcp`
- **Local dev**:  `http://localhost:3000/api/mcp`

There are two ways to authenticate, both supported indefinitely:

1. **OAuth 2.1 (recommended for end users)** — the in-app "Add Custom
   Connector" flow on Claude Desktop, Claude Code, ChatGPT, and Codex
   discovers our authorization server, opens a browser for consent, and
   "just works." Tokens rotate automatically. **No JSON editing.**
2. **Personal Access Tokens (PATs, for CI / headless / power users)** —
   issue a long-lived bearer token at `/settings/security/tokens`, paste
   it into your client's config file. Same scope model as OAuth; just no
   browser flow and no expiry rotation.

> PATs are **not deprecated** and won't be. They're the right tool for
> headless agents, CI scripts, and engineers who prefer config files.
> The recommendation order below is "what most users want first."

## OAuth 2.1 — recommended

### Claude Desktop

1. Open **Settings → Connectors → Add Custom Connector**.
2. Paste: `https://www.actionsignal.ai/api/mcp`
3. Click **Add**. A browser tab opens.
4. Sign in to Action Signal AI (if you weren't already).
5. The consent screen shows which scopes the client wants. Review and
   click **Connect to Claude Desktop**.
6. The browser closes and tools appear in Claude Desktop within a few
   seconds.

To disconnect, either remove the connector from Claude Desktop or revoke
the grant at **Action Signal AI → Settings → Security → Tokens → Active
connections**.

### Claude Code

`claude mcp add` will offer to register via OAuth if it sees a
`/.well-known/oauth-authorization-server` response (we serve one). Walk
the prompts; the consent flow is identical to Claude Desktop.

### ChatGPT

Custom Connectors → Add → MCP Server. Paste the URL; ChatGPT discovers
the authorization endpoint, opens the consent flow in a popup.

### Codex CLI

```bash
codex mcp add action-signal-ai --url https://www.actionsignal.ai/api/mcp
```

Codex detects OAuth support from the URL and opens the consent browser flow
automatically — no `--transport` or `--oauth` flag needed (older Codex
versions required both; current versions infer them). If the server is
already added but not signed in, re-authenticate with:

```bash
codex mcp login action-signal-ai
```

Verify the connection:

```bash
codex mcp get action-signal-ai
codex mcp list
```

If Action Signal's tools don't show up immediately, start a new Codex task
or restart Codex.

### What you'll see on the consent screen

- **Action Signal AI** branding at the top (so it's unambiguous which
  resource you're authorizing).
- The MCP client's display name (e.g. *"Claude Desktop wants access to
  your data"*).
- Your tenant — *"Granting from: <Your Organization>"*.
- A plain-English list of scopes. Examples:
  - *"View your tracked products and their data health"*
  - *"Refresh product data — uses your Apify budget, capped at 50
    calls/day per connection"*
  - *"Trigger new optimization analyses — counts against your AI credit
    budget"*
- **Connect** and **Cancel** buttons.

If something looks off (wrong client name, wrong tenant, scopes you
don't recognize), cancel and reach out to support.

## Personal Access Tokens — headless / CI / power users

Get a PAT from `/settings/security/tokens`. The plaintext is shown
**once** in a modal — save it before closing.

Do not paste PATs directly into shared docs, screenshots, shell history, or
source control. Prefer the env-var patterns below, where your client reads
the token from an environment variable by name rather than storing the
literal secret in a config file or command line.

### Claude Code (config file)

Edit `~/.config/claude-code/mcp.json` (macOS/Linux) or
`%APPDATA%\claude-code\mcp.json` (Windows):

```json
{
  "mcpServers": {
    "action-signal-ai": {
      "transport": "http",
      "url": "https://www.actionsignal.ai/api/mcp",
      "headers": {
        "Authorization": "Bearer asai_pat_REPLACE_ME"
      }
    }
  }
}
```

Restart Claude Code.

### Claude Desktop (config file)

Settings → Developer → Edit Config. Same JSON shape as above. For
non-engineers, use the OAuth flow instead — or click **Quick connect**
in `/settings/security/tokens` and we'll generate a `mcp-remote`-wrapped
PAT snippet you can paste in 30 seconds.

### Codex CLI (PAT)

```bash
export ACTION_SIGNAL_PAT="asai_pat_REPLACE_ME"

codex mcp add action-signal-ai \
  --url https://www.actionsignal.ai/api/mcp \
  --bearer-token-env-var ACTION_SIGNAL_PAT
```

`--bearer-token-env-var` tells Codex which environment variable holds the
token — the token itself is never written to Codex's config file.

### ChatGPT (PAT)

Custom Connectors → Add → MCP Server. Paste the URL; choose Bearer
token auth; paste the PAT.

### Custom agents (TypeScript)

```ts
import { McpClient } from "@modelcontextprotocol/sdk/client";
import { StreamableHttpTransport } from "@modelcontextprotocol/sdk/client/streamableHttp";

const transport = new StreamableHttpTransport(
  new URL("https://www.actionsignal.ai/api/mcp"),
  {
    requestInit: {
      headers: { Authorization: `Bearer ${process.env.ACTION_SIGNAL_PAT}` },
    },
  }
);

const client = new McpClient({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const tools = await client.listTools();
```

### OpenAI Agents SDK

Action Signal is a standard Streamable HTTP MCP server — point the SDK's
hosted MCP / Streamable HTTP tool at the URL above and pass
`Authorization: Bearer <PAT>` the same way as the TypeScript example. See
OpenAI's [MCP integration docs](https://openai.github.io/openai-agents-python/mcp/)
for the SDK-specific hosted and Streamable HTTP patterns.

## What you can do (21 tools)

| Tool | Scope | Description |
|---|---|---|
| `products_list` | `read:products` | List your owned products |
| `products_get` | `read:products` | Single product details + data_health |
| `products_create` | `write:products` | Onboard a new ASIN |
| `products_archive` | `write:products` | Soft-delete |
| `products_unarchive` | `write:products` | Inverse |
| `products_demote_to_competitor` | `write:products` | Demote an owned product back to competitor-only |
| `dashboard_overview` | `read:products` | Cross-product snapshot: products, reports, jobs, credits |
| `competitors_list` | `read:products` | List competitors tracked for a product |
| `competitors_add` | `write:products` | Add a competitor by ASIN or URL |
| `competitors_remove` | `write:products` | Stop tracking a competitor |
| `product_readiness` | `read:products` | Composite readiness check before triggering analysis |
| `reports_list` | `read:reports` | Recent optimization reports |
| `reports_get_section` | `read:reports` | One section of an 8-section report |
| `voc_get_customers_say` | `read:voc` | Aspect-level sentiment + quotes |
| `personas_get` | `read:voc` | Synthetic buyer personas for a product |
| `ingestion_dispatch` | `write:ingestion` | Fetch / refresh data for a product |
| `ingestion_retry_step` | `write:ingestion` | Retry a single failed enrichment step |
| `analysis_trigger` | `write:analysis` | Kick a full optimization run for one product |
| `analysis_trigger_bulk` | `write:analysis` | Kick optimization runs across many products in one call |
| `jobs_get_status` | `read:jobs` | Poll status of any background job |
| `credits_get_balance` | `read:billing` | Credit position + per-token caps |

There are also `outreach_package_*` tools (`write:outreach` scope) — these back
an internal Action Signal team workflow for prospect trials, not something a
tenant's own MCP client would request. They're omitted from the table above;
if a connector unexpectedly lists them, that's worth flagging to us.

## Typical workflows

### Catch up on a brand's portfolio

```
Agent: "Show me the latest reports for my top 3 products."
Tools: products_list (limit=3) → reports_list (per product) → reports_get_section
```

### Onboard a new ASIN and analyze it

```
products_create(asin) → jobs_get_status (poll enrichment) →
ingestion_dispatch (after enrichment) → analysis_trigger →
jobs_get_status (poll run) → reports_get_section
```

### Compare VOC across products

```
products_list → for each: voc_get_customers_say(product_id)
```

## Limits

| Limit | Default |
|---|---|
| Per-token requests / min | 60 |
| Per-token requests / day | 5,000 |
| Per-token `write:ingestion` calls / day | 3 × your plan's product slots (5–50) |
| Per-token report credits / 24h | 25% of your plan's monthly report allotment |
| PAT expiry | 90 days (max 365) |
| OAuth access token TTL | 1 hour (refresh rotates seamlessly) |
| OAuth refresh token TTL | 30 days |
| Verify cache TTL | 30 s (max revocation latency) |

All limits are per-token. The dashboard and other tokens spend
independently. OAuth and PAT tokens share the same caps; they don't pool.

The two spend limits are **derived from your plan**, so they move when you
change tier or buy product-slot add-ons — Growth (10 slots) gets 30 ingestion
calls and 10 report credits per token per day, Pro (30 slots) gets 50 and 30.
Requests/min and requests/day are flat across plans; they exist to bound abuse,
not to meter entitlement.

When issuing a PAT you can set **lower** limits than your plan allows — useful
for a shared or long-lived token. You can't set higher ones: the mint form
rejects an over-plan value with `exceeds_tier_limit`, and enforcement clamps to
the plan ceiling on every request, so a token issued before a downgrade doesn't
keep the old allowance.

## Revoking access

- **OAuth**: `/settings/security/tokens` → Active connections → Disconnect.
  Or from your MCP client's connector UI.
- **PAT**: `/settings/security/tokens` → Active tokens → Revoke.
- **Either way**: revocation propagates within ≤ 30 s (the verify cache
  TTL). Disconnecting an OAuth grant cascades — all access tokens minted
  by that grant are revoked at the same time.

## Errors

The JSON-RPC `error.code` and `data.error_code` fields are stable:

| `error_code` | Meaning | Action |
|---|---|---|
| `unauthorized` | Missing / invalid / expired / revoked token | Re-authenticate (OAuth: reconnect from your client; PAT: re-issue) |
| `scope_denied` | Token is missing the required scope | OAuth: reconnect with extra scopes; PAT: issue a new token |
| `rate_limited` | Per-token RPM or RPD cap | Back off; `data.reset_at` tells you when |
| `ingestion_capped` | Per-token `write:ingestion` daily cap | Wait or use a different token |
| `credit_capped` | Per-token 24h credit cap | Wait or raise the cap on the token |
| `not_found` | Resource doesn't exist in this tenant | — (we deliberately don't leak if it exists elsewhere) |
| `conflict` | Idempotency / state collision | Read the `data` payload |
| `downstream_unavailable` | A wrapped service (n8n / Apify / analysis) failed | Retry later |
| `validation_error` | Bad arguments | Check the tool's input schema |

## Reporting issues

`support@actionsignal.ai` — include the `error_code` and a timestamp.
