Skip to content

Scope MCP OAuth tokens to user + workspace (with backfill) #222

Description

@paulocastellano

Context

Today MCP OAuth tokens (Claude/ChatGPT connectors) are scoped to the user onlyoauth_access_tokens.workspace_id is null — while API keys (personal access tokens) are scoped to user + workspace (ApiKeyController sets workspace_id at creation).

Because the token carries no workspace, the MCP server resolves the workspace at request time from the user's current_workspace_id (see app/Mcp/Tools/** and LoadWorkspaceFromToken). That has a few consequences:

  • A teammate's MCP connection silently operates on whatever their current workspace happens to be — in multi-workspace accounts, an agent can act on the wrong workspace without anyone noticing.
  • The onboarding MCP step is account-scoped (ResolveOnboardingStatus::accountHasMcpConnection) precisely because tokens have no workspace to check.
  • The MCP settings page lists connections account-wide, and the account owner cannot revoke a teammate's grant (McpSettingsController::disconnect filters by the requester's user_id).
  • Offboarding relies on membership checks alone: a removed member's token stays valid and silently works again if they are re-added.

Proposal

Bind MCP OAuth tokens to a workspace, mirroring API keys:

  1. Connect flow: the OAuth authorize URL captures the workspace being connected (state parameter), and the issued token gets workspace_id set.
  2. Request resolution: LoadWorkspaceFromToken / MCP tools resolve the workspace from the token's workspace_id instead of the user's current_workspace_id (PATs already work this way).
  3. Backfill: data migration assigning existing MCP OAuth tokens to the user's current workspace (or first workspace on the account); tokens that can't be mapped confidently get revoked with a reconnect path.
  4. Settings page: list connections per workspace instead of account-wide; revisit whether the account owner may revoke a teammate's grant.
  5. Onboarding: accountHasMcpConnection can become workspace-scoped again (or stay account-scoped deliberately — decide).
  6. Tests: token creation binding, request resolution from token workspace, backfill mapping/edge cases, disconnect semantics.

Out of scope here

Deliberately not part of #204 (post-subscription onboarding) — the model change + backfill deserves its own PR.

References

  • app/Models/AccessToken.php (activeMcpOAuth, isMcpOAuthGrant)
  • app/Http/Controllers/App/McpSettingsController.php
  • app/Http/Middleware/Api/LoadWorkspaceFromToken.php
  • app/Actions/Onboarding/ResolveOnboardingStatus.php
  • app/Http/Controllers/App/ApiKeyController.php (PAT user+workspace model to mirror)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions