feat(claude-mcp): disable MCP servers per workspace#286
Merged
Conversation
Adds a per-row "Disable for workspace" toggle on the Claude MCP settings panel that writes to `projects.<cwd>.disabledMcpServers[]` in `~/.claude.json`, matching the scope Claude Code itself reads from. Other Jupyter workspaces are untouched: the disable list is keyed by the current working directory. The toggle is workspace-wide (not per-scope), so a server defined in user scope can still be opted out of a single workspace without removing it from Claude. Disabled rows render with a dashed border, italic name, and a "Disabled for workspace" badge; the toggle button is always visible on those rows so users don't need to hover to re-enable. Backend writes go through `config._atomic_write_json` to share the symlink/mode/parent-dir-fsync durability contract with the rest of NBI's on-disk edits. The PATCH endpoint validates both scope and the boolean payload (a stringly-typed "false" no longer flips the row). Skipped: per-scope disable lists (Claude itself doesn't model them) and local-state patching after toggle (refresh() already runs and the cost is one HTTP round-trip on a click). Closes plmbr#283
mbektas
reviewed
May 17, 2026
| # per-scope), so any of user/project/local resolves to the same write. | ||
| # We still validate it so a malformed URL fails fast. | ||
| try: | ||
| validate_scope(scope) |
Collaborator
There was a problem hiding this comment.
validate_scope import is missing
The PATCH handler added in the previous commit called validate_scope without importing it, so any real PATCH dispatch raised NameError and 500'd. Caught by mbektas in review. Adds two layers of regression coverage so the same import-miss class of bug can't repeat: - TestPatchEndpointDispatches in tests/test_claude_mcp_manager.py drives the real ClaudeMCPDetailHandler under tornado.testing with APIHandler.prepare + current_user stubbed. Verified to fail with the exact NameError when the import is removed. - ui-tests/tests/claude-mcp-patch.spec.ts hits the running JupyterLab server's /notebook-intelligence/claude-mcp/<scope>/<name> PATCH route from a browser context, so routing, auth middleware, and prepare() chain are all exercised end-to-end. Both new test suites pass with the fix and fail without it. The pre-existing chat-sidebar Galata test is unrelated and fails on upstream/main as well.
Collaborator
Author
|
Good catch, fixed in 05828e8. Added two regression tests so this class of import-miss can't slip past CI again:
Both pass with the fix and fail without it. |
mbektas
approved these changes
May 17, 2026
pjdoland
added a commit
to pjdoland/notebook-intelligence
that referenced
this pull request
May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22 and expands it to cover everything merged into upstream/main after PR plmbr#287's docs refresh. Bumps package.json to 5.0.0. CHANGELOG additions cover the post-plmbr#287 surface: - Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286), JSON-paste path in Add MCP server (plmbr#285). - Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332). - Chat sidebar and agentic UX: workspace @-mention in Claude mode (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt away from over-eager notebook creation (plmbr#336). - Skills: multi-manifest support (plmbr#321), tracks-upstream for user- imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291). - Accessibility: full sub-section covering plmbr#305-plmbr#320. - Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323), 0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK swap (plmbr#324). - Fixed: session listing unification (plmbr#310), session preview unwrap (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler leak (plmbr#294). - Removed: fastmcp dependency, history.jsonl session gate. Adds a Migration note covering the five behavior changes operators should review before upgrading from 4.x: fastmcp swap, path sandboxes, history.jsonl gate removal, workspace @-mention pointer shape, and the Copilot WebSocket auth/origin tightening. Two reviewer rounds (six personas each) applied: - Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes, and the stale `fastmcp==2.x.*` recommendation in the admin guide. - Round 2 caught the missing plmbr#301 migration bullet, missing version- matrix 5.0.x row, missing README TOC entry, and a couple of style nits (sub-heading overpromise, orphan bullet). Skipped (deferred to future PRs): - README first-run tour mention. - Admin guide HTTP kill-switch row in Failure-modes table. - Terminal drag-drop trust-model precision update after plmbr#327. - Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not AES-GCM).
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Adds a per-row Enable/Disable for Workspace toggle on the Claude MCP settings panel. The toggle writes to the same
projects.<cwd>.disabledMcpServers[]array in~/.claude.jsonthat Claude Code itself reads, so a server can be opted out of one Jupyter workspace without removing it from the user scope or affecting other workspaces.Solution
The disable list is workspace-wide (a flat array of server names keyed by the current working directory), not per-scope, so a
user-scope server can still be opted out of a single workspace.Backend:
ClaudeMCPServergains adisabled_for_workspace: boolfield, populated duringlist_servers()from the~/.claude.jsonprojects.<cwd>.disabledMcpServers[]entry for the current Jupyter root.ClaudeMCPManager.set_server_disabled(name, disabled)reads~/.claude.json, mutates the disabled list under the current working dir, and writes back viaconfig._atomic_write_jsonso the on-disk edit shares the same symlink-preserving, mode-preserving, parent-dir-fsync durability contract as the rest of NBI's config writes.PATCH /notebook-intelligence/claude-mcp/<scope>/<name>accepts{"disabled_for_workspace": <bool>}. The handler validatesscopeand rejects non-bool payloads (stringly-typed"false"no longer flips the row).Frontend:
NBIAPI.setClaudeMCPServerDisabled(name, scope, disabled)mirrors the wire contract.Testing
pytest tests/(718 passed) including 9 new tests inTestWorkspaceDisable: list-time flag stamping, per-cwd scoping, write idempotency, sibling-key preservation on partial updates, missing-config-file creation, and corrupt-config rejection.jlpm tsc --noEmit,jlpm lint:check,jlpm jest(131 passed) all green.telemetry, etc.) and sibling project blocks.Risks and follow-ups
json.loadandos.replacewould be clobbered. The window is milliseconds and the toggle is user-initiated. The same hazard exists forclaude mcp add, which the existing manager already shells out to.scopefor symmetry with GET/DELETE; the value is validated but ignored on the write path since the disable list is not per-scope. A future per-scope disable list would be a non-breaking extension.refresh()then re-reads the truth).Closes #283