Elaichi MCP OAuth API
The MCP OAuth API from Elaichi — 5 operation(s) for mcp oauth.
The MCP OAuth API from Elaichi — 5 operation(s) for mcp oauth.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/elaichi-ai:elaichi-ai-mcp-oauth-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Elaichi MCP OAuth API
version: 0.1.0
description: 'Control-plane HTTP API for Elaichi, the MCP control plane for SaaS teams. Authenticate with an organization API token (create one in the Elaichi app under Settings → API tokens) sent as a bearer token, plus the `X-Organization-Id` header on org-scoped routes; the MCP and OAuth endpoints use their own protocols. A few actions are deliberately not available to an API token and must be done by a person in the Elaichi app: managing API and SCIM tokens, creating an organization, approving an access request, deleting an organization, and changing the two-factor requirement. The operations concerned say so. Endpoints used only by the Elaichi app itself (sign-in, consent screens, browser redirects, inbound webhooks) are not documented.'
servers:
- url: https://api.elaichi.ai
description: Production
tags:
- name: MCP OAuth
paths:
/oauth/register:
post:
operationId: registerOAuthClient
summary: Register an MCP client (RFC 7591)
tags:
- MCP OAuth
responses:
'201':
description: Client registered
content:
application/json:
schema:
type: object
properties:
client_id:
type: string
description: OAuth client id (`ocli_…`).
client_secret:
type: string
description: Present only for confidential clients. Returned exactly once — store it now.
client_id_issued_at:
type: integer
description: Unix seconds.
client_secret_expires_at:
type: integer
description: '`0` — the secret does not expire.'
client_name:
type: string
redirect_uris:
type: array
items:
type: string
grant_types:
type: array
items:
type: string
response_types:
type: array
items:
type: string
token_endpoint_auth_method:
type: string
client_uri:
type: string
logo_uri:
type: string
scope:
type: string
description: Space-delimited list of every scope this server supports.
'400':
description: 'RFC 6749 error body: `invalid_client_metadata` or `invalid_redirect_uri`.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited (`temporarily_unavailable`), with `Retry-After`.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
description: 'Dynamic client registration. Public and unauthenticated by protocol — any MCP client may register itself, which is why every field is length- and count-bounded and every redirect URI is scheme-checked. The response is the only time `client_secret` is ever returned; it is stored hashed and cannot be read back. Omitting `token_endpoint_auth_method` (or sending `none`) registers a public client that authenticates with PKCE alone, which is how every native MCP client works. Registration grants nothing on its own: a registered client can only act after a user approves it at `GET /oauth/authorize`. Rate limited per client IP.'
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- redirect_uris
properties:
client_name:
type: string
maxLength: 200
description: Shown on the consent screen. Defaults to "MCP client". Not verified — see `client_name` on the grant schema.
redirect_uris:
type: array
minItems: 1
maxItems: 10
items:
type: string
maxLength: 2048
description: 'Where authorization codes may be delivered, matched later by exact string. Accepted: any `https` URI; `http` only on loopback (`localhost`, `127.0.0.1`, `[::1]`), the RFC 8252 native-app pattern; or a private-use reverse-DNS scheme such as `com.example.app:/callback`. Fragments and wildcards are rejected.'
grant_types:
type: array
maxItems: 8
items:
type: string
enum:
- authorization_code
- refresh_token
description: Defaults to both. Any other value is rejected.
response_types:
type: array
maxItems: 8
items:
type: string
enum:
- code
description: Only `["code"]` is supported.
token_endpoint_auth_method:
type: string
enum:
- none
- client_secret_post
- client_secret_basic
description: Defaults to `none` (public client, PKCE only). Choosing either `client_secret_*` method issues a secret that the token and revocation endpoints will then require.
client_uri:
type: string
maxLength: 2048
logo_uri:
type: string
maxLength: 2048
description: 'Rendered on the consent screen beside the app name, `https:` only — an `http:`, `data:` or `blob:` value falls back to a generated monogram, as does one that fails to load. It is fetched with `referrer-policy: no-referrer` and without credentials. Note that it proves nothing: registration is unauthenticated, so this is a mark the registrant chose, exactly like `client_name`, and the consent screen says so to the user. `redirect_uris` remains the only identity signal a lookalike cannot forge.'
scope:
type: string
maxLength: 200
description: Accepted and ignored; scopes are chosen per authorization.
/oauth/authorize:
get:
operationId: startOAuthAuthorization
summary: Start an authorization (browser redirect)
tags:
- MCP OAuth
responses:
'302':
description: Redirect to the consent screen (or to login, then the consent screen), or back to the registered redirect URI carrying `error`/`error_description`/`state`.
'400':
description: 'Rendered error (never a redirect): unknown `client_id`, missing or unregistered `redirect_uri`.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited (`temporarily_unavailable`), with `Retry-After`.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
description: 'Validates the request, parks it server-side, and redirects the browser to Elaichi''s consent screen — this endpoint never renders HTML and never returns a code directly. PKCE is mandatory: `code_challenge` with `code_challenge_method=S256`; an absent method is rejected rather than defaulting to `plain`. Nothing about the request travels through the browser afterwards — the redirect carries only an opaque request id, so there is nothing for a user-agent to tamper with. If the user is not signed in they are sent to login first and returned to the same consent URL. The consent screen then calls `GET /oauth/authorize-request/{id}` and one of approve/deny. Errors before the redirect URI is validated are rendered as an RFC 6749 error body (an unregistered URI must never be redirected to, or the error path becomes an open redirect); errors after it are appended to the registered redirect URI as `error`/`error_description` with `state` echoed back. Rate limited per `client_id`.'
security: []
parameters:
- name: client_id
in: query
required: true
description: From `POST /oauth/register` (`ocli_…`).
schema:
type: string
- name: redirect_uri
in: query
required: true
description: Must exactly match one of the URIs registered for this client.
schema:
type: string
- name: response_type
in: query
required: true
description: Only `code` is supported.
schema:
type: string
enum:
- code
- name: code_challenge
in: query
required: true
description: 'PKCE challenge: base64url, 43–128 characters.'
schema:
type: string
minLength: 43
maxLength: 128
- name: code_challenge_method
in: query
required: true
description: Must be `S256`. `plain` and an absent value are both rejected.
schema:
type: string
enum:
- S256
- name: scope
in: query
required: false
description: 'MCP scopes, space-delimited on the wire. `mcp:read` reads org data; `mcp:write` creates and changes it; `mcp:destructive` deletes and removes access in Elaichi; `mcp:tools` runs the third-party tools the user has connected, deletes included. `openid` and `email` are identity scopes: they authorize `GET/POST /oauth/userinfo` and grant no MCP access on their own — a grant holding only `openid email` reaches no `mcp:*` operation and no connected tool. Requesting `mcp:write` or `mcp:destructive` implies `mcp:read` — a client that can mutate what it cannot read is not a useful capability. Unknown scope values are rejected rather than silently dropped. A request with no `scope` at all defaults to `mcp:read`.'
schema:
type: string
- name: state
in: query
required: false
description: Opaque client value, echoed on every redirect back.
schema:
type: string
- name: resource
in: query
required: false
description: RFC 8707 audience. Must identify this server by origin; a resource on another origin is an audience-confusion attempt and is rejected with `invalid_target`.
schema:
type: string
format: uri
/oauth/token:
post:
operationId: issueOAuthToken
summary: Exchange a code or refresh token for an access token
tags:
- MCP OAuth
responses:
'200':
description: Tokens issued
content:
application/json:
schema:
type: object
required:
- access_token
- token_type
- expires_in
- refresh_token
- scope
properties:
access_token:
type: string
description: Bearer token for `POST /mcp`. Short-lived; re-check `expires_in` rather than assuming.
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Access token lifetime in seconds.
refresh_token:
type: string
description: Single-use — rotated on every refresh.
scope:
type: string
description: Space-delimited granted scopes.
'400':
description: '`invalid_request`, `invalid_grant` (including replay, which also revoked the grant), `invalid_scope`, `invalid_target` or `unsupported_grant_type`.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: '`invalid_client` — unknown client, or client authentication failed.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited (`temporarily_unavailable`), with `Retry-After`.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
description: 'Accepts `application/x-www-form-urlencoded` (RFC 6749) or `application/json`. Public clients authenticate with PKCE alone; confidential clients MUST present their secret, by HTTP Basic or `client_secret` in the body.
`grant_type=authorization_code` requires `code`, `redirect_uri` and `code_verifier`. The redirect URI and the client must match those the code was issued to.
`grant_type=refresh_token` requires `refresh_token`, and rotates it — the presented token is consumed and a new one returned. An optional `scope` may narrow the grant, never widen it; the grant row is the authority, not the copy on the presented token.
Replay is treated as compromise, not as a plain error: presenting an authorization code or a refresh token twice REVOKES THE WHOLE GRANT (OAuth 2.1 §4.1.3 / §6.1), invalidating every token derived from it. The user must re-authorize from `GET /oauth/authorize`. Responses are `Cache-Control: no-store`. Rate limited per `client_id`.'
security: []
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- grant_type
properties:
grant_type:
type: string
enum:
- authorization_code
- refresh_token
client_id:
type: string
client_secret:
type: string
description: Confidential clients only; HTTP Basic is also accepted.
code:
type: string
description: authorization_code grant.
redirect_uri:
type: string
description: authorization_code grant; must match the code.
code_verifier:
type: string
description: authorization_code grant; PKCE verifier for the S256 challenge.
refresh_token:
type: string
description: refresh_token grant.
scope:
type: string
description: refresh_token grant; may only narrow the grant.
resource:
type: string
description: RFC 8707 audience; must identify this server.
application/json:
schema:
type: object
additionalProperties:
type: string
/oauth/revoke:
post:
operationId: revokeOAuthToken
summary: Revoke a token (RFC 7009)
tags:
- MCP OAuth
responses:
'200':
description: Always returned on a well-formed, client-authenticated request. Body is `{}`.
content:
application/json:
schema:
type: object
'400':
description: '`invalid_request` — `token` is missing.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'401':
description: '`invalid_client` — client authentication failed.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited (`temporarily_unavailable`), with `Retry-After`.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
description: Revoking either an access token or a refresh token kills the ENTIRE grant it belongs to, not just the token presented — a client the user disconnected must not keep a live refresh token. Accepts form-encoded or JSON and authenticates the client exactly as `POST /oauth/token` does. Always answers 200, including for unknown, already-revoked, or another client's token, so this endpoint cannot be used as a token oracle. To revoke from the user side instead, use `DELETE /oauth/grant/{id}`.
security: []
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- token
properties:
token:
type: string
description: An access token or a refresh token.
token_type_hint:
type: string
description: Accepted and ignored; both types are looked up.
client_id:
type: string
client_secret:
type: string
application/json:
schema:
type: object
additionalProperties:
type: string
/oauth/userinfo:
get:
operationId: getUserinfo
summary: Get user info (OIDC)
tags:
- MCP OAuth
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
required:
- sub
properties:
sub:
type: string
description: 'Stable, opaque subject identifier (`usr_…`). Same value for every client — `subject_types_supported: ["public"]`.'
email:
type: string
format: email
description: Present only when the grant holds the `email` scope.
email_verified:
type: boolean
description: Present only alongside `email`. Derived from how the account authenticates (a consumed email code, a signed SSO assertion over a DNS-verified org domain, an identity provider that publishes and enforces its own verification signal) — never a self-reported flag.
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No credential supplied → `invalid_request`, no `error` parameter on the challenge (RFC 6750 §3.1: a request carrying no credential has failed nothing yet). A bad, expired or revoked credential → `invalid_token`, with `error="invalid_token"` on the challenge.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
headers:
WWW-Authenticate:
description: RFC 6750 §3 challenge, `error`/`error_description` present only when a credential was supplied.
schema:
type: string
'403':
description: A valid token whose grant does not hold the `openid` scope. `insufficient_scope` rather than 401 — re-authenticating would not help, the client must send the user back through consent for more.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
headers:
WWW-Authenticate:
description: Carries `error="insufficient_scope"` and `scope="openid"`.
schema:
type: string
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: 'Returns claims about the user who authorized the grant behind the bearer token. Bearer-authenticated by the same `verifyMcpAccessToken` function `POST /mcp` uses, so a revoked grant, a deactivated user and an expired token are all refused exactly as they are there. Never cached: this is the one response in this router that can carry a person''s address.
`sub` is always present — the stable `usr_…` primary key, never reassigned and never pairwise. `email` and `email_verified` are returned together, and only when the grant holds the `email` scope; a client that asked only for `openid` gets a subject and no contact detail. The two never appear separately, since an `email` without the flag beside it would invite a relying party to assume verification.'
security:
- bearerAuth: []
post:
operationId: postUserinfo
summary: Get user info (OIDC)
tags:
- MCP OAuth
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
required:
- sub
properties:
sub:
type: string
description: 'Stable, opaque subject identifier (`usr_…`). Same value for every client — `subject_types_supported: ["public"]`.'
email:
type: string
format: email
description: Present only when the grant holds the `email` scope.
email_verified:
type: boolean
description: Present only alongside `email`. Derived from how the account authenticates (a consumed email code, a signed SSO assertion over a DNS-verified org domain, an identity provider that publishes and enforces its own verification signal) — never a self-reported flag.
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'No credential supplied → `invalid_request`, no `error` parameter on the challenge (RFC 6750 §3.1: a request carrying no credential has failed nothing yet). A bad, expired or revoked credential → `invalid_token`, with `error="invalid_token"` on the challenge.'
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
headers:
WWW-Authenticate:
description: RFC 6750 §3 challenge, `error`/`error_description` present only when a credential was supplied.
schema:
type: string
'403':
description: A valid token whose grant does not hold the `openid` scope. `insufficient_scope` rather than 401 — re-authenticating would not help, the client must send the user back through consent for more.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
headers:
WWW-Authenticate:
description: Carries `error="insufficient_scope"` and `scope="openid"`.
schema:
type: string
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limited
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: 'Returns claims about the user who authorized the grant behind the bearer token. Bearer-authenticated by the same `verifyMcpAccessToken` function `POST /mcp` uses, so a revoked grant, a deactivated user and an expired token are all refused exactly as they are there. Never cached: this is the one response in this router that can carry a person''s address.
`sub` is always present — the stable `usr_…` primary key, never reassigned and never pairwise. `email` and `email_verified` are returned together, and only when the grant holds the `email` scope; a client that asked only for `openid` gets a subject and no contact detail. The two never appear separately, since an `email` without the flag beside it would invite a relying party to assume verification.'
security:
- bearerAuth: []
components:
schemas:
Error:
type: object
required:
- error
description: The Elaichi error envelope. The MCP OAuth endpoints under `/oauth` are the exception — they return RFC 6749 errors instead (see `OAuthError`).
properties:
error:
type: object
required:
- message
- code
properties:
message:
type: string
description: Human-readable. 5xx messages are deliberately generic.
code:
type: string
description: Stable machine-readable code, e.g. `not_found`, `subscription_required`.
details:
description: Structured detail for codes that carry one, e.g. `toolboxes_need_resolution`.
feature:
type: string
description: The gated feature, on entitlement failures.
plan:
type:
- string
- 'null'
enum:
- personal
- gold
- black
- null
additionalProperties: true
OAuthError:
type: object
required:
- error
properties:
error:
type: string
description: RFC 6749 / 7591 / 8707 error code, e.g. `invalid_grant`, `invalid_client`, `invalid_redirect_uri`.
error_description:
type: string
additionalProperties: true
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: session or API token
description: '`Authorization: Bearer <token>` with an org-scoped API token (`elch_{org_id}_{64 hex}`), created in the Elaichi app under Settings → API tokens. The Elaichi app itself signs in with a session cookie, which is also accepted but is not meant for integrations. Two other credential families exist and are NOT interchangeable with this one: an MCP OAuth access token (`POST /mcp` only) and a SCIM token (`/scim/v2/*` only).'
mcpOAuth:
type: oauth2
description: MCP OAuth access token, used only by `POST /mcp` — not interchangeable with `bearerAuth`. Discovered per RFC 9728/8414 at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`.
flows:
authorizationCode:
authorizationUrl: https://api.elaichi.ai/oauth/authorize
tokenUrl: https://api.elaichi.ai/oauth/token
scopes:
mcp:read: Anything you can already see in Elaichi. Never secret values. It can also ask an admin for access to something restricted for you.
mcp:write: Create and change teams, roles, toolboxes, templates and connections, and share access.
mcp:destructive: Permanently delete data and remove access in Elaichi. This cannot be undone.
mcp:tools: Run tools from the toolboxes you choose next, deleting in your connected apps included. It can also ask an admin for access to something restricted for you.
openid: That you are signed in, and the user ID we use for you. Nothing else.
email: Your email address, and whether it is verified. Apps use this to check your domain.