# Documentation

A/BLint lints A/B test specs against the rules your team writes, over two surfaces that do the same work. The MCP server puts the linter inside your assistant, and the REST API hands the same calls to a script or an agent that sends HTTP.

The MCP server exposes four tools: one that returns this documentation as markdown, one for projects, one for the rules inside them, and one that lints a spec against those rules.

## Install

The server is a remote MCP endpoint at `https://explint.com/mcp`. Claude, ChatGPT, Grok Bot, Cursor, and Gemini all connect to it.

Claude Code (shell):

```bash
claude mcp add --transport http ab-lint https://explint.com/mcp
```

Cursor (~/.cursor/mcp.json):

Cursor reads the config block from `~/.cursor/mcp.json`.

```json
{"mcpServers":{"ab-lint":{"url":"https://explint.com/mcp"}}}
```

Any other client (MCP settings):

ChatGPT takes the server URL under Settings → Connectors, Grok Bot under Settings → MCP, and Gemini under Settings → MCP servers.

```
https://explint.com/mcp
```

Ask your assistant (prompt):

The same install written as a request usually gets there fastest.

```
Add the A/BLint MCP server to my MCP configuration. It is a remote server at https://explint.com/mcp that supports OAuth, so connect that way first and let the client take me through the browser sign-in and consent screen. If OAuth does not work in my client, send an API key as a bearer token in the header "Authorization: Bearer <api key>" instead, and if I have not already given you a key, tell me to create one on the API keys tab of the A/BLint MCP page. Once it is connected, call the ablint__docs tool and read the documentation it returns before using the other A/BLint tools.
```

## Sign in

A client that handles OAuth needs nothing beyond the URL. The first call opens a browser window where you sign in and choose the organization the connection works in, and every call after that acts inside the organization you picked.

An assistant that cannot run that flow sends an API key instead, as the header `Authorization: Bearer <api key>`. You create keys on the MCP page of the A/BLint workspace, and revoking one there stops it working on the next call.

## Tools

Two of the four tools name what to do with an `operation` string, one of create, update, remove, or list, and carry that operation's arguments in the field named after it, so a call with `operation: "create"` fills `create` and leaves the rest out. `ablint__lint` takes its arguments directly, and `ablint__docs` takes no arguments and answers with the full documentation as markdown. No call names an organization, because the credential decides which one every call works in.

Projects are named by `name` on `ablint__project` and by `projectSlug` on the other two, matched inside the organization ignoring case, so any casing of the name or the slug reaches the same project.

### ablint__docs

Return the full documentation of how to use the MCP server as one markdown document, covering how to connect and sign in, every tool with its arguments and answer shapes, how listings page, and what each refusal means. The tool takes no arguments, reads and writes nothing in the organization, and never spends a lint request, so an assistant can call it at the start of a session and again whenever the shape of another tool needs checking.

```json
{}
```

### ablint__project

Create, rename, remove, and list the organization's projects.

| Argument | What it carries |
| --- | --- |
| operation | Required. One of `create`, `update`, `remove`, or `list`. |
| create | `name` for the new project. |
| update | `name` for the project to rename and `newName` for what to rename it to, which regenerates its slug with it. |
| remove | `name` for the project to remove. |
| list | `slug` to keep only projects whose slug holds that text, `limit` for how many rows one answer carries, and `cursor` to continue a listing. |

```json
{
  "operation": "create",
  "create": { "name": "Checkout cross-sell" }
}
```

### ablint__rule

Create, change, remove, and list the rules inside a project. A rule carries a key such as `hypothesis/names-metric`, a title, an instruction written in plain English that the linter judges the spec against, a severity of `error` or `warning`, and a switch for whether it is on. The category before the slash is how the workspace groups a project's rules, and a rule is found by its key matched ignoring case.

| Argument | What it carries |
| --- | --- |
| operation | Required. One of `create`, `update`, `remove`, or `list`. |
| create | `projectSlug`, `ruleKey`, `ruleTitle`, and `ruleInstruction`, plus optional `severity` (`error` or `warning`, error by default) and `enabled` (on by default). |
| update | `projectSlug` and `ruleKey` to name the rule, plus any of `ruleTitle`, `newRuleKey`, `ruleInstruction`, `severity`, and `enabled` to change. |
| remove | `projectSlug` and `ruleKey` to name the rule to remove. |
| list | `projectSlug`, plus optional `ruleKey` to keep only rules whose key holds that text, `limit`, and `cursor`. |

```json
{
  "operation": "create",
  "create": {
    "projectSlug": "checkout-cross-sell",
    "ruleKey": "hypothesis/names-metric",
    "ruleTitle": "Names the metric",
    "ruleInstruction":
      "The hypothesis names the metric it expects to move."
  }
}
```

### ablint__lint

Lint a spec against every rule the project has switched on.

| Argument | What it carries |
| --- | --- |
| projectSlug | Required. The project to lint against, by name or slug matched ignoring case. |
| content | The spec itself as markdown, HTML, or plain text, judged against the project's rules. An image embedded in it as base64 is dropped before it is judged, and content beyond the acceptable size comes back as an error. |

## A lint, end to end

One lint through every surface: the `ablint__lint` tool call beside the same request over REST, written for curl, JavaScript, and Python.

MCP tool (ablint__lint):

```json
{
  "projectSlug": "checkout-cross-sell",
  "content":
    "Hypothesis: a full-width add-to-cart button on phones increases checkout completion."
}
```

cURL (shell):

```bash
curl -X POST https://explint.com/api/projects/checkout-cross-sell/lint \
  -H "Authorization: Bearer <api key>" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hypothesis: a full-width add-to-cart button on phones increases checkout completion."}'
```

JavaScript (fetch):

```javascript
const content = "Hypothesis: a full-width add-to-cart button on phones increases checkout completion.";

const reply = await fetch("https://explint.com/api/projects/checkout-cross-sell/lint", {
  method: "POST",
  headers: {
    Authorization: "Bearer <api key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ content }),
});

const lint = await reply.json();
```

Python (requests):

```python
content = "Hypothesis: a full-width add-to-cart button on phones increases checkout completion."

reply = requests.post(
    "https://explint.com/api/projects/checkout-cross-sell/lint",
    headers={"Authorization": "Bearer <api key>"},
    json={"content": content},
)
lint = reply.json()
```

The answer:

The same shape comes back over both surfaces.

```json
{
  "quality_score": 0.5,
  "errors": [
    {
      "title": "If, then, and because are all present",
      "key": "hypothesis/structure",
      "description": "The hypothesis states what changes, what it expects, and why.",
      "probability": 0.31,
      "bad_example": "We believe the new onboarding will improve the experience.",
      "good_example": "Showing the setup checklist during signup will increase activation because new users can see what a finished workspace looks like."
    }
  ],
  "warnings": [
    {
      "title": "Success metric says how it is measured",
      "key": "metric/measurement-type",
      "description": "The success metric names how it is measured, as a rate, a count, or a median.",
      "probability": 0.62,
      "bad_example": "We will measure success by engagement.",
      "good_example": "Success is the median time from signup to first project created, measured across new accounts."
    }
  ],
  "passed": [
    {
      "title": "Names the metric",
      "key": "hypothesis/names-metric",
      "description": "The hypothesis names the metric it expects to move.",
      "probability": 0.93,
      "bad_example": "The new pricing page will perform better.",
      "good_example": "The new pricing page will increase checkout completion among new visitors."
    },
    {
      "title": "One change per test",
      "key": "hypothesis/one-change",
      "description": "The spec names one change so the result can be read against it.",
      "probability": 0.86,
      "bad_example": "The variant changes the headline, the price, and the form together.",
      "good_example": "The variant changes only the header call to action, with the rest of the page unchanged."
    }
  ]
}
```

Every rule lands in exactly one of `errors`, `warnings`, or `passed`, by the severity its rule carries, and each list comes back sorted by key. An entry carries the rule's `title`, `key`, `description`, and `probability`, the chance the model gave the content of satisfying the rule, together with the rule's `bad_example` and `good_example` where the rule carries them, each a short excerpt showing what a break looks like and what passing looks like, so an assistant can act on a break without a second call. A rule passes at a probability of 0.8 or more. `quality_score` is the share of the rules that passed, rounded to three decimals.

## REST API

Every tool above is also a REST call, so a script or an agent that sends HTTP can do the same work. The API lives at `https://explint.com/api` and takes an API key as the header `Authorization: Bearer <api key>`, created on the MCP page of the A/BLint workspace. The key decides which organization every call works in, so no call names one.

Nine endpoints cover the work the three tools do:

| Method and path | What it does |
| --- | --- |
| `GET /api/projects` | Lists the organization's projects in title order. |
| `POST /api/projects` | Creates a project named by `name` in the body. |
| `PATCH /api/projects/{name}` | Renames a project to `newName`, which regenerates its slug with the new name. |
| `DELETE /api/projects/{name}` | Removes a project and every rule it carries. |
| `GET /api/projects/{projectSlug}/rules` | Lists one project's rules in creation order. |
| `POST /api/projects/{projectSlug}/rules` | Creates a rule from `ruleKey`, `ruleTitle`, and `ruleInstruction`, with optional `severity` and `enabled`. |
| `PATCH /api/projects/{projectSlug}/rules/{ruleKey}` | Changes whichever of `ruleTitle`, `newRuleKey`, `ruleInstruction`, `severity`, and `enabled` the body carries. |
| `DELETE /api/projects/{projectSlug}/rules/{ruleKey}` | Removes a rule from its project. |
| `POST /api/projects/{projectSlug}/lint` | Checks the body's `content` against every rule the project has switched on. |

A project is named in the path by name or slug, matched inside the organization ignoring case the way the tools match it. A rule is named by its key, and a key may hold slashes, so percent-encode it on the way in: `hypothesis%2Fnames-metric` and `hypothesis/names-metric` both reach the same rule.

Listings page through their rows the way the tools do: `limit` carries from 1 to 100 rows and 25 when it is left out, `slug` and `ruleKey` narrow a listing to the rows holding that text, and the `nextCursor` an answer returns is what the next call passes as `cursor` until it comes back `null`.

A lint takes the spec as `content` and answers the same shape `ablint__lint` returns. An image embedded in the content as base64 is dropped before it is judged, content beyond the acceptable size comes back as an error, and a lint no model answered comes back as the linter being unable to answer.

A refusal answers `{"error": "<message>"}` under a status that says what kind it is:

| Status | What it answers |
| --- | --- |
| 400 | A request to fix. |
| 401 | A missing or invalid API key. |
| 402 | A balance spent on linting. |
| 403 | The Default project refusing removal. |
| 404 | No such endpoint, project, or rule. |
| 405 | A path called with the wrong method. |
| 409 | A project name or rule key already taken. |
| 502 | A lint the linter could not answer. |

The whole surface is published as an OpenAPI document at `https://explint.com/openapi.json`, which is how an agent learns every path, argument, and answer in one fetch.
