PluginWorld
Vs

vscode-notebook-mcp

MCP

Exposes the Jupyter notebooks open in VS Code to MCP-compatible AI agents, letting them edit cells in the open editor and execute them in the live kernel without file conflicts. Agents can make partial edits, track stale cells, inspect variables, and manage kernels while preserving the user's interpreter choices and unsaved work.

@YukHin · v0.6.2 · MIT License · updated today

SECURITY

B

SCORE

68

STARS

▲ 0

PLUG IN

git clone https://github.com/YukHin/vscode-notebook-mcp.git

See the README to configure this MCP server

README

vscode-notebook-mcp

Jupyter notebooks for AI agents other than GitHub Copilot. Copilot works on a notebook inside VS Code: it edits cells in the open editor and runs them in the kernel you're using. Agents outside VS Code — Claude Code, Codex, or any other MCP client — can't. They can only rewrite the .ipynb file on disk while the same notebook is open in VS Code, and that breaks in three ways:

  • File conflicts you can't save past. The editor and the file now disagree. VS Code reports that the file on disk is newer, and saving means choosing between throwing away your unsaved work and overwriting the agent's changes. Outputs and edits get lost either way.
  • No access to the running kernel. The agent can't run a cell in the kernel you're using or read its variables, so it re-runs whole notebooks from scratch or guesses.
  • Lost context. Nothing tells the agent which code the kernel actually ran, so it reasons about variables that no longer match the code on screen.

This extension closes that gap. It exposes the notebooks open in VS Code to MCP-compatible agents, so they work the way Copilot does:

  • Edit in the editor, not on disk — no file conflicts. Changes go through VS Code's notebook API, into the document you have open: undoable, marked in the editor, other cells' outputs untouched, and saved by VS Code itself only when asked, so there is never a second version of the file to conflict with.
  • Run in the live kernel. Cells run in the kernel VS Code is already using, so variables survive between the agent's steps; long runs, interrupts and restarts are handled.
  • Change only what's needed. Replace one piece of a cell instead of rewriting it, address cells by stable ids, and get compact outputs instead of whole HTML tables.
  • Keep the context straight. The agent can see which cells were edited since they ran or never ran in this kernel, rerun just what's stale, and inspect variables without adding cells.
  • Stay out of the way. It keeps your kernel and Python interpreter choices, doesn't hide your terminal, and connects each Claude Code session to the right VS Code window.

This is a fork. It is based on caryan/vscode-notebook-mcp by Colm Ryan (MIT), forked at v0.3.0 (commit 9e8061a). The original license and copyright are kept in LICENSE.

How the fork was made. The changes from 0.4.0 on — token authentication and DNS-rebinding protection, reliable execution tracking, Windows support, partial edits, stable cell ids, long-run handling, output budgets, stale-cell tracking, variable inspection, agent-edit markers and the stdio bridge — were designed, implemented and tested mainly by Claude Code (Anthropic's coding agent, model Claude Opus 5.5), working from a code review it wrote of the original project and directed and reviewed by YukHin. See CHANGELOG.md for the full list and the commit history for details.

Status: pre-alpha.

Tools (22)

Discovery

Tool Description
notebook_list_open List all open notebooks with URI, file name, cell count, and which is active
notebook_open Open a .ipynb file (absolute path, file:// URI, or workspace-relative) so its cells become available
notebook_list_cells List cells with stable id, index, kind, language, preview, execution state, and run state (fresh / edited since run / not run in this kernel)
notebook_status Does the kernel match the code on screen? Edited, not-run and downstream cells, running cells, unsaved changes
notebook_get_cell_content Full source of a cell
notebook_get_cell_output Current outputs of a cell (text, errors, images), within an output budget

Cell manipulation

Tool Description
notebook_insert_cell Insert a code or markdown cell at any position; optionally execute
notebook_replace_in_cell Replace one exact piece of text in a cell, leaving the rest untouched
notebook_apply_edits Several replace / whole-cell edits across cells, all-or-nothing
notebook_edit_cell Replace the whole contents of a cell
notebook_delete_cell Delete a cell
notebook_save Save the editor's in-memory notebook to its .ipynb

Execution

Tool Description
notebook_run_cell Execute a code cell and return this run's outputs; long runs report running after timeout_seconds
notebook_run_cells Run a range, everything from the first stale cell on (mode: "stale"), or all; stops at the first error; one summary
notebook_wait_cell Wait for a running cell without starting it again
notebook_inspect_variables Kernel variables with type / shape / dtype / columns, or previews of named ones — no cell added, execution count unchanged
notebook_interrupt_kernel Stop running and queued cells, keeping kernel state
notebook_clear_cell_output Clear outputs of one cell
notebook_clear_all_outputs Clear outputs of every cell

Kernel

Tool Description
notebook_get_kernel_info Language, status, notebook URI
notebook_select_kernel Make sure the notebook has a kernel. With no arguments it keeps the running kernel or attaches the workspace's selected Python interpreter; with python_path or kernel_id it attaches that one, but won't replace a running kernel unless replace_running_kernel is set (see Kernel selection)
notebook_restart_kernel Restart the kernel, clearing in-memory state but keeping cell outputs

All tools accept an optional notebook_uri (omitted → uses the active notebook editor) and response_format ("markdown" or "json"). Cell tools take cell_id (from notebook_list_cells; it survives inserts and deletes) or index. Tools that return outputs take max_output_chars (default 20,000; long text keeps its start and end) and max_images (default 4), and return one form per output (no HTML duplicate of a DataFrame, no ANSI color codes).

Installation

Requirements

  • VS Code 1.85 or later, with the Python and Jupyter extensions (ms-python.python, ms-toolsai.jupyter).
  • Node.js 18 or later on PATH, to build the extension and to run the bridge that Claude Code talks to.
  • A Python environment with ipykernel for your notebooks (what Jupyter in VS Code needs anyway).
  • Claude Code, or another MCP client.

Install from the Marketplace

In VS Code, open the Extensions view, search for Notebook MCP for Claude Code & Codex, and install it. Or from a terminal:

code --install-extension yukhin.vscode-notebook-mcp

Or build from source

git clone https://github.com/YukHin/vscode-notebook-mcp.git
cd vscode-notebook-mcp
npm install
npx @vscode/vsce package --no-dependencies
code --install-extension vscode-notebook-mcp-0.6.1.vsix

The extension id is yukhin.vscode-notebook-mcp. If you installed an earlier build published as colmryan.vscode-notebook-mcp, uninstall it first: code --uninstall-extension colmryan.vscode-notebook-mcp.

On Windows PowerShell, if scripts are disabled (npm.ps1 cannot be loaded), use npm.cmd and npx.cmd instead, or run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once.

Installing or updating an extension makes VS Code restart the extension host in every open window, which stops running Jupyter kernels there. Do it when no notebook has state you want to keep.

After installing, each VS Code window shows a notebook icon with :49777 (or the next free port) on the right of the status bar. Click it for the server info.

Connecting Claude Code

  1. In VS Code, open the command palette (Ctrl+Shift+P / Cmd+Shift+P) and run Notebook MCP: Add to Claude Code, then confirm. This runs, for all your projects:
    claude mcp add --scope user notebook -- node ~/.vscode-notebook-mcp/bridge.js
    
    If VS Code can't find the claude command, the command is copied to your clipboard instead; run it in a terminal (Copy Claude Code Command does the same).
  2. Start a new Claude Code session from your project folder. Claude Code loads MCP servers when a session starts.
  3. Check the connection from that folder:
    claude mcp get notebook
    
    It should say "Connected". The bridge talks to the VS Code window whose workspace contains the folder Claude Code runs in, so open the project folder in VS Code first. With several windows open, each project reaches its own window; to point at a different folder, set NOTEBOOK_MCP_WORKSPACE.

No token is stored in Claude Code's config: the bridge reads it from ~/.vscode-notebook-mcp/<pid>.json, which each window writes when it starts.

Other MCP clients can use the bridge the same way (a stdio server: node ~/.vscode-notebook-mcp/bridge.js), or talk HTTP directly to the URL in that file with Authorization: Bearer <token>. To keep the token out of a checked-in .mcp.json, reference an environment variable: "headers": { "Authorization": "Bearer ${NOTEBOOK_MCP_TOKEN}" }.

Using it

Open the notebook in VS Code, then ask Claude Code about it in plain language. It sees the notebook as it is in the editor (including unsaved changes and the running kernel), and its edits land in the editor, where you can undo them.

Examples of what to ask:

  • "Look at analysis.ipynb. Which cells are out of date, and bring the kernel up to date." → notebook_status, then notebook_run_cells with mode: "stale".
  • "In the cell that loads the data, change the date filter to 2024 and rerun everything after it." → notebook_replace_in_cell (changes only that line), then notebook_run_cells.
  • "What's in the kernel right now? I need the shape of df and the columns." → notebook_inspect_variables (no cell is added, the execution count doesn't change).
  • "Run the training cell; it takes about 10 minutes." → notebook_run_cell returns running after its timeout with the output so far; Claude Code keeps waiting with notebook_wait_cell, or stops it with notebook_interrupt_kernel if you ask.
  • "Fix the error in cell 7 and save the notebook." → edit, run, notebook_save.

What you'll see in VS Code:

  • Cells Claude changed or inserted show Changed by agent / Inserted by agent in their cell status bar until you edit or run them. Undo with Ctrl+Z (Cmd+Z) in the notebook.
  • The first time Claude inspects variables, Jupyter asks "Do you want to grant Kernel access to the extension Notebook MCP for Claude Code & Codex?". Choose Allow (once). To change it later, run Jupyter: Manage Access To Jupyter Kernels.
  • To approve every change before it's applied, turn on the setting notebook-mcp.confirmEdits: a diff opens and a notification asks Apply or Reject (no answer in 2 minutes rejects).
  • Edits stay in the editor until saved. Claude Code is told to use these tools, not its file tools, for open notebooks, and to call notebook_save when done.

The kernel is chosen the way you'd expect: if the notebook already has a running kernel, it is kept; otherwise the workspace's selected Python interpreter is used. Claude Code only switches to a different kernel if you ask it to (switching discards the kernel's variables), and it puts your workspace's Python interpreter back afterwards.

Troubleshooting

Symptom What to do
claude mcp get notebook fails: "No VS Code window with Notebook MCP is open for …" Open that folder (or a parent) in VS Code, or set NOTEBOOK_MCP_WORKSPACE to the folder of an open window.
Claude Code doesn't list the notebook tools Start a new Claude Code session; check claude mcp list.
The status bar shows the notebook icon with ✗ Click it for the error, then Restart. Another program may hold the ports 49777–49876; set notebook-mcp.port.
notebook_inspect_variables says it has no kernel access Run Jupyter: Manage Access To Jupyter Kernels and allow Notebook MCP for Claude Code & Codex.
An interrupt doesn't stop a cell on Windows time.sleep and some C extensions can't be interrupted until they return. Wait (notebook_wait_cell) or restart the kernel.
You think the token leaked Run Notebook MCP: Regenerate Access Token. The bridge picks up the new one automatically.

For development (running from source with F5, tests), see Development.

Security

The server listens on 127.0.0.1 only, rejects requests whose Host isn't 127.0.0.1/localhost on its port (DNS rebinding), rejects any request with an Origin header (web pages), sends no CORS headers, and requires the bearer token on /mcp. If the token leaks, run Notebook MCP: Regenerate Access Token.

Configuration

Setting Default Description
notebook-mcp.port 49777 Preferred port. Auto-increments if busy (up to +99).
notebook-mcp.confirmEdits false Review each agent change first: a diff opens and a notification asks to apply or reject (no answer in 2 minutes rejects).

Cells the agent changed or inserted show Changed by agent / Inserted by agent in their cell status bar until you edit or run them; undo with Ctrl+Z (Cmd+Z).

Commands

  • Notebook MCP: Add to Claude Code
  • Notebook MCP: Copy Claude Code Command
  • Notebook MCP: Regenerate Access Token
  • Notebook MCP: Restart Server
  • Notebook MCP: Show Server Info

Architecture

┌─────────────────────────────────────────────────────────┐
│                    VS Code window                       │
│                                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │  Jupyter extension (ms-toolsai.jupyter)           │  │
│  │                                                   │  │
│  │  Notebook document  ◄──►  Kernel  ──►  Outputs    │  │
│  └───────────────────────────────────────────────────┘  │
│                          ▲                              │
│                          │ vscode.NotebookEdit,         │
│                          │ notebook.cell.execute,       │
│                          │ jupyter.kernels.getKernel    │
│                          │                              │
│  ┌───────────────────────┴───────────────────────────┐  │
│  │  This extension                                   │  │
│  │                                                   │  │
│  │   HTTP server :49777  ──►  MCP tools (13)         │  │
│  │   (StreamableHTTPServerTransport)                 │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                              │ HTTP (MCP protocol)
                              ▼
                ┌───────────────────────────────┐
                │   AI agent                    │
                │   (Claude Code, Cursor, etc.) │
                └───────────────────────────────┘

Implementation notes

  • Writes go through vscode.NotebookEdit + vscode.WorkspaceEdit, preserving undo/redo.
  • Execution dispatches notebook.cell.execute and waits by polling cell.executionSummary.success for a boolean value (VS Code creates the summary as soon as execution starts but success only becomes a boolean when the kernel finishes).
  • Inserted cells are tagged with a metadata id so the executor can find them again after the index shifts due to other concurrent edits.
  • Multi-notebook: every tool accepts notebook_uri. Resolution: explicit URI → look up in vscode.workspace.notebookDocuments (or open it); otherwise fall back to vscode.window.activeNotebookEditor.

Kernel selection

The agent-friendly way to attach a kernel is to give notebook_select_kernel a python_path — the absolute path to the interpreter you want (e.g. a venv's bin/python):

notebook_select_kernel({ "python_path": "/abs/path/to/.venv/bin/python" })

You don't have to know any controller id, and the interpreter doesn't have to be one you've already selected in VS Code — the tool does both the discovery and the id computation for you. (kernel_id is still accepted for the rare case you already hold a controller id; without either argument the tool falls back to popping VS Code's kernel picker, which is useless to a headless agent.)

Under the hood this navigates two pieces of friction — the Jupyter extension exposes no public API to enumerate or assign controllers, and controller ids for interpreter-backed Python kernels are derived strings it computes internally:

  1. No controller until the interpreter is known. A venv you've never selected in VS Code has no controller to bind to. Given python_path, the tool registers the interpreter with the Python extension first (registerInterpreter in src/utils/pythonEnv.ts: refresh discovery, resolve the path, set it active for the workspace) so Jupyter creates one — the same thing "Python: Select Interpreter → Enter interpreter path…" would do, done programmatically.

  2. Computing the id. The controller id is built from the interpreter path (controllerIdForInterpreter in src/utils/kernel.ts). Empirically (verified by reading VS Code's "wanted kernel … all: …" log line) the format for an interpreter-backed kernel is:

    .jvsc74a57bd0<sha256(normalizedInterpreterPath)>.<normPath>.<normPath>.-m#ipykernel_launcher
    

    Where normalizedInterpreterPath strips just the bin/ segment of a venv path while keeping the python filename — so /x/y/.venv/bin/python becomes /x/y/.venv/python (system paths like /usr/bin/python3 are left untouched). See getKernelId and getInterpreterKernelSpecName in microsoft/vscode-jupyter.

The controller lands a beat after the interpreter becomes known, so the tool retries notebook.selectKernel for a few seconds (selectKernelById) rather than firing once and risking a silent no-op. This is brittle to upstream changes in the Jupyter extension, but the failure mode is informative — VS Code logs both the requested id and the available ones (wanted kernel DOES NOT EXIST, wanted: <id>, all: <ids>) so a changed format can be reverse-engineered the same way. The integration suite exercises the full path (including a brand-new, never-registered venv) in test/integration/suite/kernel.test.ts.

Still open / not yet handled: other forms of "kernel" (a global kernelspec name, a remote Jupyter server URL), and version probing of the interpreter.

Development

npm install
npm run build       # esbuild bundle
npm run watch       # rebuild on change
npm run typecheck   # tsc --noEmit
npm test            # @vscode/test-electron + Mocha integration tests

Press F5 in VS Code to launch the Extension Development Host.

Running the tests

npm test runs the integration suite under @vscode/test-electron. It downloads a fresh VS Code Insiders build, installs ms-python.python and ms-toolsai.jupyter into it, opens the test workspace at test/fixtures/workspace/, and runs Mocha specs that drive the MCP tools through an in-memory transport (no HTTP). Test files live in test/integration/suite/.

Why Insiders: macOS refuses to launch a second extension-host of the same bundle id, so running tests against stable VS Code fails when you already have stable VS Code open ("currently only supported if no other instance of Code is running"). Insiders and stable have distinct bundle ids and coexist. Override with VSCODE_TEST_CHANNEL=stable if you don't have stable open.

Prerequisites:

  • uv on PATH — used to manage the Python venv at test/fixtures/python/.venv that supplies the test kernel (ipykernel + matplotlib + numpy + plotly). Pinned to Python 3.13 via .python-version.

The pretest hook builds the extension (npm run build), compiles the tests (npm run build:test → out/), and runs scripts/setup-test-python.sh which is idempotent (uv sync only).

Notes on what the tests cover:

  • Non-execution tools (notebooks.test.ts, cells.test.ts) — list/open/get/insert/edit/delete/clear, error paths, active-editor fallback. Run without a Python kernel.
  • Execution + kernel (execution.test.ts, kernel.test.ts) — depend on selectVenvKernel attaching the venv's interpreter; see Kernel selection.
  • Plotly (plotly.test.ts) — verifies a Plotly figure flows through the hidden webview renderer to a PNG image content block.

Dependencies

Runtime (dependencies)

  • @modelcontextprotocol/sdk — the MCP server itself. We use McpServer (one per session, see src/mcp/server.ts) and StreamableHTTPServerTransport to speak MCP over the embedded HTTP server. Tool registration in src/mcp/tools/*.ts is all SDK API.
  • plotly.js-dist-min — bundled into dist/plotly.min.js by the copy:plotly script and loaded inside the notebook-mcp.plotlyRenderer webview, which runs as a background editor tab. The webview converts Plotly figure JSON to PNG via Plotly.newPlot + Plotly.toImage. We can't run Plotly in the extension host (no DOM), and shipping the bundle locally avoids a runtime CDN fetch under the webview's strict CSP. Using -dist-min (the prebuilt minified bundle) instead of plotly.js keeps the VSIX smaller and avoids pulling in Plotly's full build toolchain.
  • zod — schema definitions in src/schemas/index.ts and per-tool input validation across cells.ts / kernel.ts / notebooks.ts. The MCP SDK accepts zod schemas directly when registering tools, so we get validation + JSON Schema generation for free.

Dev (devDependencies)

  • @types/node — Node typings for extension-host code (Buffer, process, http, etc.).
  • @types/vscode — VS Code Extension API typings. Pinned to ^1.85.0 to match engines.vscode.
  • esbuild — bundler used by npm run build to produce the single dist/extension.js that VS Code loads. Chosen over tsc for build speed and tree-shaking; --external:vscode keeps the host-provided module out of the bundle.
  • typescript — provides tsc for the typecheck script (tsc --noEmit) and the build:test script (tsc -p tsconfig.test.json → out/). Esbuild handles the production bundle; tsc handles the test build because Mocha loads test files directly.
  • mocha + @types/mocha — test runner, the convention with @vscode/test-electron. The bootstrapper at test/integration/suite/index.ts discovers *.test.js files under out/test/integration/suite/ and feeds them to Mocha.
  • @vscode/test-electron — downloads a stable VS Code into .vscode-test/, installs ms-toolsai.jupyter into it, and launches it with the extension under development plus the test workspace fixture. See test/integration/runTest.ts.
  • glob + @types/glob — used by the Mocha bootstrapper to discover compiled test files.

Building a VSIX

To produce an installable .vsix file:

npx @vscode/vsce package

This runs vscode:prepublish (which builds via esbuild) and writes vscode-notebook-mcp-<version>.vsix to the repo root. Install it with:

code --install-extension vscode-notebook-mcp-<version>.vsix

.vscodeignore keeps the package small by excluding TS sources, node_modules, source maps, and dev configs — only the bundled dist/, package.json, README.md, and LICENSE are shipped.

References

Other projects along similar lines to this project:

The original author built the upstream project fresh, to learn and to have room to tweak the design and develop new features such as reading Plotly images; this fork builds on that work.

License

MIT — see LICENSE. Copyright (c) 2026 Colm Ryan for the original project and (c) 2026 YukHin for the fork's changes, both under the MIT license.

SIMILAR PLUGINS