Connect an agent
There are two transports and one tool contract. Remote is a route on the server; local is a subcommand of the CLI. Both expose the same fifteen tools — see the tool reference.
Remote, over HTTP
Section titled “Remote, over HTTP”claude mcp add --transport http ota https://your-server/mcpA browser opens, you sign in, and the client is connected. Nothing is installed and no token is pasted. Any MCP client that speaks Streamable HTTP and OAuth connects the same way.
The endpoint is available on every install, self-hosted or hosted. It requires
PUBLIC_URL to be the address clients actually type, because the discovery
documents are built from it.
What happens
Section titled “What happens”- The client
POSTs to/mcpwith no credentials. The server answers 401 with aWWW-Authenticate: Bearer resource_metadata="…"header pointing at/.well-known/oauth-protected-resource. That pointer is what makes a client start the flow by itself instead of just failing. - The client reads that document — it names the resource, the authorization
server, and the
adminandreadscopes — then/.well-known/oauth-authorization-serverfor the endpoints. - Dynamic client registration:
POST /oauth/registerwith a client name and its redirect URIs. The server returns aclient_id, no secret, andtoken_endpoint_auth_method: "none". A CLI or desktop agent cannot keep a secret, so PKCE is the proof of possession instead. - PKCE, mandatory:
GET /oauth/authorizerequirescode_challenge_method=S256and a base64url digest of 43 to 128 characters. Anything else is rejected. Theredirect_urimust match one registered for that client exactly — never a prefix or an origin. - The browser shows a sign-in page. There is no session to reuse, because this server authenticates with Bearer tokens only, so consent always asks for the password. A wrong password re-renders the page rather than bouncing an OAuth error at the client.
- On success the server issues an authorization code, valid 120 seconds, and redirects.
POST /oauth/tokenexchanges it. The code is burned before anything else is validated, so it is single-use whether or not the exchange succeeds; then the client id, the redirect URI and thecode_verifierare checked.- The response carries an access token valid 30 days and a refresh token. Refreshing rotates: the old access token is deleted along with the refresh token that minted it.
Access tokens are ordinary api_tokens rows with kind: "oauth". One token
system, one revocation path — revoking in settings kills an agent’s access the
same way it kills a CI token.
Without OAuth
Section titled “Without OAuth”Clients that do not implement the flow can send the header directly:
claude mcp add --transport http ota https://your-server/mcp \ --header "Authorization: Bearer ota_..."Create that token in the dashboard under the project’s settings. A token scoped
to a single project also removes the need to pass projectId to every tool.
Notes on the endpoint
Section titled “Notes on the endpoint”/mcp accepts POST only; anything else answers 405 with a JSON-RPC error,
because nothing here streams and there is no session to delete. A server and
transport are built per request and torn down after it, which is the only shape
that works on an edge runtime where the next request may land in a different
isolate.
Local, over stdio
Section titled “Local, over stdio”ota mcpThe CLI runs the same tools against whatever API it is configured for, using
the credentials from ota login, or from the environment:
{ "mcpServers": { "ota": { "command": "npx", "args": ["@open-ota/cli", "mcp"], "env": { "OTA_API_URL": "https://ota.example.com", "OTA_TOKEN": "ota_..." } } }}--project <id> and -c <channel> set the defaults for tools that omit them;
without either, they come from ota.config.json in the working directory.
Nothing may write to stdout — that is the JSON-RPC channel — so the CLI’s own
messages go to stderr.
Use stdio when you do not want to expose /mcp, when you are offline or in CI,
and when you want publish_release to build from a local directory.
One surface, not two
Section titled “One surface, not two”The contract — tool name, description and argument schema — lives once in
packages/shared/src/mcp.ts. Neither transport declares its own; each binds
handlers to the shared shapes, the remote route against the service layer and
the CLI against the API client.
apps/server/test/mcp-contract.test.ts fails if they drift. Writing that test
is what surfaced real drift between the two implementations: different
descriptions for the same tool and, worse, incompatible arguments — an agent
connected over stdio passed releaseId where one connected over HTTP passed
projectId plus release.
A release is named the way a person says it: v42 works anywhere a uuid does,
with platform and channel disambiguating when a label repeats.