> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://onepin.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/docs/_mcp/server.

# MCP server

> Onepin's remote MCP server lets an agent browse voices, author and validate workflows, price a run, start it, and hand the audio back — over OAuth, with no API key.

Onepin runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server at `https://mcp.onepin.ai`. Connect it once and your agent — Claude Code, Claude Desktop, Cursor, ChatGPT — gets nineteen tools that browse the voice catalog, author and validate workflows, price a run, start it, and hand the audio back as a link you can play.

It acts on the same workspace the console and the [REST API](/docs/api-reference) do, with exactly the permissions your workspace role already gives you. It doesn't replace the [CLI](/docs/reference/cli-reference) — what it adds is a one-command connection with nothing to install, and a confirmation step the *server* enforces before anything is spent or overwritten.

The CLI's [agent skill](/docs/get-started/agent-skill) is the other way to hand Onepin to an agent, and the two coexist. The skill has your agent run the local CLI under your API key; this runs the tools server-side under your sign-in. [Which to pick](/docs/get-started/agent-skill#skill-or-mcp).

## Sign in — there is no API key

MCP authenticates by OAuth only. You don't mint, paste, or store anything: your client sends you to a normal Onepin sign-in, you approve it once, and it holds a token that refreshes itself. The connector acts **as you**.

> **Warning**
>
> API keys are not accepted here. A client configured with an `Authorization: Bearer op_live_…` header gets a `401` telling it to sign in — remove the header and reconnect. Keys still work everywhere else; see [Authentication](/docs/get-started/authentication).

Revoke the connection from your account settings at any time; the token stops working within a minute.

## Connect

Paste the bare hostname. **There is no path and no trailing slash** — MCP has its own subdomain, and the OAuth resource identifier has to equal what you typed, character for character. A stray `/mcp` is a discovery failure your client then caches for about five minutes.

### Claude Code

```bash
claude mcp add --transport http onepin https://mcp.onepin.ai --scope user
```

Then run `/mcp`, pick **onepin**, and choose **Authenticate** — Claude Code never opens a browser by itself. `--scope user` makes the server available in every project; the default (`local`) registers it only for the directory you ran the command in.

### Claude Desktop and claude.ai

**Settings → Connectors → Add custom connector**, paste `https://mcp.onepin.ai`, and sign in when the browser opens.

### Cursor

Add it to `~/.cursor/mcp.json` (or `.cursor/mcp.json` for a single project):

```json
{
  "mcpServers": {
    "onepin": {
      "url": "https://mcp.onepin.ai"
    }
  }
}
```

### Anything else

The server speaks streamable HTTP and advertises OAuth 2.1 at `https://mcp.onepin.ai/.well-known/oauth-protected-resource`. Any client that can discover an authorization server connects with the URL alone.

> **Note**
>
> `api.onepin.ai/mcp` is retired and answers `410 Gone`. A connector still pointed at it must be re-added with the hostname above — the resource identifier is per-URL, so the old one signs you in and then fails the match.

### Verify

Ask for something small — *"list my Onepin workflows"*. If you belong to more than one workspace, the first tool call says so and lists them: `list_workspaces`, then `select_workspace`. The choice is remembered for 30 days and across reconnects.

## Tools

### Workspace

| Tool               | What it does                                                               |
| ------------------ | -------------------------------------------------------------------------- |
| `list_workspaces`  | List the workspaces you can work in, and which one is selected             |
| `select_workspace` | Choose which workspace the other tools act in — remembered across sessions |

### Workflows

| Tool                            | What it does                                                                   |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `list_workflows`                | This workspace's workflows, most recently updated first                        |
| `get_workflow`                  | One workflow as a markdown page describing what its graph does                 |
| `get_node_info`                 | The node types you can use, or one node's config schema and ports              |
| `validate_workflow_definition`  | Check a definition against every gate a run start applies — saves nothing      |
| `create_workflow`               | Create a workflow from a definition `validate_workflow_definition` approved    |
| `update_workflow`               | Replace an existing workflow's graph with an edited definition *(destructive)* |
| `list_templates`                | Browse the [template gallery](/docs/guides/use-templates)                      |
| `create_workflow_from_template` | Clone a template into this workspace as a runnable workflow                    |

### Voices

| Tool                 | What it does                                                            |
| -------------------- | ----------------------------------------------------------------------- |
| `list_providers`     | The speech providers you can use, or one provider's models              |
| `find_voices`        | Search the voice catalog — `search` takes a plain-language description  |
| `preview_voice`      | Play a voice, in the language it will actually speak                    |
| `set_workflow_voice` | Pin a catalog voice onto one locale of a generator node *(destructive)* |

### Runs

| Tool              | What it does                                                      |
| ----------------- | ----------------------------------------------------------------- |
| `estimate_run`    | Price a run before starting it — creates nothing, charges nothing |
| `run_workflow`    | Start a run *(destructive — spends credits)*                      |
| `get_run_status`  | How far a run has got                                             |
| `get_run_outputs` | The audio a run produced, one entry per output node               |
| `download_run`    | One ZIP link containing every audio file a completed run produced |

**Destructive means it can destroy or overwrite work you already have.** `run_workflow` spends credits irreversibly, `set_workflow_voice` is a last-write-wins overwrite of a voice map an open canvas may be editing, and `update_workflow` is that same overwrite over the whole graph — the definition it replaces is not versioned anywhere you can get it back from. Creating a workflow is additive, so it isn't marked.

## Nothing is spent or overwritten on the first call

`run_workflow`, `create_workflow`, and `update_workflow` do nothing the first time they are called. Each returns `requires_confirmation: true` carrying what you need to judge it, and acts only on a second call with `confirm=true`:

* **A run** comes back with `workflow_name`, `workflow_summary`, `expected_credits`, `max_credits`, and `current_balance` — plus `estimated_overage_cents` when it would bill past your plan allowance.
* **A create** comes back with `name` and `workflow_summary`, and `not_included` when the graph leaves something out: no translator, no normalizer, no quality check, or a check whose failing lines are *discarded*. If the workspace already holds something that looks like what you asked for, those candidates come back instead, and `force=true` is how you decline the offer.
* **An edit** adds `replaces` — the graph that is there now — `content_changes` for any node text the edit would rewrite, and an `edit_token` the confirming call must carry. That token is signed over the workflow, the definition, and the graph as it stood, so a workflow someone else changed while you were reading is refused rather than silently overwritten.

Two tools also require a token from a read-only call, so that what you approved is what happens: `create_workflow` needs the `validation_token` from `validate_workflow_definition`, and `run_workflow` needs the `run_token` from `estimate_run`. The run token is single-use and is refused if the workflow moved after you priced it, or if the price rose above the ceiling you approved. Both expire after **10 minutes**, and a refusal always means *nothing happened* — the remedy is one more read-only call.

> **Note**
>
> Confirmation is relayed by your client, so no MCP server can prove a person saw it. For a ceiling that doesn't depend on the client, put it on the caller's authority instead: a `viewer` role cannot start a run, and the workspace credit balance is a hard cap.

## Audio comes back as links

Every audio URL this server hands back — `find_voices` and `preview_voice` samples, `get_run_outputs`' per-line files — is a plain presigned file: no auth header, no page to log into, good for about an hour.

What that looks like depends on your client. One with a shell (Claude Code, Cursor) fetches the URL and plays it with whatever your machine has. A host that supports MCP Apps (Claude Desktop, claude.ai, Cursor, VS Code) renders an inline player — one row per sample, one per line for a run, nothing autoplaying, and past 20 lines it points at `download_run` instead. Anything else gets a titled link on its own line rather than a raw presigned URL.

Nothing plays unasked. `preview_voice(search=…)` searches and hands back a shortlist with `played: false`, leaving the choice to you; pass `catalog_voice_id` when you've already named a voice and it plays straight away.

## Authoring a workflow through an agent

`get_node_info()` with no argument returns three things: the node catalog, `definition_shape` — the envelope, where `nodes` and `edges` live under `graph` rather than at the top level — and `pipeline_choices`, the stages that are a decision rather than a default (a translator, a normalizer, a quality check) with what skipping each one does instead.

Two things no `config_schema` will tell you:

* **Don't compute positions.** `create_workflow` lays the whole graph out with the product's own canvas layout, so send `{"x": 0, "y": 0}` for every node. On an update only the nodes your edit touched are re-placed — cards someone dragged stay where they put them.
* **Wire a validator's `fail` port, or know why you aren't.** Connect it to the generator's `lines` port and a failing line is regenerated, bounded by that validator's `max_retries`. Leaving it unconnected is legal and means the opposite: failing lines are discarded, and the run completes with a shorter export than the script. See [How validators work](/docs/workflow/validators).

Editing a workflow you already have is a round trip:

```
get_workflow(include_definition=true) → edit → validate_workflow_definition
  → update_workflow()                      # writes nothing; returns replaces + an edit_token
  → update_workflow(confirm=true, edit_token=…)
```

Existing runs are untouched by an edit — hearing the difference means `estimate_run` and `run_workflow` again, which charges credits again.

## Troubleshooting

| Symptom                                                       | Cause                                                                                                                                                      |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code says `! Needs authentication` and nothing happens | That's the resting state, not a fault — it never opens a browser by itself. Run `/mcp`, pick the server, choose **Authenticate**                           |
| A `401` telling you to sign in                                | An API key was sent. Remove the `Authorization` header from your client config and reconnect                                                               |
| The server doesn't appear in your client at all               | It's registered to one project rather than to you. Run `claude mcp list` from that directory and re-add with `--scope user`                                |
| `410 Gone` naming a different URL                             | You're connecting to the retired `api.onepin.ai/mcp`. Re-add the connector with `https://mcp.onepin.ai`                                                    |
| "Couldn't reach the MCP server", although the host is up      | The URL you pasted carries a path or a trailing slash. Re-add the bare hostname — discovery is cached for about five minutes, so the fix isn't instant     |
| Sign-in completes but tools say the account is unknown        | That account has no Onepin user yet. Open [app.onepin.ai](https://app.onepin.ai) once — MCP won't create one from an OAuth token                           |
| Tools say the workspace is ambiguous                          | You're a member of several. `list_workspaces`, then `select_workspace`                                                                                     |
| A tool answers `requires_confirmation: true` and does nothing | That's the design. Read the details back to the user, then call the same tool again with `confirm=true`. Nothing was created, started, or charged          |
| `run_workflow` says the workflow changed since you priced it  | Something wrote to it between `estimate_run` and `run_workflow`. Nothing started and nothing was charged — price it again and run with the new `run_token` |
| `run_workflow` says the run token was already used            | It already started a run. Poll `get_run_status` rather than pricing and running again, which would charge twice                                            |
| A token is rejected as expired                                | Tokens last 10 minutes. Call `estimate_run` (or `validate_workflow_definition`) again and use the fresh one                                                |
| Tools missing after a server update                           | Restart the client — the tool list is fetched on connect and cached                                                                                        |