API REFERENCE
The MCP OAuth API
Per-user MCP OAuth. When a tool server needs the end user's own account, these endpoints list what can be connected, start the consent flow, and revoke it later.
Some MCP servers act on behalf of the individual user, not the project. For those you use per-user OAuth: the end user connects their own account from inside the widget, Agentifys runs an OAuth 2.1 authorization_code flow with PKCE, and the resulting token is stored per user and injected only into that user's tool calls. These endpoints drive that flow. The widget's ERA.connectMcp(serverId) helper calls them for you, but you can build your own UI.
403 SIGNED_IDENTITY_REQUIRED; if the project is in signed mode but the context is unsigned, tampered with, or older than 300 seconds they return 401 IDENTITY_SIGNATURE_REQUIRED, IDENTITY_SIGNATURE_INVALID, or IDENTITY_SIGNATURE_EXPIRED. Shared connections (admin connects once for the whole project) are configured in Settings and do not use these routes.allowed_origins configured, a request whose Origin header is missing or not on the list is rejected with 403 ORIGIN_NOT_ALLOWED — including a plain server-side call. The curl examples below work against a project with an empty allowlist; from your own backend, send a matching Origin header.List connectable servers
/v1/oauth/mcp/serversReturns the project's MCP servers that are active and configured with auth_type: "oauth" and oauth_mode: "per_user", with whether the current user has already connected each one. Shared-OAuth servers, bearer servers and paused servers are not listed. Use it to render connect and disconnect buttons.
The signed identity travels in the X-Era-User header as URL-encoded JSON. A raw JSON header is also accepted, but encode it: a header carrying non-Latin-1 characters — an Arabic name, for instance — is rejected by browsers before it leaves the page.
curl https://agentifys.ai/v1/oauth/mcp/servers \ -H "Authorization: Bearer era_your_project_key" \ -H "X-Era-User: %7B...signed...%7D"
{
"servers": [
{ "server_id": "github", "label": "GitHub", "connected": false },
{ "server_id": "notion", "label": "Notion", "connected": true }
]
}| Field | Type | Description |
|---|---|---|
server_id | string | Stable id you pass to start and revoke. |
label | string | Human label for the server, falling back to its URL. |
connected | boolean | Whether this user already has a valid connection. |
Start a connection
/v1/oauth/mcp/startBegins the OAuth flow for one server and one user. Agentifys generates the PKCE challenge and state, then returns an authorize_url. Send the user there (a popup or a redirect). When they approve, the provider calls Agentifys's callback, the token is exchanged and stored, and that user's future chats can call the server's tools.
| Field | Type | Description |
|---|---|---|
user_contextrequired | object | The signed identity of the connecting user. Ties the token to this user. |
server_idrequired | string | Which server to connect, from the servers list. |
curl -X POST https://agentifys.ai/v1/oauth/mcp/start \
-H "Authorization: Bearer era_your_project_key" \
-H "Content-Type: application/json" \
-d '{
"server_id": "github",
"user_context": { "id": "u_42", "_ts": 1735689600, "_sig": "a1b2c3..." }
}'{ "authorize_url": "https://github.com/login/oauth/authorize?client_id=...&code_challenge=...&state=..." }| Field | Type | Description |
|---|---|---|
NOT_FOUND | 404 | No server with that id is configured for per-user OAuth on this project. |
NOT_CONFIGURED | 400 | The server is per-user OAuth but an admin has not finished the client setup (no authorization endpoint or client id yet). |
OAUTH_UNAVAILABLE | 400 | The deployment has no PUBLIC_BASE_URL, so there is no redirect URI to send the user back to. |
RATE_LIMIT_EXCEEDED | 429 | More than 10 starts a minute or 60 a day for this user on this project. Carries a Retry-After header. |
connected: true.authorize_url is single use and expires 300 seconds after it is minted. Its state is an encrypted blob carrying the PKCE verifier and the binding ids for this one attempt, and the callback burns it. Do not cache it, and mint a fresh one per connection attempt.Revoke a connection
/v1/oauth/mcp/connection/{server_id}Disconnects the current user from one server and deletes their stored token. The server id goes in the path and the signed identity is passed as a user_context query param. If the provider published a revocation endpoint, Agentifys calls it first on a best-effort basis; the local token is deleted either way. Their chats can no longer call that server's tools until they connect again, and the tools return TOOL_DISABLED with “Connect your account to use this tool.” in the meantime.
user_context sits in the request line here, so it is written to every access log between the caller and Agentifys. Sign the smallest identity that works for this call. If your standard identity payload carries an auth_token, a live bearer token ends up in those logs.curl -X DELETE "https://agentifys.ai/v1/oauth/mcp/connection/github?user_context=%7B%22id%22%3A%22u_42%22%2C%22_ts%22%3A1735689600%2C%22_sig%22%3A%22...%22%7D" \ -H "Authorization: Bearer era_your_project_key"
{ "ok": true }The call is idempotent: { "ok": true } comes back whether or not the user had a connection to delete.