OAuth
How Kula Intelligence implements OAuth 2.1 for MCP clients — discovery, dynamic client registration, PKCE, scopes, and audience binding.
Last updated
Was this helpful?
How Kula Intelligence implements OAuth 2.1 for MCP clients — discovery, dynamic client registration, PKCE, scopes, and audience binding.
Kula Intelligence ships a standards-based OAuth 2.1 authorization server so MCP clients can sign a user in and receive an access token, with no token-pasting. It implements the discovery and security RFCs the MCP ecosystem relies on.
For end users, the flow is invisible — they click "connect" and sign in. This page is the detail for client developers.
Not every client does OAuth. Clients without a custom-connector flow (ChatGPT, Cursor, Lovable, the Claude API, custom agents) connect with a role-scoped access token minted in app.kula.digital instead — the same minted MCP credential, presented as a bearer header against
https://mcp.kula.digital/mcp. See Connect. This page covers the OAuth path used by Claude's custom connector.
Resource server (the MCP server)
mcp.kula.digital
the per-connector resource /connect/{…}/mcp, and its protected-resource metadata
Authorization server
app.kula.digital
discovery, registration, authorize, token; connectors are created here
Clients are pointed at a role-scoped connect link (https://mcp.kula.digital/connect/{…}/mcp) created in app.kula.digital — there is no open /mcp endpoint to connect to. The resource advertises the authorization server, so a compliant client only needs the connect link to discover everything else. For a connect link, the protected-resource metadata is path-scoped: /.well-known/oauth-protected-resource/connect/{…}/mcp.
Protected-resource metadata (RFC 9728):
GET https://mcp.kula.digital/.well-known/oauth-protected-resourceReturns the canonical resource URL and the authorization_servers list (pointing at app.kula.digital).
Authorization-server metadata (RFC 8414):
GET https://app.kula.digital/.well-known/oauth-authorization-serverReturns the authorization_endpoint, token_endpoint, registration_endpoint, code_challenge_methods_supported (["S256"]), grant_types_supported (authorization_code, refresh_token), and scopes_supported.
Clients may self-register:
Returns a client_id (and a secret only for confidential clients; public clients use token_endpoint_auth_method: "none" with PKCE).
For Claude's hosted clients, register the callback https://claude.ai/api/mcp/auth_callback. For Claude Code, loopback redirects on http://localhost:<port> are accepted with port-agnostic matching.
Authorize (GET /oauth/authorize) requires response_type=code, code_challenge, and code_challenge_method=S256 (PKCE is mandatory). The user is signed in (delegated to our identity provider), consents, and is redirected back with a code.
Token (POST /oauth/token) accepts application/x-www-form-urlencoded for the authorization_code grant (with code_verifier) and the refresh_token grant. Refresh tokens are rotated on use. Responses carry Cache-Control: no-store.
The access token is the same minted MCP token the runtime verifies — OAuth adds consent and discovery without introducing a new token type.
The OAuth scope maps to Kula's permission levels:
analytics
Aggregates only; no personal data
operations
Member/staff names visible; contact + payment details hidden
admin
Emails/phones visible; payment details hidden
full
Everything; every call recorded
offline_access
Issue a refresh token
Request the lowest scope the integration needs.
Tokens are bound to the canonical MCP resource URL (RFC 8707 resource indicator). The runtime verifies the token's audience, so a token minted for Kula can't be replayed against a different server.
Connectors are revocable from app.kula.digital and stop working within about a minute. Clients should handle a 401 by re-running the OAuth flow; if the connect link itself has been revoked or used up, the user creates a fresh connector.
Last updated
Was this helpful?
Was this helpful?
POST https://app.kula.digital/oauth/register
{ "redirect_uris": ["https://your-app.example/callback"], "client_name": "…" }