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

# MCP server

> Drive Cue from any AI coding agent over the Model Context Protocol.

Cue speaks [MCP](https://modelcontextprotocol.io), so any MCP-capable coding agent or client, from
Cursor, Windsurf, VS Code, Zed, Claude Code and Claude Desktop to one you wrote yourself, can run
all five workflows by calling tools instead of writing HTTP. It is the same API underneath. Every
tool authenticates with your Cue key, obeys the same rate limit and spend cap, and bills exactly
like the REST call it wraps.

<CardGroup cols={3}>
  <Card title="Endpoint" icon="link">
    `https://cue.vibeset.ai/mcp`
  </Card>

  <Card title="Transport" icon="bolt">
    Streamable HTTP (stateless)
  </Card>

  <Card title="Auth" icon="key">
    API key, either header
  </Card>
</CardGroup>

## Authentication

Cue's MCP server uses your API key, not an OAuth flow. Both headers work, so a config written
against either one is fine:

```
Authorization: Bearer vbsk_live_...
```

```
x-api-key: vbsk_live_...
```

The gateway checks neither. It defers `/mcp` to the app, which reads the bearer token first and
falls back to `x-api-key`. Bearer is what the MCP authorization spec uses and what most clients
default to.

Only `tools/call` is authenticated. `initialize`, `tools/list` and `ping` stay anonymous, because a
client has to finish a handshake before it has anywhere to put a credential.

<Note>
  Many remote MCP servers use OAuth and prompt you to sign in. Cue is simpler. Paste a key from the
  [developer portal](https://cue.vibeset.ai/developer) into your client's `headers`, the same key
  you would use for REST.
</Note>

Tools bill exactly like the REST calls they wrap, so an agent left running against real videos
spends real money. Accounts are provisioned with a \$100 monthly spend cap. Check yours in the
portal before you point an agent at a catalog. Keep keys server-side, and treat an agent config holding one like any
other secret.

## Connect

Most MCP clients take the same config shape, a streamable-HTTP server with a header. Add this `cue`
block to your client's MCP config:

```json theme={null}
{
  "mcpServers": {
    "cue": {
      "type": "http",
      "url": "https://cue.vibeset.ai/mcp",
      "headers": { "x-api-key": "vbsk_live_..." }
    }
  }
}
```

That drops into most agents as-is: `.mcp.json` (Claude Code), `.cursor/mcp.json` (Cursor), VS
Code's `mcp.json`, `~/.codeium/windsurf/mcp_config.json` (Windsurf), Zed's settings, and others.
Some clients omit `"type": "http"` and take `url` plus `headers` alone. A few client-specific
notes:

<Tabs>
  <Tab title="CLI agents">
    Agents with an `mcp add` command register Cue in one line, for example Claude Code:

    ```bash theme={null}
    claude mcp add --transport http cue https://cue.vibeset.ai/mcp \
      --header "x-api-key: vbsk_live_..."
    ```

    Cursor, Windsurf, and others accept the JSON block above in their MCP settings instead.
  </Tab>

  <Tab title="Desktop / IDE apps">
    In a desktop or IDE client, open its MCP settings. That is Settings, Developer, Edit config in
    Claude Desktop, and Settings, MCP in Cursor. Add the `cue` block to `mcpServers`, then restart or
    reload the app. Cue's tools then appear in the client's tool list.
  </Tab>

  <Tab title="SDK / custom">
    Point any MCP client or SDK at the streamable-HTTP endpoint and pass your key:

    * **URL** `https://cue.vibeset.ai/mcp`
    * **Transport** streamable HTTP
    * **Header** `x-api-key: vbsk_...` or `Authorization: Bearer vbsk_...`

    The server is stateless. Each request is self-contained, so there is no session to keep alive. The
    OpenAI Agents SDK, Anthropic's
    [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector), and other MCP
    SDKs all connect the same way.
  </Tab>
</Tabs>

## What you can do

Eight tools. Five are the workflows, three are plumbing. See the
[Tools reference](/docs/mcp-tools) for parameters, examples, and responses.

| Tool                                                  | Does                                               |
| ----------------------------------------------------- | -------------------------------------------------- |
| [`match_music`](/docs/mcp-tools#match_music)               | Match licensed music to a video.                   |
| [`underscore`](/docs/mcp-tools#underscore)                 | Match licensed music to a script, no video needed. |
| [`get_sync_points`](/docs/mcp-tools#get_sync_points)       | Where to cut, labeled sync offsets for a track.    |
| [`loop_music`](/docs/mcp-tools#loop_music)                 | Stretch a short track to cover a long video.       |
| [`map_beats`](/docs/mcp-tools#map_beats)                   | Beat and downbeat grid for audio you supply.       |
| [`ingest_video`](/docs/mcp-tools#ingest_video)             | Add a video for reuse, returns a `video_id`.       |
| [`check_video_status`](/docs/mcp-tools#check_video_status) | Is a video `processing`, `ready`, or `failed`?     |
| [`check_job`](/docs/mcp-tools#check_job)                   | Poll an async match, loop, sync, or beats job.     |

## Two decisions belong to the person

By default an agent using Cue stops twice and asks, rather than running the whole job on its own:

1. **Which track.** `match_music` returns a shortlist, not an answer. The agent should show several
   with their title, artist, tempo, length and whether they are instrumental, and let you pick.
2. **Where it loops or cuts.** `loop_music` returns a plan and `plan.alternates`, other loop
   regions found in the same track. `get_sync_points` returns a ranked list of offsets. Either way
   the agent should show the options and let you choose.

This is deliberate. Whether a track suits your piece turns on taste, tone and brand, and none of
that is in any field this API returns, so it is not the agent's call to settle quietly.

To skip the check-ins, say so: *"just pick the best one and run it through."* The agent will go end
to end and tell you afterwards what it chose and why. That is a mode you opt into.

<Warning>
  Ask an agent to rank tracks by `score`, `seam_score` or `plan_score` and it will produce a
  confident, wrong ordering. Each of those ranks within a single response. `seam_score` is scaled
  per song and `plan_score` per candidate set, so a `1.000` on one track is not better than a
  `0.91` on another. Compare on the plain facts instead: `repeats`, the loop region length,
  `sections_covered` and `end_label`.
</Warning>

## A typical agent flow

Ask your agent something like *"Find music for this clip and loop the best track under the whole
thing."* Under the hood it chains the tools:

<Steps>
  <Step title="Ingest">
    `ingest_video(video_url=…)` returns a `video_id` and a `poll` of `/v1/videos/{video_id}`. Then
    `check_video_status(video_id)` until it reads `ready`. Already have a `video_id`? Skip straight
    to matching.
  </Step>

  <Step title="Match">
    `match_music(video_id=…)` returns a ranked shortlist, each track with a `track_id`.
  </Step>

  <Step title="Sync or loop">
    `get_sync_points(video_id, track_id)` for where to cut, or `loop_music(video_id, track_id)` to
    lay a short track under a long clip.
  </Step>
</Steps>

`underscore` sits outside that chain. It takes a script and nothing else, so an agent working from
a draft, a storyboard, a voiceover, a post, can call it before any footage exists. `map_beats` sits
outside it too. It takes audio you can serve and never touches a video.

If the agent already knows which track it wants, `track_id` **is** the Soundstripe song id. Pass it
straight to `get_sync_points` or `loop_music` and skip matching entirely.

### The async pattern

First-time work runs off the request path so it never times out, and comes back as a job instead of
a result:

```json theme={null}
{ "status": "processing", "job_id": "9f2c1a4e-3b77-4e21-8f0d-2a1c6b4e9d55", "poll": "/v1/loop/jobs/9f2c1a4e-…", "status_code": 202 }
```

Job ids are bare UUIDs. Beats job ids are 32 hex characters with no dashes. Neither carries a
prefix.

Have the agent call `check_job(job_id, kind)`, where `kind` is `"match"`, `"loop"`, `"align"` or
`"beats"`, until it returns `status: "complete"`. Warm calls return the result directly with no
polling.

<Note>
  `match_music(video_url=…)` is the exception. It ingests only. It returns a `video_id`, a `poll`
  of `/v1/videos/{video_id}`, and a `next` telling you to call match again once the video is ready.
  There is no `job_id` in that response, so `check_job` has nothing to poll. Use
  `check_video_status(video_id)`, then call `match_music(video_id=…)`.
</Note>

Expect a job on the first call for any given pairing. `loop_music` is warm only when that exact
`(video_id, track_id)` pair has been built before. `get_sync_points` is warm on that, or when Cue
already holds both the video's sync profile and the track's audio profile. `match_music` returns a
job whenever the video's analysis is not cached yet.

## Errors

Auth failures are answered at the HTTP layer, so a client can check the status code. A rejected
tool call is a real HTTP `401` with a `WWW-Authenticate: Bearer` challenge. Every other failure
comes back as an MCP tool error carrying the same reason the REST API would give, because a bare
transport status reaches an agent as an opaque failure it cannot act on:

* `401`, missing or invalid key. An HTTP status, not a tool error. Check the header in your client
  config.
* `402`, no card on file, credits spent or expired, or the monthly spend cap hit. The message says
  which.
* `403`, the video or job belongs to another account.
* `404`, unknown `video_id` or `job_id`.
* `429`, over the 60 requests per minute limit. The agent should back off and retry.
* `502`, an async match, loop or sync job failed. A failed beats job comes back as a `422`.

See [Errors & rate limits](/docs/errors-and-limits) for the full list.

## Billing

The MCP server is a thin facade over `/v1`. A tool call is authenticated, rate-limited,
spend-capped, and metered exactly once, identically to the REST call it wraps. Going through MCP
never bills twice.

`check_video_status` is never billed. `check_job` is not billed either. The underlying job bills
once when it completes, whether or not you ever poll it, so polling adds nothing and skipping the
poll saves nothing.

What each call costs is on [Metering & pricing](/docs/metering-and-pricing).

## Troubleshooting

<AccordionGroup>
  <Accordion title="The tools don't show up">
    Confirm the server is connected in your client's MCP status (`/mcp` in Claude Code, the MCP or
    tools panel in an IDE or desktop app) and reload or restart it after editing the config. Check
    the JSON is valid and the URL is exactly `https://cue.vibeset.ai/mcp`.
  </Accordion>

  <Accordion title="Every call fails with 401">
    Your key is not reaching the server, or it is no longer valid. Both `Authorization: Bearer` and
    `x-api-key` are accepted, so the header name is not the problem. Check the key is active in the
    portal, since a revoked key stops working immediately, and that your client is sending headers on
    the POST rather than only on the initial handshake.
  </Accordion>

  <Accordion title="A tool returns 'processing' and never finishes">
    That is the async pattern. Cold work returns a `job_id`. The agent should call
    `check_job(job_id, kind)` until `status` is `complete`. If it stays `processing` for minutes, the
    video may still be analyzing, so check `check_video_status` first. Do not resend the original
    call while a job is in flight — poll the job you have. Resending with the same `idempotency_key` is safe if you must: it returns the original job rather than starting a second one.
  </Accordion>

  <Accordion title="loop_music says applicable: false">
    The video is not meaningfully longer than the track, so there is nothing to loop, and you are not
    billed. `hint` says why in plain language and `retryable` says whether calling again can change
    it. Use loop for clips longer than the track by 30s or more. For shorter clips, use
    `get_sync_points` and play the track straight. See [Loop](/docs/loop).
  </Accordion>
</AccordionGroup>
