End-user tools: OAuth model

End-user tools are the ones each of your visitors connects with their own account, and every one of them is per-user OAuth. Where the provider supports dynamic client registration, Command+K registers an OAuth client for you and you supply nothing — that is Linear, Notion, Atlassian, ClickUp, monday.com, Sentry, Stripe and Miro, each verified by actually registering against them. Where it doesn't — GitHub and Intercom — you register your own OAuth app and paste its client ID and secret. A third mode exists, a platform-owned OAuth app shared across workspaces, but no such app is configured in production, so it is not a path you can pick right now. This page is the exact per-provider truth: which mode applies, which callback URL to register, where the tokens live, and who can revoke them.

Read this first: the shared platform OAuth app is not available

Command+K supports a platform-owned OAuth app (the shared mode below) where visitors would see "Command+K" on the consent screen and nobody registers anything. The mechanism is implemented, but no shared OAuth app is configured in production, so that mode cannot be selected today. What works today is dynamic client registration (eight of the ten providers) and bring-your-own credentials (all ten, and the only option for GitHub and Intercom).

What an end-user tool is

The Tool library has two tabs, and they behave completely differently. Assistant tools are connected once by your workspace and used by the assistant on everyone's behalf — one credential, one account. Your users' tools are connected by each visitor with their own account, so the assistant acts as that person upstream. This page is only about the second kind.

  • A verified identity is mandatory. The connect endpoints refuse anonymous visitors. Your widget must be in a signed-in mode and the visitor must carry a widget JWT you signed — see End-user authentication (JWT). On a public widget the "Your connections" entry point does not render at all.
  • One connection per (workspace, user, tool). Reconnecting replaces the stored token rather than adding a second row.
  • Connect attempts are rate limited to 5 per end user per hour, per workspace.

The three OAuth client modes

Every end-user tool row carries a client mode. It answers one question: where does the OAuth client_id and client_secret come from?

  • dcr — dynamic client registration (RFC 7591). Nobody registers an OAuth app. Command+K discovers the provider's registration_endpoint, then registers a client bound to our exact callback URL on the first visitor's connect and caches the resulting client ID. Requires the provider to publish that endpoint in its authorization-server metadata, and to accept our callback URL. Verified today for Linear, Notion, Atlassian, ClickUp, monday.com, Sentry, Stripe and Miro — we registered a real client with each of them from production code.
  • byoc — bring your own credentials. You register an OAuth app with the provider, set its redirect URI to our end-user callback, and paste the client ID and secret. The secret is encrypted before it is stored. Your users see your app's name on the provider's consent screen, which is the right answer for white-label. Available today for all ten providers, and the only option for GitHub and Intercom.
  • shared — a platform-owned OAuth app. Credentials are read from Command+K's own server environment and nothing is stored per workspace; visitors would see "Command+K" on the provider's consent screen. A preset is registered for all ten marketplace providers, but the credentials themselves are not set on this deployment, so selecting the mode fails with a clear "no managed app is configured" error. Not available today for any provider.

Which mode a tile picks for you

Enabling a marketplace tile resolves the mode in this order: dcr if the provider is known to accept our registration, then shared if this deployment has a managed app for it, then bring-your-own. You can always override and paste your own credentials. The same resolution happens when you add a custom end-user server by URL: OAuth discovery either sets it up as dcr, or pre-fills the authorize and token URLs so you only have to add credentials. A workspace holds one entry per server URL, so a provider is on one path at a time.

Per-provider table

The end-user marketplace contains exactly ten providers. Anything not in this table is not connectable per end user today.

ProviderMode availableWhat we measured (2026-09-10)Scopes requestedCallback URL to registerPlan gatingToken storageEnd-user revoke
Linear
https://mcp.linear.app/mcp
dcr (preferred) · byo
Zero setup. Linear's authorization server publishes an RFC 7591 registration endpoint, so enabling the tile registers a client automatically and you paste nothing. Bring your own OAuth app instead if you want your name on the consent screen.
Registered for real against https://mcp.linear.app/register with our production callback: client_id issued, public PKCE client (token_endpoint_auth_method: none). The authorize URL built with that client_id renders Linear's real login page. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.read, writehttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Notion
https://mcp.notion.com/mcp
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials.
Registered for real against https://mcp.notion.com/register with our production callback: public PKCE client issued. The authorize URL redirects into Notion's real install-integration flow. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.none sent — Notion's server advertises a single default scope and the catalog requests no explicit scopeshttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
GitHub
https://api.githubcopilot.com/mcp/
byo only
You must create a GitHub OAuth app and paste its client ID and secret. There is no credential-free path for GitHub today.
GitHub's authorization server publishes no registration endpoint at all, so dynamic registration is impossible — not a configuration we can change.repo, read:userhttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Atlassian
https://mcp.atlassian.com/v1/sse
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials.
Registered for real against https://mcp.atlassian.com/v1/register with our production callback: public PKCE client issued. The authorize URL redirects to id.atlassian.com login. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.byo path: read:jira-work, read:confluence-content.summary, offline_access. dcr path: none sent — Atlassian's metadata advertises no scope list, and we only request scopes the server declares.https://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Intercom
https://mcp.intercom.com/sse
byo only
Intercom advertises a registration endpoint, but registration fails for us, so you must create an Intercom OAuth app and paste its client ID and secret — and ask Intercom support to allowlist our redirect URI first.
Registration attempted for real and REJECTED: HTTP 400 invalid_redirect_uri — "Redirect URI https://commandplusk.com/api/public/widget/mcp/callback is not in the allowlist, reach out to Intercom Customer Support if you believe we should support it." Intercom keeps a redirect-URI allowlist, so dynamic registration stays unavailable until Intercom adds ours.none sent — Intercom's server advertises no scope listhttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
ClickUp
https://mcp.clickup.com/mcp
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials.
Registered for real against https://mcp.clickup.com/oauth/register with our production callback: public PKCE client issued (token_endpoint_auth_method: none). The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.read, writehttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
monday.com
https://mcp.monday.com/mcp
dcr (preferred) · byo
Zero setup, with one difference from the rest: monday.com's authorization server does not accept public clients, so the client it issues carries a secret. Command+K encrypts that secret and uses it at the token endpoint automatically — you still paste nothing.
Registered for real against https://auth.monday.com/oauth_ms/oauth/register with our production callback: client ID and client secret issued (confidential client). The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.me:read, boards:read, items:read, docs:read, updates:readhttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Sentry
https://mcp.sentry.dev/mcp
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials.
Registered for real against https://mcp.sentry.dev/oauth/register with our production callback: public PKCE client issued. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.org:readhttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Stripe
https://mcp.stripe.com
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials. Bear in mind what the visitor is connecting: their Stripe account. New bindings are read-only by default and should stay that way unless you have a reason.
Registered for real against https://access.stripe.com/mcp/oauth2/register with our production callback: public PKCE client issued, scope mcp. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.mcphttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.
Miro
https://mcp.miro.com/mcp
dcr (preferred) · byo
Zero setup. Registration endpoint published, so the tile self-configures with no credentials.
Registered for real against https://mcp.miro.com/register with our production callback: public PKCE client issued. The code-for-token exchange is not proven — it needs a real human consent and has never been run end to end.boards:readhttps://commandplusk.com/api/public/widget/mcp/callbackNone. End-user tools don't consume the MCP-connections plan cap. Gates: owner/admin role, a workspace not in read-only mode, a signed-in (non-anonymous) visitor, 5 connect attempts per user per hour.end_user_mcp_connections, one row per (workspace, user, tool). Access + refresh tokens AES-256-GCM encrypted; service-role only under RLS.Widget header → Your connections → Disconnect. Deletes the stored token; does not revoke the grant at the provider.

The scope column is what Command+K actually sends on the authorize request. On the byo and shared paths that is the catalog's scope list. On the dcr path we intersect the catalog list with the scopes the provider's metadata declares, so a provider that declares no scope list gets no scope parameter and its own default grant applies. If you need a scope that is not sent, add the server as a custom end-user tool and set the scopes yourself.

What was measured, and what is still unproven

On 2026-09-10 we ran real RFC 7591 registration against the four DCR candidates, from our own production code path, with the redirect URI https://commandplusk.com/api/public/widget/mcp/callback. Linear, Notion and Atlassian each issued a client ID as a public PKCE client (token_endpoint_auth_method: none), and the authorize URL built with that client ID reached the provider's own real login or install screen. Intercom refused with 400 invalid_redirect_uri because it keeps a redirect-URI allowlist. GitHub publishes no registration endpoint to try. On 2026-09-11 we repeated the same real registration against a further set of candidates: ClickUp, monday.com, Sentry, Stripe and Miro each issued a client ID and were added to the marketplace, while Asana, Figma, Vercel, Dropbox, Shortcut, Cal.com and Square all refused our callback URL and are therefore not offered. What we have not proven is the last step: exchanging the authorization code for an access token — that needs a real person to click Allow, and we have not run it end to end. Treat the token exchange as implemented but unverified, and tell us if you hit a wall on it.

Slack is not an end-user tool

Slack is available on Command+K only as an assistant-level connector: your workspace connects one Slack app, and the assistant uses it for every visitor. A visitor cannot link their own Slack account from the widget. If you saw Slack listed as a per-user marketplace tool in an older version of these docs, that was wrong. Slack also requires your own Slack app — and it registers the assistant-level callback URL, not the end-user one. The limit is Slack's: only apps published in the Slack Marketplace, or apps internal to a single workspace, are permitted to use Slack's MCP server, and Slack does not offer dynamic client registration for it. An app that installed into each of your customers' own Slack workspaces would have to pass Marketplace review first.

The two callback URLs

Command+K has two separate OAuth callbacks, one per audience. They are not interchangeable — registering the wrong one gives you a redirect_uri mismatch at the provider, usually at the very last step of an otherwise successful flow.

  • End-user (widget visitor) connections — the ten providers in the table above, and any custom end-user tool.
    https://commandplusk.com/api/public/widget/mcp/callback
  • Assistant-level (workspace-owned) connections — everything on the Assistant tools tab, including Slack.
    https://commandplusk.com/api/public/mcp/oauth/callback

Both are derived from the origin serving the request, so if you reach Command+K on a different host, swap the origin and keep the path exactly as shown.

Where the tokens live

  • Storage: one row per (workspace, end user, tool) in end_user_mcp_connections, holding the encrypted access token, the encrypted refresh token, the expiry, and the granted scopes.
  • Encryption: AES-256-GCM envelope encryption. The key is derived with HKDF-SHA256 from a Command+K master secret under a dedicated domain separator, so it is cryptographically independent of the signing key that shares the same source. Envelopes are versioned, and any tamper of the IV, ciphertext, or auth tag fails the decrypt rather than returning garbage.
  • Access: the table has row-level security enabled with no policies for regular database roles — every read and write goes through a server endpoint that has already verified the visitor's widget JWT. Plaintext tokens are never returned to a browser and never logged. The widget's connections list returns only display name, server URL, connection state, and timestamps.
  • Lifetime and refresh: the token's own expiry is whatever the provider issued. Before a chat turn uses a tool, an expired (or within-a-minute-of-expiring) token is refreshed with the stored refresh token, and a rotated refresh token replaces the old one. If the refresh fails, the connection is marked expired and the visitor is shown a reconnect prompt instead of a broken tool call. There is no automatic deletion of idle connections — a row lives until someone deletes it, or until the workspace or the allowlist entry is deleted (both cascade).
  • In-flight state: the OAuth state and PKCE verifier are stored in a separate short-lived table, valid for 10 minutes and deleted on first use, so an authorization code cannot be replayed.

Which account is connected

Providers like Linear, Notion and Atlassian ask the visitor to pick a workspace during OAuth, and the first one in the list is easy to accept by mistake. Right after a successful connect, CMD+K asks the newly connected server who it is — its get_workspace, whoami, me or viewer tool, whichever it offers — and stores the workspace or account name on the connection.

Your connections then shows that name under the provider, so "Linear · Connected · AnnounceKit · since Sep 11" instead of just "Linear · Connected". A connected row also has a Switch button: it reopens the provider's OAuth screen so the visitor can pick a different workspace or account, and the new grant replaces the old one — no disconnect-then-reconnect. Servers that expose no identity tool simply show no name; the connection works either way.

Revoking a connection

What the end user can do

  1. Open the widget and click Your connections in the header (the plug icon). It appears only for signed-in visitors.
  2. Click Disconnect next to the tool and confirm.
  3. The stored connection row — access token, refresh token and all — is deleted immediately. Later chat turns can no longer call that tool as them; they are offered a fresh Connect instead.

What a workspace admin can do

  • Revoke one connection: Dashboard → Users → open the visitor → Revoke on the connection. Owner or admin role required.
  • Purge everything for a visitor: the same page offers a GDPR-style purge that deletes their conversations, messages, tool-call audit rows, MCP connections, pending OAuth state, usage counters, and plan overrides.
  • Withdraw a tool from everyone: disabling or deleting the entry in Your users' tools removes it from every visitor at once — deleting it cascades away all connection rows for that tool.
  • What an admin cannot do: connect a tool on behalf of a user. Authorization always happens in the visitor's own browser against their own provider account; there is no admin-side impersonation path, by design.

Known gap: revocation is local, not upstream

Both the end-user Disconnect and the admin Revoke delete Command+K's copy of the token. They do not call the provider's token-revocation endpoint, so the OAuth grant remains listed in the user's provider account until they remove it there (for example Linear's or GitHub's connected-apps settings). If your security review requires upstream revocation, treat it as not implemented today and tell us — it is a tracked gap, not a setting you have missed.

Setup checklist for a new provider

  1. Try enabling it with no credentials first. If the provider supports dynamic registration, you are done — nothing to register, nothing to paste.
  2. If it needs an OAuth app (GitHub and Intercom today), register one with the provider and set its redirect URI to exactly https://commandplusk.com/api/public/widget/mcp/callback. For Intercom, ask Intercom Customer Support to allowlist that redirect URI as well.
  3. Enable the tool in Tools → Your users' tools and bind it to the assistants that should offer it. The per-widget binding decides which assistant exposes which tools.
  4. Make sure the widget runs in a signed-in mode and your host page supplies a widget JWT. Without it, the connect endpoints return 403.
  5. Optionally turn on prompt on open for the tool so signed-in visitors are asked to connect when the chat opens, rather than mid-conversation.

Next steps

FAQ

Do end users need their own API keys for marketplace tools?
No API keys — but yes, every end user authorizes their own account through OAuth. What you as the workspace owner supply depends on the provider: Linear, Notion, Atlassian, ClickUp, monday.com, Sentry, Stripe and Miro accept dynamic client registration, so Command+K registers an OAuth client automatically and you supply nothing. GitHub publishes no registration endpoint, and Intercom advertises one but rejects our callback URL, so for those two you must register your own OAuth app and paste its client ID and secret. There is no pre-configured, platform-shared OAuth app in production today.
Can my end users connect Slack?
Not today. Slack is available only as an assistant-level connector — one Slack app that your workspace connects once, used by the assistant on everyone's behalf. It is not in the end-user marketplace, so a visitor cannot link their own Slack account through the widget.
Which callback URL do I register in my OAuth app?
It depends on which side of the product you are wiring. End-user (widget visitor) connections use https://commandplusk.com/api/public/widget/mcp/callback. Assistant-level connections — the tools your workspace connects once — use https://commandplusk.com/api/public/mcp/oauth/callback. They are two different endpoints; registering the wrong one produces a redirect_uri mismatch at the provider.
Where are the end-user tokens stored?
In the end_user_mcp_connections table, one row per (workspace, end user, tool). Access and refresh tokens are encrypted with AES-256-GCM using a key derived from a Command+K master secret; the table is service-role only under row-level security, and tokens are decrypted in memory only while a chat turn is calling that tool.
Can a workspace admin disconnect a tool on a user's behalf?
Yes — Dashboard → Users → the user → Revoke deletes the stored connection. What neither the admin nor the end user gets today is an upstream revocation: Command+K deletes its copy of the token but does not call the provider's token-revocation endpoint, so the grant stays alive in the provider's account settings until the user removes it there.
Does connecting an end-user tool use up a plan slot?
No. The 'MCP connections' plan cap counts assistant-level connectors only. End-user marketplace entries are not counted against it. The gates that do apply are: you must be a workspace owner or admin to enable one, the workspace must not be in read-only mode (expired trial or lapsed subscription), and the visitor must be signed in — anonymous visitors on a public widget cannot hold a personal connection.