Repository navigation
Show restricted members only the MCP servers their roles reach - #1667
Merged
Merged
Conversation
In a workspace with restricted member visibility, a member who is not an admin now sees, in GET /api/mcp-servers, GET /api/mcp-servers/:id and /:id/activity, only the servers they own or that hold a connector they can see, with only those connectors named and counted. A hidden server answers the same 404 as an unknown id. The server picker (GET /api/mcp-connections/targets and the OAuth consent page) applies each workspace's own mode and the caller's role there, and a grant drops a hidden server like an unknown id. A VIEWER can no longer create an MCP server (403), in every mode, like every other server write. The default server created at sign-up and invite does not go through this route. Admins and open workspaces keep today's code path.
7 of 14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fourth step of "member visibility" (after #1664, #1665 and #1666). In a workspace set to restricted mode, a member who is not an admin now sees on the MCP server routes only the servers their AI client can use: the servers they own, and the servers that hold at least one connector they can see. Inside each server they see only those connectors. The server picker and connection grants follow the same rule, per workspace.
It also fixes a permission bug: a VIEWER could create an MCP server. That is now refused with 403 in every mode, like every other server write.
Admins and open workspaces take today's code path. No workspace has the setting yet and the switch in the UI comes last, so nothing changes on deploy apart from the VIEWER rule.
Changes
Every read handler has the same shape:
scopeFor(req.user), and when the scope is not restricted, today's code unchanged.MCP servers controller (
api/mcp-servers):GET /: visible servers only, filtered in the Prismawhere(newMcpServersService.findAllVisibleByOrg, workspace filter ANDserverWhere), solimitandoffsetpage over visible servers. Connector names and_count.connectorscount only visible connectors.usagestays: two numbers, no identity.GET /:id: 404 for a hidden server, with exactly the body an unknown id gets (MCP server not found). Connectors and their count filtered (newfindVisibleById, one query).GET /:id/activity: same 404 for a hidden server, then today's numbers.POST /(D10): 403Viewers cannot modify MCP serversfor a VIEWER, in every mode, before the plan check. Same message as the other server writes. EDITOR and ADMIN unchanged. The default server a member gets at sign-up or invite comes fromMcpServersService.createDefaultForUser, not this route, so it is not affected. The integration spec creates the fixtures' default servers through that method, for a VIEWER too.MCP connections (
api/mcp-connections) and the OAuth consent page:GET /targets: per workspace, by that workspace's own mode and the caller's role there (read from the membership row). A user can be an admin in one workspace and a member in another. In a restricted workspace where the caller is not an admin, only visible active servers are listed, each counting only visible connectors.PUT /:clientIdwithserverIds: a server the caller cannot see is dropped like an unknown id. If nothing is left the answer is today's{ ok: false, reason: "nothing-granted" }, the same as for an unknown id.McpConnectionGrantService(listSelectableTargets,grantServers). The consent page of the shared/mcpendpoint uses the same two methods (login.controller.ts,maybeAskWhichServersandselect-servers), so one change covers the API and the page: a restricted member is not shown hidden servers, and a tampered form that names one grants nothing for it.GET /(the caller's own grants) is unchanged.resolve, which the MCP endpoint calls per request, is unchanged.Wiring:
McpServersModuleimportsMemberVisibilityModule, neverSettingsModule. The File Search worker does not reachMcpServersModule.McpServersControllerandMcpConnectionGrantService(no@Optional), so the app does not boot without it. It is the last constructor parameter; existing unit specs that build these classes by hand without it take today's path. Every existing spec passes untouched.POST /api/mcp-serversdescription says a VIEWER gets 403. The generated CLI operations do not change (--checkis clean).Type
Testing
New specs:
mcp-servers.member-visibility.spec.ts(368 tests): route coverage, so the suite fails when a GET route of the MCP servers or MCP connections controller has no declared rule. Then the matrix: ADMIN / EDITOR / VIEWER x open / restricted x (no whitelist, role with tools, role without tools), over servers the caller owns, other members' servers, and other members' default servers (D9: an empty one stays hidden, one holding a visible connector is visible). 404 bodies are compared with the body an unknown id gets. Pagination over the filtered list, targets across three workspaces (restricted as a member, restricted as an admin, open), grants, D10 for every role and mode, and fail closed. I checked that removing the activity check or the grant filter makes tests fail.mcp-servers.member-visibility.integration.spec.ts(38 tests, real PostgreSQL throughAUDIT_TEST_DATABASE_URL, as in CI): the same matrix over HTTP with the real services,RolesServiceandorg_settingsrows, including a second workspace where one user is an admin and another a member, the stored grant rows, D10 with real status codes (VIEWER 403, EDITOR and ADMIN 201), pages of a filtered list, and a 500 when the role lookup throws.mcp-servers.module.spec.ts(2 tests): the module providesMemberVisibilityServiceto the controller and the grant service.Commands, in the worktree on
origin/main45e0fdf (packages/backendunless noted). The real-DB runs used a database created for this PR on the local 5433 server, migrated withnpx prisma migrate deployand dropped afterwards.npx jest src/mcp-servers src/auth/server-picker.spec.ts src/auth/login.controller --maxWorkers=4: 14 suites, 204 tests passed (existing specs, unchanged).npx jest src/mcp-servers src/auth src/settings src/roles src/connectors src/mcp-server/mcp-endpoint --maxWorkers=4withAUDIT_TEST_DATABASE_URLset: 181 suites passed, 2 skipped, 1 failed; 4438 tests passed, 11 skipped, 14 failed. All 14 failures areauth/jwt-or-management-key.guard.spec.ts, which needsDATABASE_URLpointing at a migrated database and fails in its setup without it. WithDATABASE_URLset to the test database: 14 passed. This PR does not touch that file.npx jest src/common/openapi.spec.ts src/auth/jwt-or-management-key.guard.boot.spec.ts --maxWorkers=4: 6 passed.npx eslint <changed files>: 0 errors, 0 warnings.npx tsc --noEmit -p tsconfig.json: pass.node scripts/generate-cli-operations.mjs --check: up to date (213 operations).node scripts/generate-llms-txt.mjs --check: exit 0.npx nest build, thennode dist/src/main.jsagainst the test database: "Nest application successfully started",GET /api/mcp-serverswithout a token answers 401. Process stopped afterwards.Cloud and self-hosted: identical, off by default. The VIEWER rule on
POST /api/mcp-serversapplies in both from the deploy on. On Cloud on 2026-11-13, the six servers viewers own are all the default server created at invite time, not through this route.Notes for review, not changed here:
PUT /:id,DELETE /:id,PUT /:id/connectors) already refuse a VIEWER (403). For a restricted member they answer 403 on a hidden server and 404 on an unknown id, so a write can tell "hidden" from "unknown". Only D10 was approved for writes, so they keep today's rule.PUT /:id/connectorschecks only that each connector id is in the workspace. A restricted EDITOR who owns a server can therefore name a connector they cannot see (the MCP endpoint still filters its tools by role).Checklist
npm test) (see the one environment-dependent suite above)Related Issues
Relates to #1664, #1665, #1666.