# Havadis MCP

> Streamable HTTP MCP server. Authenticate with a Bearer token minted from the Havadis dashboard, then call any tool below.

- Endpoint: `https://api.gethavadis.co/api/mcp`
- Protocol: `mcp/streamable-http` · MCP version `2024-11-05`
- Server version: `0.1.0`
- Docs (HTML): https://gethavadis.co/developers
- Manifest (JSON): https://api.gethavadis.co/api/mcp/manifest

## Authentication

- Scheme: `Bearer` (HTTP `Authorization` header)
- Token format: `havadis_<prefix>_<secret>`
- Mint endpoint (JWT-protected): `https://api.gethavadis.co/api/tenants/current/api-keys`

### Scopes

- `brands:read`
- `brands:write`
- `content:read`
- `topics:write`
- `jobs:read`
- `jobs:write`
- `credits:read`
- `billing:read`

## Rate limits

Per tenant (1 active key = 1 tenant). Window in seconds.

- `free`: 60 requests / 60s
- `pro`: 300 requests / 60s
- `pro_plus`: 1000 requests / 60s

## Tools

### `list_brands`

List the brands owned by the authenticated tenant. Use this to discover brand IDs before calling brand-scoped tools (e.g. list_content, create_job).

Scopes: `brands:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

### `get_brand`

Fetch a single brand the authenticated tenant owns, including its description, logo and primary color. Returns 404-equivalent if the brand does not belong to the tenant.

Scopes: `brands:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    }
  },
  "required": [
    "brandId"
  ],
  "additionalProperties": false
}
```

### `list_content`

List generated content for one of the tenant's brands, newest first. Pagination via `page`/`limit` (defaults: page=1, limit=20). Result items contain id, title, contentType and createdAt — call `get_content` for full body.

Scopes: `content:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "page": {
      "default": 1,
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "limit": {
      "default": 20,
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    }
  },
  "required": [
    "page",
    "limit",
    "brandId"
  ],
  "additionalProperties": false
}
```

### `get_content`

Fetch a single generated content item including its full body, summary and metadata blocks (SEO, AEO, GEO, social variants). The `brandId` must own the content — cross-brand fetches return null.

Scopes: `content:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    },
    "contentId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    }
  },
  "required": [
    "brandId",
    "contentId"
  ],
  "additionalProperties": false
}
```

### `get_credit_balance`

Return the authenticated user's credit balance. Credits are pooled at the user level and never expire — they roll over indefinitely. `isUnlimited: true` users have no balance cap (`balance` is reported as 0 for them; check `isUnlimited` first).

Scopes: `credits:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

### `get_credit_history`

Paginated credit ledger for the authenticated user. Each entry records a deduction, top-up, subscription grant or welcome bonus. Optional `brandId` narrows to one brand's spend.

Scopes: `credits:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "page": {
      "default": 1,
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "limit": {
      "default": 20,
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    }
  },
  "required": [
    "page",
    "limit"
  ],
  "additionalProperties": false
}
```

### `get_subscription`

Return the tenant's current plan and Stripe subscription state: `plan` (free / pro / pro_plus), `subscriptionStatus`, `currentPeriodEnd`, `cancelAtPeriodEnd`, `canceledAt`. Free-tier tenants have null subscription fields. Use `list_plans` to discover upgrade targets.

Scopes: `billing:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

### `list_plans`

List subscription plans available to the tenant — Pro and Pro+. Each plan exposes monthly price (cents), credits granted per month, and an `isCurrent` flag against the tenant's current plan. Stripe checkout / cancel actions are intentionally not exposed via MCP; manage subscriptions in the web dashboard.

Scopes: `billing:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
```

### `list_jobs`

List content-generation jobs for a brand, newest first. Each item carries the job status (queued / running / completed / failed) and timing/credit summary. Use `get_job` for the full pipeline detail and recent agent logs.

Scopes: `jobs:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    },
    "page": {
      "default": 1,
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "limit": {
      "default": 20,
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    }
  },
  "required": [
    "brandId",
    "page",
    "limit"
  ],
  "additionalProperties": false
}
```

### `get_job`

Fetch a job along with its pipeline progress and recent agent logs. Poll this every 2-5 seconds while `status` is queued or running. `pipeline` lists each agent step with timing; `logs` is the latest activity feed (most recent last).

Scopes: `jobs:read`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    },
    "jobId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    },
    "logsLimit": {
      "default": 50,
      "type": "integer",
      "minimum": 0,
      "maximum": 200
    }
  },
  "required": [
    "brandId",
    "jobId",
    "logsLimit"
  ],
  "additionalProperties": false
}
```

### `create_brand`

Create a new brand for the authenticated tenant. Free-tier tenants are limited to 1 brand and will receive a `feature_locked` error on the second create attempt — upgrade to Pro for unlimited brands. The brand becomes the default automatically when it is the first one.

Scopes: `brands:write`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "slug": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
    },
    "logoUrl": {
      "default": null,
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "primaryColor": {
      "default": "#000000",
      "type": "string",
      "pattern": "^#[0-9A-Fa-f]{6}$"
    },
    "industry": {
      "default": "",
      "type": "string",
      "maxLength": 100
    },
    "website": {
      "default": "",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "string",
          "const": ""
        }
      ]
    },
    "description": {
      "default": "",
      "type": "string",
      "maxLength": 500
    }
  },
  "required": [
    "name",
    "logoUrl",
    "primaryColor",
    "industry",
    "website",
    "description"
  ],
  "additionalProperties": false
}
```

### `suggest_topics`

Enqueue an LLM-driven topic discovery run for the brand. Costs 3 credits, charged when the run is enqueued. Returns immediately with `{runId, status, contentType}` — the run executes asynchronously on a worker and the resulting suggestions appear in the dashboard. Requires the brand to have a completed knowledge analysis.

Scopes: `topics:write`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    },
    "contentType": {
      "default": "blog",
      "type": "string",
      "enum": [
        "blog",
        "instagram",
        "x_thread"
      ]
    },
    "language": {
      "default": "en",
      "type": "string",
      "minLength": 2,
      "maxLength": 5
    }
  },
  "required": [
    "brandId",
    "contentType",
    "language"
  ],
  "additionalProperties": false
}
```

### `create_job`

Enqueue a content-generation pipeline job. Costs depend on the tenant plan (free skips SEO/AEO/GEO). Free-tier tenants are limited to the 'short' word-count range; attempting 'medium', 'long' or 'unlimited' returns a `feature_locked` error. The pipeline runs asynchronously — poll `get_job` every 2-5s while `status` is queued or running.

Scopes: `jobs:write`

Input schema (JSON Schema):

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "contentType": {
      "type": "string",
      "enum": [
        "blog",
        "instagram",
        "x_thread"
      ]
    },
    "brief": {
      "type": "string",
      "minLength": 10,
      "maxLength": 4000
    },
    "styleUrls": {
      "default": [],
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "format": "uri"
      }
    },
    "researchUrls": {
      "default": [],
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "format": "uri"
      }
    },
    "keywords": {
      "minItems": 1,
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "targetPersonaId": {
      "default": null,
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "instructions": {
      "default": "",
      "type": "string",
      "maxLength": 2000
    },
    "language": {
      "default": "en",
      "type": "string",
      "minLength": 2,
      "maxLength": 5
    },
    "wordCountRange": {
      "default": null,
      "anyOf": [
        {
          "type": "string",
          "enum": [
            "short",
            "medium",
            "long",
            "unlimited"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "suggestionId": {
      "default": null,
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "brandId": {
      "type": "string",
      "pattern": "^[a-f\\d]{24}$"
    }
  },
  "required": [
    "contentType",
    "brief",
    "styleUrls",
    "researchUrls",
    "keywords",
    "targetPersonaId",
    "instructions",
    "language",
    "wordCountRange",
    "suggestionId",
    "brandId"
  ],
  "additionalProperties": false
}
```
