Skip to main content
Glama
rreusser

Observable Notebook Kit Debug MCP Server

by rreusser
README.md
# mcp-observable-notebook-kit-debug

> MCP server for debugging [Observable Notebook Kit](https://github.com/observablehq/notebook-kit) notebooks.

Enables AI assistants to inspect values, view errors, and capture canvas output from notebooks running in a web browser. For a fully working example, see [./example](./example).

## Why?

The [Observable Desktop](https://observablehq.com/notebook-kit/desktop) is really cool, but it had some limitations. For one, okay, so I paid Anthropic some money for Claude Code, but it's not the *right* Anthropic integration to be able to configure an API key in the app, so I'm out of luck. And I was trying to get some WebGPU experiments running, but the app didn't have access to a WebGPU context. So one thing led to another, and I wrote a quick MCP server to exfiltrate values from notebooks running in the browser.

## Setup

### 1. Install the package

```bash
npm install @rreusser/mcp-observable-notebook-kit-debug
```

### 2. Add the Vite plugin

In your `vite.config.js`:

```js
import { defineConfig } from "vite";
import { observable, config } from "@observablehq/notebook-kit/vite";
import { debugNotebook } from "@rreusser/mcp-observable-notebook-kit-debug";

export default defineConfig({
  ...config(),
  plugins: [debugNotebook(), observable()],
});
```

### 3. Run the dev server

```bash
vite -c vite.config.js
```

### 4. Configure the MCP server

Add to your `.mcp.json`:

```json
{
  "mcpServers": {
    "Notebook": {
      "command": "mcp-notebook-kit-debug",
      "args": []
    }
  }
}
```

Note that you might need an absolute path to the mcp-notebook-kit-debug bin, and you might be using opencode or some other tool, so this step could vary a bit.

### 5. Go!

You can now use an agent like Claude Code to poke and prod at notebooks running in a web browser, inspecting values and even capturing canvas output as images.

## MCP Tools

### Observable Runtime Tools

These tools interact directly with the Observable runtime's reactive graph.

| Tool                      | Description                                                                 |
| ------------------------- | --------------------------------------------------------------------------- |
| `ListValues`              | List all named values in the Observable runtime's reactive graph            |
| `GetValue`                | Get a value from the runtime by name; returns images for Canvas/SVG elements |
| `GetValues`               | Get multiple values at once (snapshot of runtime state)                     |
| `GetValueMetadata`        | Get metadata: state, type, dependencies (inputs), and dependents (outputs)  |
| `GetDependencyGraph`      | Get the dependency graph showing how values depend on each other            |
| `SetInputValue`           | Set an input widget's `.value` property and trigger reactive updates        |
| `RuntimeEval`             | Evaluate an expression with access to all notebook variables                |

### Browser Tools

These tools interact with the browser/DOM context, outside the Observable runtime.

| Tool                      | Description                                                                 |
| ------------------------- | --------------------------------------------------------------------------- |
| `BrowserEval`             | Execute JavaScript in the browser (has DOM access but NOT Observable runtime) |
| `GetElementContent`       | Get content from a DOM element by CSS selector; captures canvas/SVG as images |
| `MouseClick`              | Simulate a mouse click at a position or on an element                       |
| `MouseDrag`               | Simulate a mouse drag from start to end position                            |
| `MouseHover`              | Simulate mouse hover at a position, triggering hover states and tooltips    |
| `MouseWheel`              | Simulate a mouse wheel scroll at a position                                 |
| `SendKeys`                | Simulate keyboard input to an element                                       |

### Session Tools

These tools manage notebook connections and debug sessions.

| Tool                      | Description                                                                 |
| ------------------------- | --------------------------------------------------------------------------- |
| `ListNotebooks`           | List all connected notebooks (use when multiple notebooks are open)         |
| `FocusNotebook`           | Set a default notebook for subsequent commands (when multiple are connected) |
| `Refresh`                 | Refresh the page and wait for notebook initialization                       |
| `Navigate`                | Navigate a notebook to a different URL (e.g., switch from notebook-a to notebook-b) |
| `GetConsoleMessages`      | View console logs from the current session                                  |
| `GetErrors`               | Get all errors (DOM-reported and values in rejected state)                  |

### Multi-Notebook Support

When multiple notebooks are open in different browser tabs, you can target a specific notebook using the `notebook` parameter on any tool:

```
# By path (without .html extension)
notebook: "index"
notebook: "second-notebook"

# By index
notebook: "0"
notebook: "1"

# By URL
notebook: "http://localhost:5173/"
```

If multiple notebooks are connected and you don't specify which one, the tool will return an error listing the available notebooks.

Use `ListNotebooks` to see all connected notebooks with their URLs and indices.

### Navigating Between Notebooks

The `Navigate` tool allows you to navigate an open notebook to a different URL, or open a URL in your default browser if no notebooks are connected:

```javascript
// If no notebooks are connected, opens URL in default browser
Navigate({ url: "http://localhost:5173/index.html" })

// Navigate to a different notebook (relative URL)
Navigate({ url: "/second-notebook.html" })

// Navigate using absolute URL
Navigate({ url: "http://localhost:5173/second-notebook.html" })

// Navigate a specific notebook (when multiple are open)
Navigate({ url: "/second-notebook.html", notebook: "index" })

// Skip waiting for page load (useful if target page doesn't have debug plugin)
Navigate({ url: "/some-page.html", wait_for_completion: false })
```

**Behavior:**
- **No notebooks connected**: Opens the URL in your OS default browser
- **One notebook connected**: Navigates that notebook's browser window to the new URL
- **Multiple notebooks connected**: 
  - Navigates the focused notebook (if you've used `FocusNotebook`)
  - Navigates the specified notebook (if `notebook` parameter provided)
  - Returns an error listing all notebooks (if no focus is set and no notebook specified)

The tool waits for the new page to load and initialize, similar to `Refresh`, and reports any errors that occur during initialization.

### Value States

Values in an Observable notebook can be in one of three states:

- **fulfilled**: The value has been computed successfully
- **pending**: The value is still being computed (e.g., async/Promise)
- **rejected**: The computation threw an error

The `GetValue` and `GetValues` tools return state information along with the value or error.

### Image Support

`GetValue` automatically returns images inline for:
- **Canvas elements**: Captured as PNG
- **SVG elements**: Rendered to canvas and captured as PNG

No need to save to files - images are returned directly in the MCP response.

### Runtime Evaluation

Use `RuntimeEval` to evaluate expressions in the Observable runtime context with access to all notebook variables. The body must use a `return` statement.

```javascript
// Compute derived values from notebook variables
RuntimeEval({ body: "return number * 2 + rangeValue" })

// Filter or transform data
RuntimeEval({ body: "return data.filter(d => d.value > 0)" })

// Multi-statement expressions
RuntimeEval({
  body: `
    const doubled = number * 2;
    const added = doubled + rangeValue;
    return { doubled, added };
  `
})

// Persist the result as a named variable for later retrieval
RuntimeEval({
  name: "myResult",
  body: "return number * 2"
})
```

Dependencies are auto-detected from the expression. If `name` is provided, the result persists in the runtime and can be retrieved with `GetValue`. If `name` starts with `_tmp_`, it is automatically deleted after the value resolves.

### Setting Input Values

Use `SetInputValue` to programmatically change the value of interactive input widgets created with `Inputs.*` (e.g., `Inputs.range`, `Inputs.select`, `Inputs.text`). This sets the widget's `.value` property and dispatches an input event, triggering reactive updates to dependent values.

```javascript
// If the notebook has: slider = Inputs.range([0, 100])
SetInputValue({ name: "slider", value: 50 })

// If the notebook has: dropdown = Inputs.select(["A", "B", "C"])
SetInputValue({ name: "dropdown", value: "B" })
```

### Mouse Interaction

Simulate mouse events for testing interactive visualizations:

- **`MouseClick`**: Click at coordinates or on an element (supports left/middle/right buttons)
- **`MouseDrag`**: Drag from start to end position with configurable duration
- **`MouseHover`**: Hover at a position, dispatching mouseenter/mouseover/mousemove events
- **`MouseWheel`**: Scroll at a position with deltaX/deltaY

All mouse tools accept an optional `selector` parameter to target a specific element, with coordinates relative to that element.

### Keyboard Interaction

Use `SendKeys` to simulate keyboard input:

```javascript
// Type plain text
SendKeys({ keys: "hello world" })

// Use special keys with braces
SendKeys({ keys: "{Enter}" })
SendKeys({ keys: "{Tab}" })
SendKeys({ keys: "{ArrowDown}" })

// Modifier combinations
SendKeys({ keys: "{Ctrl+a}" })  // Select all
SendKeys({ keys: "{Ctrl+c}" })  // Copy
SendKeys({ keys: "{Shift+Tab}" })  // Reverse tab

// Target a specific element
SendKeys({ selector: "#my-input", keys: "typed text{Enter}" })

// Hold modifiers for all keys
SendKeys({ keys: "abc", modifiers: { shiftKey: true } })  // Types "ABC"
```

Supported special keys: `{Enter}`, `{Tab}`, `{Escape}`, `{Esc}`, `{Backspace}`, `{Delete}`, `{Insert}`, `{Space}`, `{ArrowUp}`, `{ArrowDown}`, `{ArrowLeft}`, `{ArrowRight}`, `{Home}`, `{End}`, `{PageUp}`, `{PageDown}`, `{F1}`-`{F12}`.

## License

© 2026 Ricky Reusser. MIT License.

TDQS

A3.6/5.0

Scored across 20 tools

Disambiguation4/5

Tools are largely purpose-specific: value retrieval is split into single/bulk/metadata/list, and evaluation is split into browser vs runtime contexts. A few boundaries require care—GetErrors vs GetConsoleMessages both surface errors, and GetValues with no names overlaps with ListValues—but descriptions clarify intended usage.

Naming Consistency4/5

Most tools follow a PascalCase VerbNoun pattern (ListNotebooks, GetValue, SetInputValue, SendKeys). Exceptions break the pattern: Refresh and Navigate are bare verbs, while BrowserEval/RuntimeEval and MouseClick/Drag/Wheel/Hover put the context or object before the action, making the convention less predictable.

Tool Count3/5

20 tools is at the high end for a debugger and feels slightly heavy, with multiple value-retrieval variants and five input-simulation tools. The breadth is defensible because the server covers runtime state, DOM inspection, and mouse/keyboard interaction, but it is borderline rather than clearly well-scoped.

Completeness4/5

The server covers the main notebook debugging loop: discover/connect notebooks, inspect runtime values and dependencies, read console/errors, manipulate inputs, and simulate interactions. Obvious gaps such as a dedicated screenshot or wait-for-condition tool are workaroundable via BrowserEval/RuntimeEval, but they are minor omissions.

Maintenance

ActivityInactive
ResponsivenessNo issues