# MCP
URL: https://openship.io/docs/mcp.md

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.

A read-only token can only reach GET tools. A scoped token sees tools enabled by its grants, including
collection tools when it has the matching wildcard permission. A grant to one project does not authorize
listing every project. 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

To see your **workspace ID**, ask the agent to call `get_permissions_workspaces` with no arguments.
Openship calls it `organizationId` in the API. The result lists workspace names and IDs, including empty
workspaces, and identifies the current workspace and any credential binding.

When installing an app or deploying Compose, pass the chosen `organizationId` alongside `body` on each
tool call. The server uses that exact workspace. If several destinations are available, the agent's
instructions tell it to clarify the destination before creating resources. See
[workspace selection](/docs/api/mcp#discover-and-select-a-workspace) for examples.

Tokens and OAuth connections created today are bound to one organization. To target another, authorize
the connection or create a token in that workspace. Selecting an ID cannot widen a credential's scope,
and it does not move an already installed app between organizations.

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_permissions_workspaces` | `/api/permissions/workspaces` | [Workspace discovery](/docs/api/permissions#list-workspaces) |
| `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` | `/api/cloud` | [Cloud API](/docs/api/cloud) |
| `get_system_servers_destinations` | `/api/system/servers/destinations` | [Desktop folder → Cloud](/docs/api/mcp#deploy-a-desktop-folder-on-cloud) |
| `get_jobs`, `post_jobs_by_key_run` | `/api/jobs` | [Jobs guide](/docs/guides/jobs) |
| `get_apps_catalog`, `post_apps` | `/api/apps` | [App catalog](/docs/api/apps) |
| `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>

The [generated MCP catalog](/docs/api/mcp-tools) publishes the current tool names, descriptions and
HTTP-only boundaries directly from the routes. The [cluster and scaling guide](/docs/guides/mcp-clusters-and-scaling)
covers private networking, automated k3s setup, application replica changes, recovery and cleanup.
Manual replica scaling is supported; metric-driven autoscaling and live worker joining are not implemented.

## 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.

## Troubleshooting

<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).

## Related guides

<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>
