---
title: MCP
description: Let AI clients like Claude drive your Openship instance as tools, over the Model Context Protocol.
---

import { Step, Steps } from 'fumadocs-ui/components/steps';

**MCP (Model Context Protocol)** is a standard way for AI clients — Claude, Cursor, VS Code Copilot, and
others — to call an app's features as *tools*. Point one of those clients at Openship and it can list your
projects, kick off deployments, check logs, add domains, and more — using the exact same permissions you'd
have yourself.

The important part: it's not a second, looser door into Openship. **Every tool call re-runs the full auth
and permission stack**, so an agent can only ever see and do what its credential is allowed to — nothing more.

<Callout title="Where this lives in the dashboard">
Everything below is set up under **Settings → MCP**. That tab shows the exact endpoint for *your* instance,
a per-client "add" recipe (Claude Code, Codex, Cursor, VS Code, Claude Desktop, Windsurf, Zed), and the list
of clients you've already connected.
</Callout>

## The endpoint

There's one endpoint, and it's the same on self-hosted and Openship Cloud:

```
POST /api/mcp
```

So on your instance that's `https://<your-host>/api/mcp`. A few things worth knowing up front:

- It's a stateless **Streamable-HTTP JSON-RPC 2.0** endpoint — request in, response out.
- It's **`POST`-only**. A `GET` returns `405` (there's no server→client stream to open).
- **No batching** — send one JSON-RPC message per request, not an array.

You rarely type this URL by hand. **Settings → MCP** shows it pre-filled and gives you a copy button.

## Connect a client

There are two ways to authenticate, and most clients pick one for you. Use the tab that matches your client.

<Tabs items={['OAuth (recommended)', 'Static token']}>

<Tab value="OAuth (recommended)">

Openship is a standards-compliant **OAuth 2.1** MCP server, so capable clients need nothing but the URL.

<Steps>

<Step>
### Paste the endpoint into your client

Give the client your endpoint — `https://<your-host>/api/mcp`. In Claude Code that's one command:

```bash
claude mcp add --transport http openship https://<your-host>/api/mcp
```

Codex registers the server and authorizes it as two commands:

```bash
codex mcp add openship --url https://<your-host>/api/mcp
codex mcp login openship
```

Other clients take it through their own UI; **Settings → MCP** has the exact snippet for each one.
</Step>

<Step>
### Approve it in the browser

For most clients this happens on the first connect: a browser window opens by itself. With Codex it's the
explicit `codex mcp login openship` above — the server is registered without it, but every call comes back
unauthorized until you run it.

Either way, that browser page is where you **choose what the client may do** — read-only or full control,
and either all resources or a specific set of projects, servers, and repositories. No credential to copy
or paste.
</Step>

<Step>
### You're connected

The client can now call tools within exactly the scope you approved. The connection shows up under
**Settings → MCP → Connected clients**, where you can disconnect it at any time.
</Step>

</Steps>

<Callout title="The scope you pick is enforced, not advisory" type="info">
Your choices are stored as the client's grant — the same grant model as a scoped [personal access
token](/docs/api/tokens). The client can never exceed what you granted, or what you can access yourself.
</Callout>

<Callout title="Codex: local surfaces share one config; hosted tasks don't" type="warn">
The Codex **CLI, desktop app, and IDE extension** all read the same local MCP configuration, so adding
Openship once covers all three. Restart any of them that were already running — they read the config at
startup.

A **hosted** Codex task is a different environment. It doesn't inherit your machine's MCP config, and it can
only reach `https://<your-host>/api/mcp` if that environment has network access to your instance. For a
private or LAN-only self-hosted Openship, it won't — the endpoint has to be reachable from wherever the agent
actually runs. Openship Cloud, or a self-hosted instance on a public hostname, is reachable; a
`localhost`/private-network endpoint is not. See [remote access](/docs/getting-started/installation) for
exposing an instance deliberately.
</Callout>

</Tab>

<Tab value="Static token">

For clients without OAuth support — or when you want a fixed, narrowly-scoped credential in a config file —
authenticate with a **personal access token** as a bearer credential. There's no separate MCP credential.

Create the token under **Settings → Tokens** (see the [Tokens API](/docs/api/tokens)), then drop the endpoint
and token into your client's config:

```json
{
  "mcpServers": {
    "openship": {
      "url": "https://<your-host>/api/mcp",
      "headers": { "Authorization": "Bearer opsh_pat_…" }
    }
  }
}
```

For Claude Code, add the header on the command:

```bash
claude mcp add --transport http openship https://<your-host>/api/mcp \
  --header "Authorization: Bearer opsh_pat_…"
```

<Callout title="Give an agent a narrow token" type="warn">
Mint a **read-only** or **scoped** token for agents rather than handing over your full access. A read-only
token limits the agent to read tools; a scoped token confines it to exactly the projects, servers, and
repositories you grant — even below your own role.
</Callout>

</Tab>

</Tabs>

## Every call is re-checked

This is the heart of the security model, and it's why MCP access is as safe as your token:

- Authenticating once resolves your **capability**, but that's used only to **filter `tools/list`** — so a
  client isn't shown tools its token could never use.
- The real gate runs on **every `tools/call`**: the endpoint builds an internal request, forwards your
  bearer, and dispatches it through the same routes, validation, auth middleware, and per-resource permission
  check as a normal HTTP call.

The practical upshot: a read-only token can only reach read tools, and a scoped token stays boxed into its
granted resources. A scoped token isn't even *shown* the org-wide list and create tools — `get_projects`
(list every project) simply won't appear in its `tools/list`; it sees only the per-resource tools for the
resource types it was granted. And if a `tools/call` touches something outside that scope, it comes back
`404` — the scope doing its job. For the full protocol details (JSON-RPC methods, negotiation, error codes),
see the [MCP API reference](/docs/api/mcp).

The **organization boundary** applies to MCP exactly as it does to a browser session: because every call
runs through the same per-resource permission check, an MCP client acts only within organizations you
belong to and can never reach another org's projects, servers, or secrets — a resource outside your
membership comes back `404`, just as it would in the dashboard. See [Isolation](/docs/security/isolation).

## Which tools map to which API

Tools aren't a separate feature set — each one **maps onto a permission-tagged REST route**, and the tool name
is built from that route's method and path. So `get_projects` is `GET /api/projects`, `post_domains` is
`POST /api/domains`, and `patch_projects_by_id` is `PATCH /api/projects/:id`. Call `tools/list` to see the
exact set your token can use.

Not every route is a tool: a route becomes one only when it **opts in**, so the set is a curated slice of the
API rather than the whole surface. Each exposed tool also carries two hints a client can surface —
`readOnlyHint` (the tool only reads) and `destructiveHint` (it deletes or tears something down) — so an agent
UI can flag a destructive call before it runs.

| Tool name (examples) | REST route | Reference |
|---|---|---|
| `get_projects`, `patch_projects_by_id` | `/api/projects` | [Projects API](/docs/api/projects) |
| `get_deployments`, `post_deployments_build_access` | `/api/deployments` | [Deployments API](/docs/api/deployments) |
| `get_projects_by_id_services`, `post_projects_by_id_services` | `/api/projects/:id/services` | [Services API](/docs/api/services) |
| `get_domains`, `post_domains` | `/api/domains` | [Domains API](/docs/api/domains) |
| `get_github_repos` | `/api/github` | [GitHub API](/docs/api/github) |
| `get_analytics` | `/api/analytics` | [Analytics API](/docs/api/analytics) |
| `get_notifications_channels` | `/api/notifications` | [Notifications API](/docs/api/notifications) |
| `get_settings`, `patch_settings_build_mode` | `/api/settings` | [Settings API](/docs/api/settings) |
| `get_projects_by_projectId_backup_policies`, `post_backup_policies_by_policyId_run` | `/api/backups` | [Backups API](/docs/api/backups) |
| `get_cloud_status`, `get_cloud_workspaces` | `/api/cloud` | [Cloud API](/docs/api/cloud) |
| `get_jobs`, `post_jobs_by_key_run` | `/api/jobs` | [Jobs guide](/docs/guides/jobs) |
| `get_apps_catalog`, `post_apps` | `/api/apps` | *(REST reference pending)* |
| `get_updates`, `post_updates_scan` | `/api/updates` | [Updating Openship](/docs/guides/updating) |

<Callout title="Credentials and auth are never exposed" type="info">
The `tokens`, `auth`, and `mcp` modules are hard-blocked from ever becoming tools — a full-access MCP token
can't mint itself a fresh credential or escape its own scope. Manage tokens and connected clients from the
dashboard or the [Tokens API](/docs/api/tokens), not from an agent.
</Callout>

## Guided flows

Some tasks are a chain of calls in a specific order — deploying a repo, uploading a folder, installing a
catalog app. The server ships a small set of **prompts** (`prompts/list` / `prompts/get`) that spell those
chains out step by step, referencing the exact tools to call. Compatible clients surface them as
slash-commands or starters, so an agent can pick a flow and follow it rather than reverse-engineering the
order from the flat tool list. See the [MCP API reference](/docs/api/mcp#guided-flows-prompts) for the full
catalog.

## Verify from the terminal

To confirm a token works and see what it can do, ask for the tool list:

```bash
curl -s https://<your-host>/api/mcp \
  -H "Authorization: Bearer opsh_pat_…" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

This returns exactly the tools that token is allowed to call.

## If something goes wrong

<Callout title="401 — Missing or invalid access token" type="error">
There's no bearer credential, or it's expired or revoked. The response includes a `WWW-Authenticate` header
so OAuth clients can start discovery; if you're using a static token, mint a fresh one under
**Settings → Tokens** and update your client config.
</Callout>

<Callout title="405 on a GET request" type="warn">
Expected. This is a request/response server with no server→client stream, so `GET /api/mcp` isn't supported.
Always `POST` your JSON-RPC message.
</Callout>

<Callout title="A tool call returns 404 for a project, server, or repo" type="info">
`tools/list` worked but a specific `tools/call` reports `404` — that's the token's scope working as intended.
It wasn't granted that resource. Re-authorize the client (or mint a token) with the resource included.
</Callout>

## Manage connected clients

Every OAuth client you've approved is listed under **Settings → MCP → Connected clients**, tagged with its
access level (read-only or full control) and how many resources it can reach. **Disconnect** revokes the
client's issued tokens immediately and drops its consent, so it has to re-authorize next time. The same list
and controls are available programmatically via the [Tokens API](/docs/api/tokens).

## What next?

<Cards>
  <Card title="MCP API reference" href="/docs/api/mcp" description="The JSON-RPC methods, protocol negotiation, and error codes behind /api/mcp." />
  <Card title="Tokens API" href="/docs/api/tokens" description="Mint scoped or read-only personal access tokens and manage connected MCP clients." />
  <Card title="Settings" href="/docs/dashboard/settings" description="Where the MCP tab, tokens, and connections live in the dashboard." />
</Cards>
