Repository navigation
feat(mcp): allow project members to authorize platform-wide MCP - #163
AdamSelene wants to merge 3 commits into
Conversation
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>
|
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 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. 💜 |
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>
Description
Lets project members use the platform MCP server (
POST /mcp/platform), governed by the existing per-projectREAD_MCPpermission.Today platform MCP is effectively admin-only: minting a platform-scoped OAuth token (
projectId = null) requiresPlatformRole.ADMIN. This PR opens it to members and expresses "who may use MCP" through the project-levelREAD_MCPpermission 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:
ap_set_project_contextbounds selectable projects to the user's own, and now further to the projects whose role grantsREAD_MCP.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
PlatformRole.ADMINrestriction in the approve controller.mcpAccess.listMcpAccessibleProjectshelper — 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 grantsREAD_MCP. Used by both the approve controller andap_set_project_context.READ_MCPin ≥1 project.READ_MCPon that project (was membership).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 stripsREAD_MCPnow 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-apion the MCP suites (Node 22):test/integration/ce/mcp— 256 passed / 3 skipped (14 files). Added: a platform MEMBER can authorize platform-wide (POST /v1/mcp-oauth/approvewith noprojectId→ 200).test/integration/cloud/mcp— 9 passed. Addedmcp-access.test.ts(privileged → all projects; member withREAD_MCP→ included; custom role withoutREAD_MCP→ excluded) and a no-role → every-tool-denied case inmcp-rbac.test.ts.npm run lint-dev— no new errors.Fixes # (issue)
Breaking change? (required — CI fails if this is left unedited)
Security impact? (required — CI fails if this is left unedited)
🤖 Generated with Claude Code