# Connect an MCP client (https://upapi.io/docs/mcp-clients)



Pick a transport, then copy the block for your client. Both transports expose
tools with the same schemas and draw on the same monthly pool of
[weighted units](https://upapi.io/docs/rate-limits). They differ in how a call is authenticated,
and — following from that — in what the per-minute burst limit is counted
against: the hosted server meters per organization, the local server per API key.
The hosted server serves every public operation; the local server catches up in the next
`@upapi/mcp` release (0.2.0 serves 63 and still withholds Social Media and Utility in `directory` mode).

{/* TODO(mcp-release, 2026-10-08): drop the qualifier after @upapi/mcp 0.3.0 ships (order: sdk → mirror --only mcp → mcp → registry → mcpb → GH release) */}

See [MCP server](https://upapi.io/docs/mcp) for what the tools look like once connected.

To let a coding agent do all of this itself, see [Agent setup](https://upapi.io/docs/agent-setup).

| Transport | Hosted                         | Local                           |
| --------- | ------------------------------ | ------------------------------- |
| Endpoint  | `https://app.upapi.io/api/mcp` | `npx -y @upapi/mcp` (stdio)     |
| Auth      | sign in to upAPI in a browser  | `UPAPI_API_KEY` in the config   |
| Runs      | on upAPI                       | on your machine                 |
| Best for  | desktop clients and IDEs       | scripts, CI, self-hosted agents |

Prefer the hosted server. It authorizes over OAuth 2.1 with PKCE against the
same accounts system as the dashboard, so there is no key to paste into a file
on disk and no key to rotate when a laptop is lost.

<Callout type="warn" title="The hosted server needs a full account">
  A [guest session](https://upapi.io/docs/quickstart#1-create-an-account) cannot connect: the endpoint answers `401
    GUEST_FORBIDDEN`. Guest identities are temporary and get reaped, which would break a long-lived
  integration in a way you could not diagnose from the client. Convert the guest account first — the
  local server has no such restriction, since it authenticates with an API key.
</Callout>

For the local server, create a key at
[Dashboard → API keys](https://app.upapi.io/dashboard/api-keys) first. It reads
two environment variables:

| Variable          | Required | Value                                                            |
| ----------------- | -------- | ---------------------------------------------------------------- |
| `UPAPI_API_KEY`   | Yes      | an `upapi_` key                                                  |
| `UPAPI_BASE_URL`  | No       | gateway origin, defaults to `https://api.upapi.io`               |
| `UPAPI_TOOL_MODE` | No       | `compact` (default), `directory`, `claude` or `full` — see below |

## Which tools you get [#which-tools-you-get]

Within a transport, every mode serves the same operations. What changes between modes is how
many of them are *advertised* in `tools/list` — a context-budget and
presentation choice, never an access one. An operation a mode does not list is
still callable by name.

| Mode        | Advertises                                                    | Use it when                                                       |
| ----------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| `compact`   | `search_ops` + `call_op` + three always-on operations         | a long chat that re-sends its tool table every turn               |
| `directory` | 11 named flagship tools, read tools and write tools separated | a desktop client, or anywhere a human reads the tool list         |
| `claude`    | 8 named tools upAPI computes itself, reads and writes split   | the Claude connector listing                                      |
| `full`      | one tool per public operation                                 | a fixed workflow that wants every schema present without a lookup |

Every tool in every mode carries the four MCP behavioural hints —
`readOnlyHint`, `destructiveHint`, `idempotentHint` and `openWorldHint` — so a
client can decide what may run unattended without inferring it from a name.

The hosted server picks the mode from the URL and defaults to `compact`:

```text
https://app.upapi.io/api/mcp                    # compact
https://app.upapi.io/api/mcp?tools=directory    # named flagship tools
https://app.upapi.io/api/mcp?tools=claude       # named tools upAPI computes itself
https://app.upapi.io/api/mcp?tools=full         # one tool per operation
```

The local server reads `UPAPI_TOOL_MODE` and defaults to `compact`, the same shape the hosted
endpoint defaults to. `full` (one tool per operation, this server's original default) remains
available as a rollback for a client already configured against it. An unrecognized value falls
back to the default rather than refusing to start.

## Claude Code [#claude-code]

```bash title="Hosted"
claude mcp add --transport http upapi https://app.upapi.io/api/mcp
```

Claude Code opens a browser to authorize the connection on first use.

```bash title="Local"
claude mcp add upapi --env UPAPI_API_KEY=upapi_YOUR_KEY -- npx -y @upapi/mcp
```

## Claude Desktop [#claude-desktop]

The shortest path is the **desktop extension**. Download `upapi.mcpb` from the
[latest release](https://github.com/DevinoSolutions/upapi-mcp/releases/latest),
double-click it, and paste your API key when Claude Desktop asks: the key is
held as a sensitive setting rather than written into a config file on disk, and
the extension defaults to the `directory` tool table.

For the hosted server, add it through the app rather than the config file:
**Settings → Connectors → Add → Add custom connector**, then enter
`https://app.upapi.io/api/mcp` and click Add. Claude connects from Anthropic's
infrastructure rather than from your machine, which is fine here — the endpoint
is public.

The local server goes in `claude_desktop_config.json`:

```json title="claude_desktop_config.json"
{
  "mcpServers": {
    "upapi": {
      "command": "npx",
      "args": ["-y", "@upapi/mcp"],
      "env": {
        "UPAPI_API_KEY": "upapi_YOUR_KEY"
      }
    }
  }
}
```

Restart Claude Desktop; the operations appear in its tool list.

## Cursor [#cursor]

One click installs the hosted server:
[**Add upAPI to Cursor**](cursor://anysphere.cursor-deeplink/mcp/install?name=upapi\&config=eyJ1cmwiOiJodHRwczovL2FwcC51cGFwaS5pby9hcGkvbWNwIn0=).
The link only opens Cursor's install dialog — nothing is written until you
confirm there.

Both forms also go in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json`
(global):

```json title=".cursor/mcp.json — hosted"
{
  "mcpServers": {
    "upapi": {
      "url": "https://app.upapi.io/api/mcp"
    }
  }
}
```

```json title=".cursor/mcp.json — local"
{
  "mcpServers": {
    "upapi": {
      "command": "npx",
      "args": ["-y", "@upapi/mcp"],
      "env": {
        "UPAPI_API_KEY": "upapi_YOUR_KEY"
      }
    }
  }
}
```

## VS Code [#vs-code]

VS Code uses `servers` rather than `mcpServers`, and wants an explicit `type`.
Put this in `.vscode/mcp.json` for one workspace, or in your user profile's
`mcp.json` for all of them:

```json title=".vscode/mcp.json — hosted"
{
  "servers": {
    "upapi": {
      "type": "http",
      "url": "https://app.upapi.io/api/mcp"
    }
  }
}
```

For the local server, take the key from a prompted input rather than hardcoding
it — `.vscode/mcp.json` is usually committed:

```json title=".vscode/mcp.json — local"
{
  "inputs": [
    {
      "type": "promptString",
      "id": "upapi-key",
      "description": "upAPI API key",
      "password": true
    }
  ],
  "servers": {
    "upapi": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@upapi/mcp"],
      "env": {
        "UPAPI_API_KEY": "${input:upapi-key}"
      }
    }
  }
}
```

## Other clients [#other-clients]

Any client that can launch a stdio server takes the same command
(`npx -y @upapi/mcp`) with `UPAPI_API_KEY` in its environment, and any client
that speaks remote MCP takes the hosted URL. Nothing in either transport is
client-specific.

<Callout type="warn" title="Your key is a credential in a config file">
  Every `UPAPI_API_KEY` block above puts a live key in plain text on disk. Use a key created for
  this purpose so you can delete it without disturbing production, keep the file out of dotfile
  repositories, and prefer the hosted server or VS Code's `inputs` prompt where you can.
</Callout>
