Skip to content

feat(mcp): allow project members to authorize platform-wide MCP - #163

Open
AdamSelene wants to merge 3 commits into
mainfrom
feat/platform-mcp-for-members
Open

AdamSelene wants to merge 3 commits into
mainfrom
feat/platform-mcp-for-members

Conversation

@AdamSelene

@AdamSelene AdamSelene commented Sep 11, 2026 •

Copy link
Copy Markdown

Description

Lets project members use the platform MCP server (POST /mcp/platform), governed by the existing per-project READ_MCP permission.

Today platform MCP is effectively admin-only: minting a platform-scoped OAuth token (projectId = null) requires PlatformRole.ADMIN. This PR opens it to members and expresses "who may use MCP" through the project-level READ_MCP permission that already exists, rather than a coarse platform-role gate.

Why it's safe

Platform MCP is cross-project by design, and the per-project enforcement already runs on the platform path — it was simply never reached by a non-admin because the admin gate stopped them first:

  • Reach is membership + permission scoped. ap_set_project_context bounds selectable projects to the user's own, and now further to the projects whose role grants READ_MCP.
  • Per-project permissions are re-resolved on every tool call (resolvePermissionChecker({ userId, projectId: selectedProjectId })), reading the member's real role in the selected project. Read-on-project-X / write-on-project-Y is enforced independently per project.

Changes

  1. Allow members to authorize platform-wide MCP — remove the PlatformRole.ADMIN restriction in the approve controller.
  2. New mcpAccess.listMcpAccessibleProjects helper — the single source of truth for MCP reach: all accessible projects for privileged users (and in CE, which has no project RBAC), otherwise the projects whose role grants READ_MCP. Used by both the approve controller and ap_set_project_context.
    • Platform-wide authorization now requires READ_MCP in ≥1 project.
    • Project-scoped authorization now requires READ_MCP on that project (was membership).
  3. Harden the permission checker — when the user has no role in the selected project, deny all tools, not just those declaring a permission. Three helper tools (ap_resolve_property_options, ap_resolve_property_chain, ap_validate_step_config) declare no permission but read the selected project's connections; this closes a stale-selection window (a member removed from a project while their 24h Redis project-selection is still live) and hardens the existing project-MCP path.

Behaviour note (EE/Cloud): all default project roles (Admin/Editor/Viewer) carry READ_MCP, so default setups are unaffected. Only a custom role that explicitly strips READ_MCP now loses MCP token minting on that project — which is the intended semantics. CE behaviour is unchanged (RBAC filtering is skipped off-EE, so the helper returns the same accessible-projects set the previous membership check used).

No new endpoints, fields, columns, env vars, or migrations. Frontend consent page already renders the platform-wide flow for any logged-in user — no frontend change.

How was this tested?

npm run test-api on the MCP suites (Node 22):

  • CE test/integration/ce/mcp — 256 passed / 3 skipped (14 files). Added: a platform MEMBER can authorize platform-wide (POST /v1/mcp-oauth/approve with no projectId → 200).
  • Cloud test/integration/cloud/mcp — 9 passed. Added mcp-access.test.ts (privileged → all projects; member with READ_MCP → included; custom role without READ_MCP → excluded) and a no-role → every-tool-denied case in mcp-rbac.test.ts.
  • Existing per-project RBAC coverage (Editor write allowed, Viewer write denied, etc.) still green.

npm run lint-dev — no new errors.

Fixes # (issue)

Breaking change? (required — CI fails if this is left unedited)

  • no — reviewed, not breaking
  • yes — technical (removed/renamed API field or endpoint, dropped column, new required field, removed/required env var)
  • yes — functional (default/limit/behaviour change, new self-hosted setup step)

Security impact? (required — CI fails if this is left unedited)

  • no — reviewed, no security impact
  • yes — security-sensitive (call out the risk and mitigation in the description above)

🤖 Generated with Claude Code

The platform MCP server (POST /mcp/platform) was effectively admin-only:
minting a platform-scoped OAuth token (projectId=null) required
PlatformRole.ADMIN. This lets any platform member authorize platform-wide
MCP so one connection can reach all of THEIR projects, switchable via
ap_set_project_context.

The scoping that makes this safe already existed on the platform path and
was simply never reached by a non-admin:
- ap_set_project_context bounds selectable projects to the user's
  accessible projects (getAllForUser).
- registerPlatformTools re-resolves the per-project permission checker on
  every tool call against the selected project, so a member's real role in
  each project is enforced independently.

Two changes:
1. Drop the ADMIN-only gate on platform-wide authorization in the approve
   controller (project-scoped access check unchanged).
2. Harden resolvePermissionChecker: when the user has no role in the
   selected project, deny ALL tools — including the few helper tools that
   declare no permission but read the project's connections
   (ap_resolve_property_options / _chain, ap_validate_step_config). Closes
   a stale-selection window after a membership revocation; also hardens the
   existing project-MCP path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Hey — thanks so much for taking the time to open this! 🙏

We've temporarily paused unsolicited pull requests from outside the core team. This isn't about you or the quality of your change — an open PR is a promise from us to review it carefully, and right now the volume (a lot of it AI-generated) means we can't keep that promise for every PR without it becoming unfair to contributors who wait weeks for a reply. So instead of leaving PRs hanging, we close them and point you to a faster path.

Already working with us? If you're an existing customer and we've already discussed this change over Slack, ping us there and we'll add the keep-open label and reopen this PR for review.

You can still build and publish your own piece — just outside this repo, as your own package rather than a PR here. See CONTRIBUTING.md for the full policy. Thanks again for caring about the project. 💜

@github-actions github-actions Bot closed this Sep 11, 2026
@AdamSelene AdamSelene reopened this Sep 11, 2026
@AdamSelene AdamSelene added the enhancement New feature or request label Sep 11, 2026
AdamSelene and others added 2 commits September 11, 2026 13:38
Option (b): reuse the existing per-project READ_MCP permission to decide
who may use MCP, instead of a coarse platform-role gate.

New helper mcpAccess.listMcpAccessibleProjects returns the projects a user
may use MCP on: all accessible projects for privileged users (and in CE,
which has no project RBAC), otherwise the projects whose role grants
READ_MCP. It is the single source of truth for MCP reach, used in two
places:

- Approve controller: platform-wide authorization now requires READ_MCP in
  at least one project; project-scoped authorization requires READ_MCP on
  that project (was membership). Default roles all carry READ_MCP, so only
  custom roles that strip it are affected — which is the intent.
- ap_set_project_context: a member's selectable projects are the ones where
  their role grants MCP, so platform-wide reach == MCP-permitted projects.

CE behaviour is unchanged (RBAC filtering is skipped off-EE, so the helper
returns the same accessible-projects set the membership check used before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace the per-project getRole loop (N+1) with a single
  projectMemberService.listProjectIdsWithPermission(READ_MCP) query,
  unioned with the user's own personal projects (which that query does not
  return, since an owner is not a project_member).
- Honor the operator contract: an OPERATOR is privileged and keeps MCP
  reach to every team project, but no longer reaches other users' personal
  projects, matching the PlatformRole.OPERATOR definition ("all projects
  except others' private projects"). ADMIN still reaches everything.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request keep-open

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant