# MCP Server

> Connect an AI assistant to your event data through the Model Context Protocol.

Source: https://docs.centinelanalytica.com/api/mcp

## Overview

The MCP server gives an AI assistant the same read-only access as the [Analytics Query API](https://docs.centinelanalytica.com/api/analytics.md). The assistant writes the SQL. The server runs it under the ClickHouse account of your organisation and returns the rows.

The endpoint is `POST https://api.centinelanalytica.com/mcp`. It speaks the Streamable HTTP transport of protocol revision `2025-06-18`.

The server keeps no state between requests. There is no event stream and no session identifier. Each request carries its own key, and each request receives one answer.

> **Note:** The assistant reads only the events of your own organisation. The server applies this filter to every statement. A statement cannot widen past your own rows.

## Connect a client

Each request must have your API key in an `Authorization` header. This rule includes the `initialize` request. The server has no unauthenticated method and no OAuth flow.

Use the same key as the Analytics Query API. Create a key in the [dashboard](https://docs.centinelanalytica.com/install/dashboard.md).

Add the server to Claude Code with one command:

```bash
claude mcp add --transport http centinel https://api.centinelanalytica.com/mcp \
  --header "Authorization: Bearer sk_api_ro_your_key_here"
```

For a client that reads a JSON configuration file, add this block:

```json
{
  "mcpServers": {
    "centinel": {
      "type": "http",
      "url": "https://api.centinelanalytica.com/mcp",
      "headers": {
        "Authorization": "Bearer sk_api_ro_your_key_here"
      }
    }
  }
}
```

> **Warning:** The key gives read access to all of your event data. Keep the configuration file on a machine that you control. Do not commit the key to a repository.

To test the connection without a client, send the `initialize` request yourself:

```bash
curl -X POST https://api.centinelanalytica.com/mcp \
  -H "Authorization: Bearer sk_api_ro_your_key_here" \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
```

The server returns the protocol revision and its one capability:

```json
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"centinel-analytics","version":"1"}}}
```

## Tools

The server has three tools. The list is the same for each caller: the tenant boundary is the database account, not the tool list.

| Tool           | Arguments | Returns                                                                 |
| -------------- | --------- | ----------------------------------------------------------------------- |
| `query`        | `sql`     | The rows of one read-only `SELECT`.                                     |
| `query_help`   | none      | The rules of the account, and example queries.                          |
| `list_columns` | none      | Each readable column of `events_distributed`, with its ClickHouse type. |

Call `query` with one statement:

```bash
curl -X POST https://api.centinelanalytica.com/mcp \
  -H "Authorization: Bearer sk_api_ro_your_key_here" \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query","arguments":{"sql":"SELECT action, uniqExact(request_id) AS requests FROM events_distributed WHERE created_at >= now() - INTERVAL 1 DAY GROUP BY action FORMAT JSONEachRow"}}}'
```

The `query_help` tool returns the account rules and worked examples. Call it once before your first statement, because it reads no data and costs no query time.

The `list_columns` tool reads the column grant live. `DESCRIBE` and `SHOW CREATE` are denied to the account, so this tool is the only way to read the current column list. Where a document and this tool disagree, this tool is correct.

### Query rules

The tool descriptions carry the rules that the assistant must know. Two of them change your numbers rather than fail:

* Select `FROM events_distributed`. A per-shard table such as `events` holds the rows of one shard. The query succeeds, the count comes back low, and nothing reports it.
* `decision` is what the engine concluded. `action` is what your policy enforced. Most blocks run in monitor first, so `decision = 'block'` overstates the traffic that you stopped.

Refer to [Analytics Query API](https://docs.centinelanalytica.com/api/analytics.md) for the event schema and the full rule list.

## Errors

The server separates a failed statement from a failed request. The two have different shapes, because an assistant can correct only the first.

A tool error means that the assistant can correct the call itself. The status is `200`. The result has `isError` set to `true`, and the text says what to change. A statement that the database refused carries the message of the database:

```json
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Code: 47. DB::Exception: Unknown expression identifier `hostnme` ..."}],"isError":true}}
```

The assistant reads that message and writes the statement again. A `query` call without a `sql` argument is a tool error too. That call never reaches the database, but the assistant supplies the argument and calls again.

A protocol error gives the assistant nothing to correct, so the server uses one only when the request never became a tool call:

| Code     | Meaning                                                                                |
| -------- | -------------------------------------------------------------------------------------- |
| `-32700` | The request body is not valid JSON.                                                    |
| `-32600` | The body is valid JSON, but it is not a JSON-RPC 2.0 request.                          |
| `-32601` | The method is unknown. The server serves `initialize`, `tools/list`, and `tools/call`. |
| `-32602` | The tool name is unknown. The server has `query`, `query_help`, and `list_columns`.    |

Three failures happen before the JSON-RPC layer:

* The server returns `401` and an empty body if the key is absent, unknown, revoked, or expired.
* The server returns `405` for each method other than `POST`.
* The server returns `202` and an empty body for a notification. A notification is a request without an `id`, and the protocol gives it no answer.

## Limits

| Limit                          | Value        |
| ------------------------------ | ------------ |
| Statement in one `query` call  | 65,456 bytes |
| Request body                   | 64 KB        |
| Answer of one tool call        | 256 KiB      |
| Query execution time           | 30 seconds   |
| Rows returned                  | 1,000,000    |
| Row retention                  | 90 days      |
| `headers` and `body` retention | 14 days      |

An answer above 256 KiB is discarded, not shortened. The server returns a tool error that asks for a `LIMIT`, because a truncated answer would look complete to the assistant.

This route collects the whole answer before it sends it, because a JSON-RPC result carries the answer as one string. The [Analytics Query API](https://docs.centinelanalytica.com/api/analytics.md) streams the answer instead, so the 256 KiB cap does not apply there. Use that endpoint for a large export.

The database gateway collects each answer before it relays it, and it refuses an answer above 4 MiB. That ceiling applies to both endpoints. On this route the 256 KiB cap is reached first.

## See also

* [Analytics Query API](https://docs.centinelanalytica.com/api/analytics.md) for the event schema and the raw SQL endpoint
* [Docs MCP Server](https://docs.centinelanalytica.com/api/docs-mcp.md) to let an assistant search this documentation
* [Crawlers](https://docs.centinelanalytica.com/api/crawlers.md) for the crawler categories
* [Dashboard](https://docs.centinelanalytica.com/install/dashboard.md) to create and revoke API keys
