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

# Hosted MCP server

> Connect an MCP-capable agent to Agents Ask with the hosted BotShield MCP server and your agent key.

If your agent speaks the [Model Context Protocol](https://modelcontextprotocol.io), it can use Agents Ask as tools instead of calling the REST API. BotShield hosts the server; you connect with your agent key. The tools map one-to-one onto the operations in [Link a human](/agents-ask/link-a-human) and [Propose an action](/agents-ask/propose-an-action).

| Setting | Value |
| - | - |
| URL | `https://mcp.botshield.ai/mcp` |
| Transport | Streamable HTTP |
| Authentication | `Authorization: Bearer bs_agent_…` (your [agent key](/agents-ask/register-an-agent#the-agent-key)) |
| Environment | Production. Use a production agent's key. |
| Server name | `botshield` |

The server is stateless and passes your key through to the BotShield API on every tool call, so a tool behaves like the matching API operation: same agent, same allowed categories, same error messages and codes. A request without a Bearer token gets HTTP 401.

If you [rotate the agent's key](/agents-ask/register-an-agent#rotate-a-key) in the Console, update the key in your MCP client's configuration at the same time. The previous key stops working immediately.

<Warning>
  **There is deliberately no "confirm" tool.** An agent can ask, wait, and withdraw. Only the human can Confirm or Deny, in the BotShield app, with their device biometric. Nothing an agent sends through this server can answer on a person's behalf.
</Warning>

## What the server covers

The server has five tools, all for Agents Ask: `bind_session`, `check_binding`, `propose_resolution`, `check_resolution_status` and `cancel_resolution`. They are listed under [Tools](#tools).

BotShield Gate and Trusted Accounts have no MCP tools. A gate runs in the person's browser through the [web component](/gate/web-component), and a person secures an account by tapping the offer on your page. Neither is something an agent can start. Use the REST API and the widget for both.

## Connect a client

Use any MCP client that supports remote servers over Streamable HTTP with a custom header.

<Tabs>
  <Tab title="Generic MCP client">
    Most clients that take a JSON server list accept this shape:

    ```json theme={null}
    {
      "mcpServers": {
        "botshield": {
          "type": "http",
          "url": "https://mcp.botshield.ai/mcp",
          "headers": {
            "Authorization": "Bearer bs_agent_MeridianConcierge__…"
          }
        }
      }
    }
    ```

    Load the key from your client's secret or environment mechanism rather than committing it to a file. Set the client's tool-call timeout above 35 seconds; see [Set the client timeout](#set-the-client-timeout).
  </Tab>

  <Tab title="Claude Desktop">
    Claude Desktop's `claude_desktop_config.json` launches local commands, so bridge to the hosted server with the `mcp-remote` package:

    ```json theme={null}
    {
      "mcpServers": {
        "botshield": {
          "command": "npx",
          "args": [
            "-y",
            "mcp-remote",
            "https://mcp.botshield.ai/mcp",
            "--header",
            "Authorization:${BOTSHIELD_AUTH}"
          ],
          "env": {
            "BOTSHIELD_AUTH": "Bearer bs_agent_MeridianConcierge__…"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="OAuth 2.0 clients">
    Some platforms only offer OAuth 2.0 for remote MCP servers. The server supports the **client credentials** grant and publishes discovery metadata at `https://mcp.botshield.ai/.well-known/oauth-authorization-server`.

    | Setting | Value |
    | - | - |
    | Token endpoint | `https://mcp.botshield.ai/token` |
    | Grant type | `client_credentials` |
    | Client ID | Your agent's name |
    | Client secret | Your agent key |
    | Scope | `queue` |

    Treat the access token with the same care as the agent key. For Salesforce, follow [BotShield MCP server for Salesforce](/integrations/salesforce/mcp-server).
  </Tab>
</Tabs>

## Tools

Every input is a flat scalar, a string or an integer, with no nested objects. That keeps the schemas easy for a model to fill and acceptable to strict tool catalogs.

| Tool | Inputs (`?` = optional) | Waits for the human |
| - | - | - |
| `bind_session` | `display_name?` | No |
| `check_binding` | `code`, `wait_seconds?` | Yes, up to `wait_seconds` |
| `propose_resolution` | `request_id`, `opaque_id`, `summary_title`, `category`, `detail_label?`, `detail_value?`, `ttl_seconds?` | No |
| `check_resolution_status` | `request_id`, `wait_seconds?` | Yes, up to `wait_seconds` |
| `cancel_resolution` | `request_id`, `reason?` | No |

### `bind_session`

Starts the one-time link with a human. Maps to `POST /agent/bind-session`.

| Input | Type | Required | Description |
| - | - | - | - |
| `display_name` | string | No | How the agent is named on the person's link screen, for example `Meridian Airlines Concierge`. |

Returns `status: "pending"`, a 6-character `code`, a `claim_url`, `expires_at` (10 minutes), and `display_name`. The agent should show the person both the code and the link.

`bind_session` always returns a code and a claim URL. It never returns an `opaque_id`, and it does not recognize a person who linked before, because BotShield cannot know who is in the conversation until they claim a code. Remembering the link is your agent's job: store the `opaque_id` that `check_binding` returns against your own user record, pass that stored value to `propose_resolution` from then on, and do not call `bind_session` again for a person you already hold an `opaque_id` for.

### `check_binding`

Checks whether the person has completed the link. Maps to `GET /agent/check-binding`.

| Input | Type | Required | Description |
| - | - | - | - |
| `code` | string | Yes | The code returned by `bind_session`. |
| `wait_seconds` | integer | No | How long the call waits for the person before it answers. 0 to 25. Default `20`. `0` answers immediately. Values outside the range are rejected. |

Returns `status`: `pending`, `bound`, `expired`, `cancelled`, `not_found`, or `revoked`. With `bound` it also returns the `opaque_id`.

The call holds until the person completes the link or the wait ends, whichever comes first, and answers as soon as the status leaves `pending`. While it returns `pending`, call it again right away; there is no need to pause between calls. Stop at any other status: `expired` and `cancelled` mean a new `bind_session`, and `not_found` means the code is wrong for this agent, which waiting does not fix.

### `propose_resolution`

Asks the person to Confirm or Deny an action. Maps to `POST /agentlink/inquire`.

| Input | Type | Required | Description |
| - | - | - | - |
| `request_id` | string | Yes | A UUID the agent generates. Idempotency key; reuse it to retry safely. |
| `opaque_id` | string | Yes | The `OP_…` id from `check_binding`. This is the only way to identify the person. |
| `summary_title` | string | Yes | Card headline, up to 120 characters. |
| `category` | string | Yes | Must be one of the agent's allowed action categories. |
| `detail_label` | string | No | Label of the detail row, for example `TOTAL`. Up to 40 characters. |
| `detail_value` | string | No | Value of the detail row, for example `$168.45`. Up to 80 characters. |
| `ttl_seconds` | integer | No | How long the person has to answer, in seconds. 60 to 86400 (24 hours). Omit it for the default of 600 (10 minutes). |

The detail row is sent only when both `detail_label` and `detail_value` are present. Returns `status: "queued"`, `card_id`, and `ttl_at`. The call returns as soon as the card is queued; the decision comes from `check_resolution_status`.

A `ttl_seconds` outside the range is not clamped. The tool returns an error with the code `ttl_below_floor` or `ttl_above_ceiling`, the same as the [REST API](/agents-ask/propose-an-action#errors). The tool does not take an Adaptive Card or a `trusted_account_id`. Call the REST API when you need those fields.

### `check_resolution_status`

Reads the current state of a proposal. Maps to `GET /agentlink/check-status`.

| Input | Type | Required | Description |
| - | - | - | - |
| `request_id` | string | Yes | The `request_id` used in `propose_resolution`. |
| `wait_seconds` | integer | No | How long the call waits for the person before it answers. 0 to 25. Default `20`. `0` answers immediately. Values outside the range are rejected. |

Returns `status` (`queued`, `approved`, `denied`, `expired`, `cancelled`), `verdict` (`approve`, `denied`, or `null`), `resolution_jwt` (the signed Proof of Resolution, or `null`), `ttl_at`, `resolved_at`, and `ceremony_id`.

The call holds while the proposal is `queued` and answers as soon as it reaches a final status, or when the wait ends. While it returns `queued`, call it again right away, until `ttl_at`. A person has to pick up their phone, so expect a few calls.

### `cancel_resolution`

Withdraws a proposal that is still waiting. Maps to `POST /agentlink/cancel`.

| Input | Type | Required | Description |
| - | - | - | - |
| `request_id` | string | Yes | The proposal to withdraw. |
| `reason` | string | No | Recorded for your audit trail. Up to 200 characters. |

Returns `status: "cancelled"` with `cancelled_at`, or the current status with `already_resolved: true` if the proposal already had a final status.

## Set the client timeout

`check_binding` and `check_resolution_status` are long-polls. With the default `wait_seconds` of 20, a call can take a little over 20 seconds to answer; with the maximum of 25, a little over 25. The server gives up on its own call to the BotShield API 10 seconds after the wait, so the longest a tool call can run is 35 seconds.

* Set your MCP client's **tool-call timeout above 35 seconds**. 40 to 45 seconds works well. A shorter timeout cuts off a healthy wait, and the agent sees a client-side timeout instead of the person's decision.
* If your client's timeout cannot be raised that far, pass a smaller `wait_seconds` so that the wait plus 10 seconds fits inside it. `wait_seconds: 0` makes the tool a single immediate check; leave a few seconds between calls in that case.
* One held call replaces a dozen short polls, which matters in agent hosts that run one turn at a time.

The other three tools answer immediately and need no special timeout.

## Tool results

A tool result is one text block containing JSON.

<CodeGroup>
  ```json Success theme={null}
  {
    "data": {
      "data": {
        "status": "bound",
        "opaque_id": "OP_QDhs65484684"
      }
    }
  }
  ```

  ```json Error (isError: true) theme={null}
  {
    "error": true,
    "status": 403,
    "message": "Action category \"payment.authorize\" is not in agent's allowed_action_categories.",
    "detail": {
      "message": "Action category \"payment.authorize\" is not in agent's allowed_action_categories.",
      "statusCode": 403
    }
  }
  ```

  ```json Error with a code (isError: true) theme={null}
  {
    "error": true,
    "status": 400,
    "message": "ttl_seconds (30) is below the floor (60). Below the floor defeats the architectural purpose — the user has not had a real chance to engage.",
    "code": "ttl_below_floor",
    "detail": {
      "message": "ttl_seconds (30) is below the floor (60). Below the floor defeats the architectural purpose — the user has not had a real chance to engage.",
      "statusCode": 400,
      "code": "ttl_below_floor",
      "min_ttl_seconds": 60
    }
  }
  ```
</CodeGroup>

On success the text is the API response with its envelope, so the payload is at `data.data`.

Every failure is an MCP **tool error**: the result has `isError: true`, and its text is a JSON object with this shape.

| Field | Type | Present | Meaning |
| - | - | - | - |
| `error` | boolean | Always | `true`. |
| `status` | number | Always | The error's status code: the API error's `statusCode`, or the HTTP status when the request itself failed. `504` means the server could not reach the BotShield API or got no answer in time. |
| `message` | string | When the API reported an error | The API's error message. |
| `code` | string | When the API error has one | The API's error code, for example `ttl_below_floor` or `ttl_above_ceiling`. |
| `detail` | object | Always | The API's error object, unchanged, including extra fields such as `min_ttl_seconds` or `violations`. |

The BotShield API reports handler errors inside an HTTP 200 response, at `data.error`. The MCP server reads that envelope for you: an unknown `opaque_id`, a category the agent is not allowed, a revoked or wrong-environment key, and an out-of-range `ttl_seconds` all arrive as tool errors, never as a successful result that contains an error. The messages and codes are the ones listed in [Propose an action](/agents-ask/propose-an-action#errors) and [Link a human](/agents-ask/link-a-human#errors).

A `504` on `propose_resolution` means the outcome is unknown. Call the tool again with the **same** `request_id`: it is the idempotency key, so a proposal that did land is returned instead of duplicated.

## The model asks; your code verifies

Tool calls are made by a language model, and a model cannot check a signature. Treat the MCP server as the way your agent *reaches* a human, not as the control that protects the action.

* Keep the execution of the action in deterministic code that you own.
* Have that code take the `resolution_jwt` and run the checks in [Proof of Resolution](/agents-ask/proof-of-resolution) before it does anything: signature, issuer, audience, expiry, `jti`, and `verdict === "approve"`.
* Never let a model's summary of a tool result ("the user approved") stand in for the proof.
* Subscribe to the `agents_ask.resolution.*` [webhooks](/webhooks/events) if you want your backend to receive the proof without depending on the agent to pass it along.

## Run a local MCP server from the SDK

The `botshield-sdk` npm package also runs as a local (stdio) MCP server. It exposes the SDK's three action methods as `propose_resolution`, `check_resolution_status`, and `cancel_resolution`. It has no link tools, so obtain the `opaque_id` through the hosted server or the REST API. Node.js 20 or later is required.

```json theme={null}
{
  "mcpServers": {
    "botshield-local": {
      "command": "npx",
      "args": [
        "-y", "--package", "botshield-sdk",
        "--",
        "mcp", "start",
        "--server-url", "https://api.botshield.ai/operations",
        "--agent-key-auth", "Bearer bs_agent_MeridianConcierge__…"
      ]
    }
  }
}
```

Always pass `--server-url`; the package does not default to the production API. The `--agent-key-auth` value is sent as the `Authorization` header as written, so include `Bearer `. List every flag with:

```bash theme={null}
npx -y --package botshield-sdk -- mcp start --help
```

## Next steps

<CardGroup cols={2}>
  <Card title="Register an agent" icon="robot" href="/agents-ask/register-an-agent">
    Create the production agent whose key you connect with.
  </Card>

  <Card title="Proof of Resolution" icon="file-signature" href="/agents-ask/proof-of-resolution">
    The verification your executing code must run.
  </Card>

  <Card title="Salesforce MCP setup" icon="cloud" href="/integrations/salesforce/mcp-server">
    Register the server for Agentforce Employee agents.
  </Card>

  <Card title="TypeScript SDK" icon="js" href="/sdk/typescript">
    Call the same operations from your own code.
  </Card>
</CardGroup>
