API

Tokens and MCP clients

Create scoped access tokens and manage MCP client consent.

The Tokens API manages the credentials that authenticate you to Openship: personal access tokens (PATs) for the CLI and direct API calls, and the MCP client bindings created when an AI assistant connects over OAuth. In the dashboard this is the Settings → Access area; from the terminal it's the openship token command. Every route is self-scoped — you only ever see and manage your own tokens and clients.

Create a token

Mints a new PAT and returns the plaintext secret in data.token exactly once — it is hashed at rest and can never be retrieved again. List responses only ever show the short tokenPrefix.

const result = await ship.tokens.create({
  name: "deploy-bot",
  readOnly: false,
  grants: [
    {
      resourceType: "project",
      resourceId: "proj_123",
      permissions: ["read", "write"],
    },
  ],
});
console.log(result);
curl -X POST "$OPENSHIP_URL/api/tokens" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"deploy-bot","readOnly":false,"grants":[{"resourceType":"project","resourceId":"proj_123","permissions":["read","write"]}]}'

Prop

Type

Scope must be stated, not inferred

Send exactly one of grants (scoped) or fullAccess: true (unscoped). An empty or omitted grants with no fullAccess returns 400 TOKEN_SCOPE_REQUIRED; sending both returns 400 AMBIGUOUS_TOKEN_SCOPE.

Openship used to treat an empty grants as full access. That made "I want no limits" and "my scoped selection came out empty" the same request — and it failed open for the dangerous one, so a caller that meant to restrict a token could silently receive full access. Intent is now explicit.

Each entry in grants is a TokenGrant:

Prop

Type

Copy the secret immediately

The response's data.token (an opsh_pat_… string) is shown once and never again. GET /api/tokens only returns the prefix. If you lose it, revoke the token and mint a new one.

A token can never exceed its minter

Every grant is validated against your own access before the token is created. An unknown resourceType returns 400 INVALID_RESOURCE_TYPE; granting access you don't hold yourself returns 403 GRANT_EXCEEDS_ACCESS.

The "projects it creates" scope

To hand an agent (e.g. an MCP client) the ability to create new projects and fully manage only the ones it creates — with zero visibility into anything else — give it a single create grant on the project collection:

{ "name": "agent", "grants": [{ "resourceType": "project", "resourceId": "*", "permissions": ["create"] }] }

create is collection-only: it authorizes POST /api/projects and lets the token list projects (filtered to its own), but grants no read/write/admin on any existing project. Each project the token creates auto-adds a { project, <id>, [read, write, admin] } grant, so it fully controls its own work and nothing else. In the dashboard this is the "Only projects it creates" option on the MCP authorize screen.

List tokens

const tokens = await ship.tokens.list();
console.log(tokens);
curl "$OPENSHIP_URL/api/tokens" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

Returns your tokens with no secret material — each row carries id, name, tokenPrefix, readOnly, scoped, expiresAt, lastUsedAt, revokedAt, and createdAt.

Revoke a token

await ship.tokens.revoke("pat_123");
curl -X DELETE "$OPENSHIP_URL/api/tokens/pat_123" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

404 — token not found

The :id isn't one of your own tokens (or was already removed). List your tokens with GET /api/tokens to get valid IDs.

MCP clients

When an AI assistant connects to Openship over OAuth, the consent page records what that client is allowed to do, then Openship issues it scoped access tokens that run through the same scoped-principal path as a PAT. These routes back that flow and the Settings → Access → Connected clients list.

Authorize a client

Called by the OAuth consent page (browser cookie session) before a token is issued. It persists the user's chosen read-only flag and resource grants as the client's binding. Grants are validated ⊆ your own access, identical to token creation. Re-authorizing overwrites the binding's grants wholesale.

POST /api/tokens/mcp-authorize

Prop

Type

Consent errors

Missing clientId returns 400 CLIENT_ID_REQUIRED; an organizationId you're not a member of returns 403 ORG_NOT_A_MEMBER. Invalid or over-broad grants fail the same way as token creation (400 INVALID_RESOURCE_TYPE / 403 GRANT_EXCEEDS_ACCESS).

Scope intent is explicit here too: an empty grants without fullAccess: true returns 400 TOKEN_SCOPE_REQUIRED, and sending both returns 400 AMBIGUOUS_TOKEN_SCOPE. An unscoped binding lets the client act with your own role, so it can never be reached by omission.

List connected clients

GET /api/tokens/mcp-clients

Returns one entry per OAuth binding with clientId, name, organizationId, organizationName, readOnly, scoped, grantCount, authorizedAt, and lastUsedAt.

Disconnect a client

Revokes the client's issued tokens first (stopping it immediately), then drops the scope binding, its grants, and the recorded consent so a reconnect re-prompts. Scoped to you — a client shared across users keeps working for everyone else.

DELETE /api/tokens/mcp-clients/:clientId

Operations

OperationSDKREST API
List your tokens without exposing their secrets.tokens.list()GET /api/tokens
settings:read
Create a token; its secret is returned once.tokens.create(input)POST /api/tokens
settings:write
Authorize or explicitly re-scope an MCP client binding.tokens.authorizeMcpClient(input)POST /api/tokens/mcp-authorize
settings:write
List your authorized MCP clients.tokens.listMcpClients()GET /api/tokens/mcp-clients
settings:read
Revoke a token by its token ID.tokens.revoke(id)DELETE /api/tokens/:id
settings:write
Read one MCP client authorization.tokens.getMcpClient(id)GET /api/tokens/mcp-clients/:clientId
settings:read
Revoke an MCP client authorization.tokens.disconnectMcpClient(id)DELETE /api/tokens/mcp-clients/:clientId
settings:write

On this page