MCP Server

AvsB hosts a Model Context Protocol (MCP) server, so an AI agent such as Claude Code can read your experiments, flags, metrics and results, and make changes, without you writing an integration.

The server is generated from the public REST API. Every endpoint in the API becomes one tool, so anything you can do with /api/v1 is available to an agent whose token is permitted to do it. Nothing is hand-listed, which means the tools an agent sees and the API's real behaviour cannot fall out of step.

Connect

The server lives at one URL and authenticates with the same Bearer token the REST API uses:

HTTP
POST https://app.avsb.cloud/api/mcpAuthorization: Bearer avsb_pat_...
HTTP2 lines

Create a personal access token under Account Settings, Personal Access Tokens. See personal access tokens for the walkthrough. Organization service tokens (avsb_svc_...) work too, and are the better choice for a shared or unattended agent.

Claude Code

Shell
claude mcp add --transport http avsb https://app.avsb.cloud/api/mcp \  --header "Authorization: Bearer avsb_pat_..."
Shell2 lines

Any MCP client

Most clients accept a JSON configuration block. The server speaks MCP's streamable HTTP transport, so configure it as an HTTP server rather than a stdio one:

JSON
{  "mcpServers": {    "avsb": {      "type": "http",      "url": "https://app.avsb.cloud/api/mcp",      "headers": {        "Authorization": "Bearer avsb_pat_..."      }    }  }}
JSON11 lines

If you belong to more than one organization, add the header that names which one to act on. Without it, a personal token that maps to several organizations is refused rather than a guess being made:

JSON
{  "headers": {    "Authorization": "Bearer avsb_pat_...",    "AvsB-Org-Id": "204915"  }}
JSON6 lines

The value is the organization id, or the short numeric id from your dashboard URL.

What the agent sees

Ask the server for its tools and you get one entry per endpoint your token can reach. Names follow the same vocabulary as the generated SDKs and the CLI, so list_projects, get_experiment_results and create_metric mean what they look like they mean.

Each tool carries:

  • the HTTP method and path it calls, so the agent can explain what it is about to do;
  • the scope required to call it;
  • a JSON Schema for its arguments, converted from the same validation schema the API enforces, so an agent that fills the schema correctly sends a request the API accepts;
  • a hint marking reads as read-only and deletes as destructive, which well-behaved clients surface before running anything.

Path ids accept either the long id or the short numeric id shown in dashboard URLs. List tools accept limit and cursor for paging. Request payloads go in a single body argument, which keeps them from colliding with ids in the path.

A worked example

Here is one full round trip: calling list_projects (GET /api/v1/projects, scope projects:read) for the first two projects.

tools/call request
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_projects",    "arguments": { "limit": 2 }  }}
JSON9 lines

The response wraps the result in content[0].text. That text is the exact response body a plain HTTP call to /api/v1/projects would return, printed as a string:

tools/call response
{  "jsonrpc": "2.0",  "id": 1,  "result": {    "content": [      { "type": "text", "text": "{ \"data\": [...], \"page\": { \"nextCursor\": \"...\", \"hasMore\": true } }" }    ],    "isError": false  }}
JSON10 lines

Decoded, that text field reads like this. Every field here is a real field on a project: id, shortId, name, type, and status.

Decoded text field
{  "data": [    { "id": "clx1a2b3c4d5e6f", "shortId": 123, "name": "Checkout Flow", "type": "WEB_EXPERIMENTATION", "status": "ACTIVE" },    { "id": "clx9y8x7w6v5u4t", "shortId": 124, "name": "Pricing Page", "type": "FEATURE_FLAG", "status": "ACTIVE" }  ],  "page": { "nextCursor": "eyJpZCI6MTI0fQ", "hasMore": true }}
JSON7 lines

hasMore: true means there is another page. Call again with "arguments": { "limit": 2, "cursor": "eyJpZCI6MTI0fQ" } to get it.

Your token decides the tool list

The tool list is filtered to the token that connected. A token holding only projects:read sees the project read tools and nothing else. The write tools are not hidden behind a refusal, they are absent, so an agent never plans work it cannot finish.

Personal access tokens derive their permissions from your role in the organization, so a token belonging to someone with a read-only role produces a read-only tool list. Service tokens carry exactly the scopes chosen when they were created.

This has a useful consequence: the safest way to give an agent access is to give it a narrow token. A token scoped to experiments:read and results:read produces an agent that can analyse your tests and cannot touch them.

Two checks apply, not one. The tool list is filtered, and every call is checked again on the way through. A call to a tool the token does not cover is refused before any request is made, and the underlying endpoint would refuse it independently anyway.

Read first

Reads are safe to let an agent run freely. Writes are not, and they are not simulated: a write tool performs the same change the dashboard performs. It takes effect immediately and is recorded in your organization's audit log, attributed to the token (and, for a personal token, to you).

The server states this to connecting clients at handshake time, and marks every write tool accordingly. Clients that ask for confirmation before running a tool will ask before running these. Treat approval of a write tool the same way you would treat clicking the button yourself.

Scoping, isolation and limits

Everything the REST API guarantees applies here, because the tools call the real endpoints rather than reaching into the database:

  • Organization isolation. The organization comes from your token, never from a tool argument. There is no argument an agent can set that reads another organization's data. A resource belonging to another organization returns not_found, which is also what a resource that does not exist returns, so nothing is confirmed by its absence.
  • Rate limits. The MCP endpoint has its own per-token budget, and each underlying API call is metered as usual. A busy agent gets rate_limited and should back off, exactly like any other client.
  • Plan entitlements. Endpoints gated by your plan stay gated.

Errors

The server speaks JSON-RPC, so there are two kinds of failure and they mean different things.

A problem with the request itself comes back as a protocol error: an unknown tool, a missing argument, a token without the scope for the tool, or a credential that is missing, expired or revoked. Scope and authentication failures carry the same code and docUrl fields the REST API uses, so unauthorized and scope_missing point at the same documentation either way.

A problem reported by the API comes back as a normal tool result flagged as an error, carrying the response body unchanged. The agent reads it and can react. A validation_failed response, for example, names the field that was wrong, so the agent can correct the payload and retry.

See API conventions for the full vocabulary of codes.

Limits worth knowing

Stated plainly, so nothing here surprises you later:

  • Tools only. The server exposes tools. It does not serve MCP resources or prompts, and it does not use sampling. Everything is reachable by calling a tool.
  • No streaming. Responses are returned whole. The server never opens a server-initiated event stream, so a client that tries to open one is told the endpoint does not offer it. Long-running work returns when it is done rather than reporting progress along the way.
  • Stateless. There is no session. Each request stands alone and is authenticated on its own, which means a token change takes effect on the next call with no reconnect.
  • One tool per endpoint. Tools are not bundled into higher-level workflows. Creating and launching an experiment is several calls, in the order the API documents.
  • Server to server. Like the rest of the API, this endpoint expects a token and does not accept a browser session.
Was this helpful?