Add Busabase to your product's third-party integrations catalog — OAuth 2.1 discovery, dynamic client registration, multi-tenant space targeting, and the approval-first tool contract, using Buda's own integration as the reference implementation.
This page is for a different reader than MCP + OAuth. That page tells your users how to point their own agent at Busabase. This page tells you, a platform engineer, how to add a "Connect Busabase" button to your own product so any of your users can link their Busabase workspace — the way Buda's Settings → Integrations does it today.
Buda is not special-cased on the Busabase side. It is one row in an integrations catalog next to Linear, Notion, Slack, Supabase, and 15+ other MCP providers, registered through the same OAuth 2.1 metadata this page documents. Everything here is what Buda's engineers had to learn to add that row — read this instead of re-deriving it from the OAuth spec.
A platform integration is three pieces wired together:
A provider manifest in your catalog — an authorization URL, a token URL, an MCP server URL, and either a static client id or dynamic registration.
An OAuth 2.1 + PKCE client — your platform is the client; Busabase is the authorization server and the resource server.
A generic MCP bridge — once you hold a token, you talk to Busabase exactly like any other MCP server. Nothing about tool-calling is Busabase-specific.
Buda's implementation is almost entirely generic code shared across every MCP provider it supports. Only three things are Busabase-specific, and they are small:
Confirms the token response's issuer origin equals the MCP server's origin before trusting the token
Connect-time tool listing
same file
Calls listTools() once, immediately after token exchange, so a broken connection fails at connect-time instead of at first real use
Embed-link UI affordance
agent/hooks/use-busabase-embed-link-tool.ts
Pure frontend: when the generic tool bridge returns an embed_links_create result, render it as a preview panel instead of raw JSON
Everything else — the manifest schema, the OAuth session, PKCE, token storage, refresh, and the tool-calling bridge — is provider-agnostic. If you are building your own integrations catalog, budget your Busabase-specific code at roughly that size.
You need a client_id before you can start the OAuth flow. Busabase supports both of the standard registration paths, and they are not tiered by capability — a dynamically-registered client can do everything a static one can.
This is RFC 7591 Dynamic Client Registration — the same endpoint MCP-aware clients like Cursor and Claude Code call automatically the first time they connect. No account or approval step is required. Rules enforced server-side (packages/share-domains/auth/oauth/oauth21.ts):
1–10 redirect_uris, each either https://… or an http://localhost / 127.0.0.1 / ::1 loopback — no embedded userinfo, no fragment, max 2048 chars.
token_endpoint_auth_method must be omitted or "none" — Busabase only issues public clients. There is no client secret to protect, so there is none to leak.
If grant_types is present it must include authorization_code and contain nothing outside {authorization_code, refresh_token}; if response_types is present it must be exactly ["code"].
client_id_expires_at is one year out; re-register before it lapses. Registration is rate-limited to 20 requests/hour per IP — call it once at deploy time and persist the returned client_id, don't re-register per user or per session.
A handful of clients — Busabase's own native app, mobile app, CLI, AirApp runtime, and Buda (buda-mcp) — are hardcoded in apps/busabase-cloud/src/domains/auth/logic/oauth-clients.ts with a fixed redirect_uris allowlist controlled by the Busabase team, not by self-service registration. This buys a stable, typo-proof client_id and a curated display name in Busabase's own consent screen, at the cost of needing a code change on the Busabase side. Unless you specifically want that pinned treatment, use dynamic registration — it produces a fully-functional client with no waiting.
Don't hardcode /api/oauth/authorize and friends. Busabase publishes standard OAuth discovery documents so a well-behaved client resolves them at runtime — this is what lets Cursor, Claude Code, and Codex all work against the same server without per-client server-side config:
GET https://busabase.com/.well-known/oauth-protected-resource/api/mcp
This is RFC 8414 (authorization server metadata) and RFC 9728 (protected resource metadata). Two things worth designing around if you build your own MCP server later: Busabase deliberately omits authorization_response_iss_parameter_supported (a known Codex 0.145 bug drops the iss query parameter before its OAuth library validates it — advertising the capability broke that client), and the /api/mcp-suffixed protected-resource URL is a duplicate of the top-level one, kept only because some clients look up metadata by appending the resource path instead of using the value returned in a 401's WWW-Authenticate header.
Standard authorization-code flow with mandatory PKCE (S256) and no client secret:
Generate PKCE. Create a random code_verifier, derive code_challenge = BASE64URL(SHA256(code_verifier)), and generate a random state.
Redirect the user to /api/oauth/authorize with client_id, redirect_uri, response_type=code, state, code_challenge, code_challenge_method=S256, resource=https://busabase.com/api/mcp, and scope=mcp. Busabase renders its own sign-in and consent screen — your platform never sees the user's Busabase credentials.
Receive the callback at your redirect_uri with code and state. Verify state matches what you generated (Busabase's own authorization session is single-use and expires quickly; add a countdown story to your UX for OAuth popups).
Exchange the code at /api/oauth/token:
POST https://busabase.com/api/oauth/tokenContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=<CODE>&redirect_uri=<REDIRECT_URI>&client_id=<CLIENT_ID>&code_verifier=<VERIFIER>&resource=https://busabase.com/api/mcp
The response includes access_token (prefixed bso_), refresh_token, expires_in, scope, and issuer.
Verify the issuer before trusting the token. Confirm new URL(response.issuer).origin equals the origin of the mcpServerUrl you connected to. This defends against an authorization-server-confusion attack where a token minted by a different server is replayed against yours. Buda enforces this as a hard failure (integration-service.ts, "OAuth authorization server mismatch") — copy the check verbatim into your own callback handler; it is the one step every generic OAuth library skips because it doesn't know your resource URL.
Store the token pair encrypted, associated with the user and the Busabase account, not just the user — a person can hold more than one Busabase account.
Refresh proactively.grant_type=refresh_token at the same token endpoint. Busabase rotates the refresh token on every use and tracks a token family; reusing an already-consumed refresh token revokes the entire family server-side (replay-detection, not just expiry). Treat a refresh failure as "the grant was revoked," not as a retryable network error.
Validate the connection before you report success
Immediately after exchanging the code, call tools/list once with the new access token. A token that exchanges successfully but can't list tools (wrong scope, wrong resource, revoked mid-flow) should fail the connection attempt in your UI rather than silently save a dead credential the user discovers is broken days later.
Once you hold a valid access token, talk to https://busabase.com/api/mcp as a standard MCP Streamable HTTP server with Authorization: Bearer <access_token>. Do not write Busabase-specific tool-calling code — build (or reuse) one generic MCP client in your platform and point it at every provider's mcpServerUrl, the same way Buda's agent only ever calls two generic tools, list_integration_tools and call_integration_tool, never a Busabase-named tool. Two habits worth copying from Buda's bridge:
Re-validate the tool name against a live tools/list before every call, not just at connect time. This closes a prompt-injection path where content read from a document tries to make the agent invoke a tool that was never actually offered.
Surface tool annotations (readOnlyHint, destructiveHint) in your own UI/consent layer if you build one — Busabase tags every tool it publishes so a generic client can distinguish a safe read from a destructive write without knowing Busabase's domain model.
On Busabase's side, both an OAuth bearer token and a plain API key (Authorization: Bearer <API_KEY>, no bso_ prefix) resolve to the same thing: a specific user's API key, used to call Busabase's own OpenAPI surface on the tool's behalf. An OAuth grant is, structurally, a scoped, revocable, browser-issued wrapper around an API key — this is why the permission levels on the consent screen (read / changeRequest / write / manage, see MCP + OAuth) are the same ones available to a manually-created API key.
A Busabase account can belong to more than one space (workspace). A platform that assumes "one user → one space" will work in the demo and break for the first real customer with two workspaces. Copy this pattern:
Call the auth_verify tool right after connecting. It returns the current user, the current space, and every space the credential can see.
If more than one space comes back, ask your user which one this integration should target — don't guess, and don't silently default to the first result.
Pass the chosen id as targetSpaceId on every subsequent space-scoped tool call. Busabase maps it to an x-busabase-space request header internally.
If you skip step 2 and a credential resolves to more than one space, Busabase's REST layer rejects the ambiguous call outright (400, with the candidate space list in the error body) instead of picking one for you. Design your integration to surface that error to the user, not to retry blindly.
If your own platform brokers access to its own multi-tenant resources for third-party agents, this reject-on-ambiguity design is worth adopting directly: a wrong silent default is a data-leak-shaped bug, a loud rejection with the candidate list is not.
Busabase's MCP catalog is deliberately smaller than its full API. Tools are excluded for two different reasons, and the distinction matters if you are deciding what your own platform should expose to an autonomous agent:
No agent-safe substitute exists yet — e.g. the stateful dump/restore session tools (dump_export_tables, dump_import_begin, …). An agent has no legitimate reason to move raw rows around, and a half-driven import/export session left open is a real failure mode. busabase-cli keeps all of these; the MCP catalog does not.
The data itself is unsafe to hand to an agent — Vault tools (vault_get, vault_update, vault_clear) return decrypted secret values. Publishing vault_get as an agent tool means every credential in the workspace is one prompt-injected message away from being read back to an attacker.
Everything else Busabase exposes over MCP is meant to be called freely, subject to the operating rule Busabase asks every connected agent to follow: read before proposing, prefer a Change Request over a direct write, and never merge/close a Change Request unless the human explicitly asked for that decision in this turn. If your platform lets an agent act on a user's behalf across many third-party tools, treat "propose, don't commit" as the default posture for any tool a provider marks destructive, and reserve auto-execution for tools a provider marks read-only.
This is the complete provider manifest Buda registers for Busabase (apps/buda/src/domains/integrations/logic/provider-catalog.ts), included here as a concrete example of the minimum shape a catalog entry needs:
A platform using dynamic registration instead would omit staticClientId, set registrationMode: "dynamic", add a registrationUrl: "https://busabase.com/api/oauth/register", and persist the client_id returned the first time registration runs rather than hardcoding one.
Busabase's own test suite pins the exact shape of these endpoints so a protocol regression fails CI instead of silently breaking every connected client — apps/busabase-cloud/tests/mcp-oauth-metadata.test.ts snapshots the two well-known documents, and apps/busabase-cloud/tests/oauth-dynamic-registration.test.ts runs a full register → authorize → consent → token-exchange flow end to end, including the RFC 7591 error shape ({"error": "invalid_client_metadata"}) for a malformed registration. Write the equivalent test on your side against a real Busabase Cloud (or self-hosted) instance, not a mock — an OAuth integration that only passes against a hand-rolled stub has not verified the one thing that actually breaks: the two ends disagreeing about the protocol.