> ## Documentation Index
> Fetch the complete documentation index at: https://docs.haloagents.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Protocol Reference

> JSON-RPC methods, OAuth endpoints, discovery URLs, and rate limits. For developers building custom MCP clients against Halo.

## Transport

| Aspect             | Value                                                                                |
| ------------------ | ------------------------------------------------------------------------------------ |
| **Protocol**       | [Model Context Protocol](https://modelcontextprotocol.io/specification) `2025-03-26` |
| **Transport**      | HTTP + JSON-RPC 2.0 (single endpoint, stateless per request)                         |
| **Endpoint**       | `POST https://app.haloagents.ai/api/mcp`                                             |
| **Auth**           | OAuth 2.1 with PKCE, `Authorization: Bearer halo_at_*`                               |
| **Server name**    | `halo-agents`                                                                        |
| **Server version** | `1.0.0`                                                                              |

## Discovery URLs

A bare `GET /api/mcp` returns a small descriptor with all the URLs an MCP-aware client needs:

```json theme={null}
{
  "name": "halo-agents",
  "version": "1.0.0",
  "protocolVersion": "2025-03-26",
  "transport": "http+jsonrpc",
  "methods": ["initialize", "ping", "tools/list", "tools/call"],
  "authentication": {
    "type": "oauth2",
    "authorization_server": "https://app.haloagents.ai/.well-known/oauth-authorization-server",
    "protected_resource_metadata": "https://app.haloagents.ai/.well-known/oauth-protected-resource"
  },
  "mcp_url": "https://app.haloagents.ai/api/mcp"
}
```

You can also walk the standard discovery flow:

| URL                                       | RFC      | What it returns                                                                                                                                                |
| ----------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/.well-known/oauth-protected-resource`   | RFC 9728 | The resource id and the authorization-server **issuer** (`https://app.haloagents.ai`) to talk to.                                                              |
| `/.well-known/oauth-authorization-server` | RFC 8414 | The full OAuth metadata: `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `revocation_endpoint`, supported scopes, supported PKCE methods. |

A 401 from `/api/mcp` carries a `WWW-Authenticate: Bearer realm="halo-mcp", resource_metadata="...", as_uri="..."` header pointing at the same documents, so a client doesn't need to know the URLs ahead of time.

## OAuth 2.1 endpoints

| Endpoint                   | Method | RFC           | Notes                                                                                   |
| -------------------------- | ------ | ------------- | --------------------------------------------------------------------------------------- |
| `/api/mcp/oauth/register`  | `POST` | RFC 7591      | Dynamic Client Registration. Public; rate-limited at 10 registrations per IP per hour.  |
| `/api/mcp/oauth/authorize` | `GET`  | RFC 6749 §4.1 | Authorization-code + PKCE entry point. Bounces unauthenticated users through `/signin`. |
| `/api/mcp/oauth/consent`   | `POST` | (Halo)        | Internal. The consent UI's form posts here to issue the authorization code.             |
| `/api/mcp/oauth/token`     | `POST` | RFC 6749 §3.2 | Token + refresh grants. `grant_type` must be `authorization_code` or `refresh_token`.   |
| `/api/mcp/oauth/revoke`    | `POST` | RFC 7009      | Idempotent. Always returns 200.                                                         |

Supported grants: `authorization_code`, `refresh_token`. Supported PKCE methods: `S256` only. Supported response types: `code`. Supported token endpoint auth methods: `none` (public clients), `client_secret_basic`, `client_secret_post`.

## Token formats

| Token              | Prefix     | Lifetime                         |
| ------------------ | ---------- | -------------------------------- |
| Access token       | `halo_at_` | 1 hour                           |
| Refresh token      | `halo_rt_` | Long-lived, rotates on every use |
| Authorization code | `halo_ac_` | 5 minutes, single-use            |

All three are 256-bit random values, base64url-encoded after the prefix. Halo persists only the SHA-256 hash; on every request the bearer is hashed and looked up. There is no JWT, no signature verification, no `jwks_uri`.

## JSON-RPC methods

### `initialize`

Capabilities handshake. Required as the first call after a fresh OAuth.

**Request:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize"
}
```

**Response:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "serverInfo": { "name": "halo-agents", "version": "1.0.0" },
    "capabilities": {
      "tools": { "listChanged": false }
    }
  }
}
```

### `ping`

Liveness check. Returns an empty object on success.

```json theme={null}
{ "jsonrpc": "2.0", "id": 2, "method": "ping" }
```

### `tools/list`

Returns the tool descriptors the token's scopes (and the org's connections + agent toggles) unlock.

**Request:**

```json theme={null}
{ "jsonrpc": "2.0", "id": 3, "method": "tools/list" }
```

**Response:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      {
        "name": "search_docs",
        "description": "Search the public Halo Agents documentation...",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": { "type": "string" },
            "page_size": { "type": "number" }
          },
          "required": ["query"],
          "additionalProperties": false
        }
      },
      {
        "name": "linear.create_bug",
        "description": "File a bug ticket in Linear...",
        "inputSchema": { "type": "object", "properties": { "...": "..." } },
        "annotations": { "destructiveHint": true }
      }
    ]
  }
}
```

Tool names are namespaced as `<integration>.<action>` for native integrations, `mcp.<slug>.<tool>` for connected external MCP servers, and prefixed bare names for the built-in tool families: `search_docs` / `read_doc` (docs proxy), `setup_*` (setup helpers), `data_*` (workspace-data reads plus the `data_update_user` / `data_update_company` writes), `inbox_*` (ticket replies, notes, triage, outreach), and `automation_*` (campaign create / edit / preview / activate). The `annotations.destructiveHint: true` flag is set on tools that mutate external state.

### `tools/call`

Invoke a tool. Wrap arguments under `params.arguments`; pass session metadata under `params._meta`.

**Request:**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "hubspot.create_deal",
    "arguments": { "name": "ACME renewal", "amount": 50000 },
    "_meta": {
      "bound_end_user_id": "u_abc123"
    }
  }
}
```

**Response (success):**

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"deal_id\": \"deal_xyz\", ...}"
      }
    ],
    "isError": false
  }
}
```

The result is always wrapped in a single `text` content block. Structured tool output is JSON-stringified (with 2-space indent) inside that block.

## Error codes

Standard JSON-RPC plus a couple of Halo-specific codes:

| Code     | Meaning                                                                                                              |
| -------- | -------------------------------------------------------------------------------------------------------------------- |
| `-32700` | Parse error (invalid JSON body).                                                                                     |
| `-32600` | Invalid Request (bad JSON-RPC envelope).                                                                             |
| `-32601` | Method not found, or unknown tool name on `tools/call`.                                                              |
| `-32602` | Invalid params (validation failure on tool args).                                                                    |
| `-32603` | Internal error.                                                                                                      |
| `-32001` | Unauthorized (no token, bad token, expired token). Returned with HTTP 401 + `WWW-Authenticate`.                      |
| `-32002` | Forbidden (scope denied, destructive blocked, integration disabled, agent disabled, rate limited, feature flag off). |

Tool-call failures preserve the original error code where possible: `unauthorized` → `-32002`, `validation` → `-32602`, `not_found` → `-32601`, anything else → `-32603`.

## Rate limits

| Method       | Per-token cap  |
| ------------ | -------------- |
| `tools/list` | 60 per minute  |
| `tools/call` | 120 per minute |
| Other        | 60 per minute  |

Caps are per-token, on top of the IP-based dashboard limit. Hitting one returns HTTP 429 with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Per-action and per-org caps still apply on top.

DCR is rate-limited at 10 registrations per IP per hour.

## Session metadata (`_meta`)

The MCP `_meta` envelope on `tools/call` lets clients pass per-call context that isn't part of the tool's input schema:

| Key                 | Value            | Effect                                                                                |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------- |
| `bound_end_user_id` | Halo customer id | The tool runs on behalf of that customer (resolved per call against your org).        |
| `bound_email`       | Email string     | Same as above but resolved by email lookup. Use when the client only knows the email. |

The binding is re-validated server-side on every call. A stale id, a forged id, or an id from a foreign tenant is rejected.

## Helpful headers

Halo sets these on relevant responses:

| Header                                                                             | When                                             |
| ---------------------------------------------------------------------------------- | ------------------------------------------------ |
| `WWW-Authenticate: Bearer realm="halo-mcp", resource_metadata="...", as_uri="..."` | On 401 responses from `/api/mcp`.                |
| `Cache-Control: public, max-age=300`                                               | On the `.well-known/*` discovery documents.      |
| `Cache-Control: no-store`                                                          | On `/oauth/token` responses (per RFC 6749 §5.1). |
| `Retry-After`, `X-RateLimit-*`                                                     | On 429 responses.                                |

## A bare-bones client

A minimal `curl` walkthrough once you have an access token:

```bash theme={null}
TOKEN="halo_at_..."
URL="https://app.haloagents.ai/api/mcp"

# Initialize
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

# List tools
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# Call a tool
curl -sX POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":3,"method":"tools/call",
    "params": {
      "name":"search_docs",
      "arguments": {"query":"escalation policy","page_size":5}
    }
  }'
```
