feat(plugins): dynamic OAuth clients, plus Notion, Slack and Gmail
Connections become a first-class thing, separate from plugins.
A connector registry (packages/shared/src/connectors/connectors.json) owns how an account is obtained, refreshed and disconnected. A connections table holds one row per connected account, keyed on the provider's own account id. A plugin manifest only names the connector it needs — extensions.superset.connector: { "slug" } — and carries no OAuth configuration of its own: no scopes, no client mode, no requires_env.
Agents reach a plugin's tools through /mcp/plugins/<marketplace>/<plugin> on the API, which resolves the caller's connection and proxies to the vendor's MCP server, or serves the tools in-process for the plugins named in SUPERSET_HOSTED_PLUGINS (gmail, slack). The credential stays on the server and is attached there; it is never written into a local agent config.
Connectors
Which client path each takes was decided by probing the real servers, not by assumption:
| Connector | Registration | CIMD | Client |
|---|---|---|---|
notion | yes | yes | client: "dynamic" |
linear | yes | yes | static (LINEAR_CLIENT_*) |
slack | no | no | static + PKCE (SLACK_CLIENT_*) |
google | no | no | static + PKCE + authorization_params (GOOGLE_CLIENT_*) |
github | — | — | static (PLUGIN_GITHUB_CLIENT_*) |
sentry | — | — | static (SENTRY_CLIENT_*) |
GOOGLE_CLIENT_*, SLACK_CLIENT_*, LINEAR_CLIENT_* and SENTRY_CLIENT_* are the variables that were already set; only PLUGIN_GITHUB_CLIENT_* keeps its prefix. Google needs access_type=offline + prompt=consent to be issued a refresh token at all — that is what authorization_params is for — and its hour-long tokens are the first real exercise of refresh-before-dispatch.
Dynamic clients
"client": "dynamic" means the API takes endpoints and client identity from the MCP server at connect time:
- Read
/.well-known/oauth-protected-resourcefor the server themcp.urlnames. - Follow the authorization server it points at for the endpoints.
- Get a client identity — a client id metadata document we host when the server advertises
client_id_metadata_document_supported, otherwise an RFC 7591 registration kept inplugin_oauth_clients, unique on(issuer, redirect_uri)so everyone connecting one server shares a registration.
The flow is PKCE with an RFC 8707 resource indicator, and the authorization response's iss is checked against the discovered issuer before the code is redeemed (RFC 9207). Registration asks only for the grant types the server advertises, and refuses to register as a public client when the server never offered none.
The point: a hosted MCP server becomes installable without anyone registering an OAuth app first.
One account, or an error
A lookup that matches two live connections raises AmbiguousConnectionError — 409 from the endpoint, CONFLICT from tRPC — rather than resolving to whichever row was updated last. "Newest wins" is the kind of behaviour people rely on before anyone notices it was arbitrary.
A connection whose refresh fails is written back as disconnected_at + disconnect_reason = 'needs_reauth', so it stops reading as connected everywhere at once. connectors.status and integration.connectionStatus keep such a row and flag it, and a trigger built on one says Reconnect, not Connect. A token endpoint that is merely unreachable or answering 5xx is a 502 instead: the provider being down must not disconnect a good connection.
Schema and secrets
The published JSON Schema at /schemas/plugin/1.0.0.json is generated from pluginManifestSchema in packages/shared, and superset plugins publish validates against that same definition — the schema we serve and the schema we enforce cannot drift.
Connection secrets are sealed with SECRETS_ENCRYPTION_KEY under a v1: prefix; unprefixed values still decrypt with the legacy key, so rotating the auth secret no longer invalidates every connection.
Migration
0119_plugin_oauth_clients_and_connectors creates connections and plugin_oauth_clients, and drops plugin_connections with no backfill. integration_connections stays in place as dead data. Both are deliberate: the only installs today are internal, and nobody has a connection worth migrating.
It also changes ingest.webhook_events.provider from the integration_provider enum to text, because connectors are open-ended slugs now. Enum and text are not binary-coercible in Postgres, so that statement takes ACCESS EXCLUSIVE and rewrites the table — worth knowing before it runs against production.
Verified against the live Notion server
Not just unit tests — the whole path, end to end:
- Discovery against
mcp.notion.comselects the CIMD branch. - Authorize returns
302with ourclient_id,redirect_uri,code_challenge/S256andresourceround-tripped. - Consent completes, the callback exchanges the code, and a connection row lands.
- Tools list and call through
/mcp/plugins/superset/notion, returning real workspace data with the credential held server-side.
Plus repo-wide typecheck, lint, check:plugins, and check:i18n with every new string translated in all 16 locales.
Known gaps
- Linear, Slack and Gmail are unexercised end to end. Notion was run against the live server; the others have not completed a real connect. Slack's
oauth.v2.user.accessresponse shape deserves a first real run, and its user scopes may need to travel inuser_scoperather thanscope. - Verifying a dynamic client needs a public https tunnel. CIMD makes the
client_ida URL the authorization server fetches, sohttp://localhostcannot work, andredirect_urisin the hosted document come fromNEXT_PUBLIC_API_URL— settingPLUGIN_CLIENT_METADATA_BASE_URLalone is not enough. - Slack's MCP server entry moved to the OAuth path. Existing installs materialised
mcp.slack.com/mcpinto agent configs; with nomcpServersentry, the next sync reaps it. Fine for internal installs, which is all there are today. PLUGIN_CATALOGis a second, hand-maintained source of truth for plugin versions alongside the generated manifests, and it has already drifted. Consolidating it ontoFIRST_PARTY_MANIFESTSis a follow-up.- The MCP endpoint is on SDK 1.30, not the v2
@modelcontextprotocol/serverhelpers, because/mcpand the CLI pin 1.30 today. Moving all three together is a follow-up ticket, along with tool policy, syntheticauthenticatetools, and multi-account support.
https://claude.ai/code/session_0142DJCnAikxQQSXWNQCegWz
Summary by CodeRabbit
-
New Features
- Added Notion, Slack, Gmail, Google Docs, and Google Sheets plugins to the marketplace.
- Added app deep-link installation support and expanded plugin branding icons.
- Added dynamic OAuth setup with provider discovery, PKCE, automatic client registration, and token refresh.
- Added a dedicated Remove button for installed plugins.
- Added Notion skills for searching and writing pages.
-
Updates
- Updated Linear to version 1.5.0 with dynamic OAuth support.
- Added provider-specific OAuth authorization and token parameters.