Skip to main content
Glama
hansschenker

rxjs-spy-mcp

by hansschenker
README.md
# rxjs-spy-mcp

Experimental **RxJS runtime debugging** prototype for **Chrome DevTools MCP**.

This repository modernizes the idea behind Nicholas Jamieson's `rxjs-spy` for an AI-assisted debugging workflow: RxJS runtime events are captured into a structured heap registry, exposed through a small debug API, and made readable by Chrome DevTools MCP agents.

## Main contributor note

**ChatGPT is the main contributor to this project.**

The architecture, TypeScript starter implementation, RxJS custom debug operators, MVU demo, time-travel heap registry, and Chrome DevTools MCP bridge were generated and refined with ChatGPT from the user's RxJS debugging requirements.

## Project goal

The goal is not to replace `rxjs-spy` completely yet. This is a first experimental implementation of an MCP-friendly RxJS debugging model.

The current prototype focuses on:

- typed RxJS debug operators
- an Elm-like MVU demo
- time-travel state history
- passive heap-based inspection
- safe snapshotting and redaction
- Chrome DevTools third-party tool discovery
- AI-readable debug frames

The long-term direction is a modern `rxjs-spy-mcp` runtime that can inspect:

- tagged streams
- notifications: `next`, `error`, `complete`
- subscriptions and unsubscriptions
- MVU transitions: `Msg -> Model`
- inner subscription behavior from `switchMap`, `mergeMap`, `concatMap`, `exhaustMap`
- scheduler-aware timing traces

## Mental model

```text
Observable      = static dataflow description
Subscription    = runtime execution
Notification    = runtime event: next | error | complete
Scheduler       = runtime time policy
Heap registry   = durable debug memory
Chrome MCP      = AI-readable inspection bridge
```

The debugger turns fast asynchronous RxJS events into durable debug frames:

```text
Msg / next / error / complete / unsubscribe
        ↓
spyOnHeap / spyOnMvuLoop
        ↓
window.__RXJS_SPY_MCP__
        ↓
Chrome DevTools MCP / console / debug panel
```

## Installation

```bash
npm install
```

## Run the demo

```bash
npm run dev
```

Open the local Vite URL printed in the terminal, usually:

```text
http://127.0.0.1:5173
```

## Sample: using the debug feature manually

1. Start the app with `npm run dev`.
2. Open the browser DevTools console.
3. The app should already have recorded an initial `INIT` transition.
4. Inspect the tracked streams:

```js
window.__RXJS_SPY_MCP__.listStreams()
```

5. Inspect the main MVU state stream:

```js
window.__RXJS_SPY_MCP__.inspectStream('main-app-state')
```

6. Read only the timeline frames:

```js
window.__RXJS_SPY_MCP__.getTimeline('main-app-state', 10)
```

7. Read the compact runtime story:

```js
window.__RXJS_SPY_MCP__.story('main-app-state', 20)
```

As a table:

```js
console.table(window.__RXJS_SPY_MCP__.story('main-app-state', 20))
```

The story output turns raw debug frames into rows like:

```text
INIT -> query="", active="", loading=false, results=0
SET_QUERY -> query="rxjs", active="", loading=false, results=0
START_SEARCH -> query="rxjs", active="rxjs", loading=true, results=0
SEARCH_SUCCESS -> query="rxjs", active="rxjs", loading=false, results=3
```

8. Type a search query, for example `rxjs`, and press **Search**.
9. Simulate a failing async effect by typing:

```text
error
```

Then press **Search**.

10. Inspect the story again:

```js
console.table(window.__RXJS_SPY_MCP__.story('main-app-state', 20))
```

You should see a sequence similar to:

```text
INIT
SET_QUERY
START_SEARCH
SEARCH_FAILURE
```

Each `mvu-transition` frame stores:

```ts
{
  action: Msg,
  resultingState: Model
}
```

This gives a readable runtime story:

```text
The user changed the query.
A search request started.
The async effect failed.
The model moved into an error state.
The view rendered the error.
```

## If `getTimeline('main-app-state', 20)` returns `[]`

Run this first:

```js
window.__RXJS_SPY_MCP__.diagnose()
```

Then run:

```js
window.__RXJS_SPY_MCP__.listStreams()
```

Expected after a fresh page load:

```text
streamCount >= 1
streamTags includes "main-app-state"
mainStateHistorySize >= 1
```

Also check that you use the exact global name with two underscores before and after `RXJS_SPY_MCP`:

```js
window.__RXJS_SPY_MCP__
```

not:

```js
window._RXJS_SPY_MCP_
```

If the timeline is still empty:

```bash
git pull
npm install
npm run dev
```

Then hard-refresh the browser tab and run:

```js
window.__RXJS_SPY_MCP__.diagnose()
window.__RXJS_SPY_MCP__.getTimeline('main-app-state', 20)
```

The current implementation uses a seeded `BehaviorSubject<Msg>` for the MVU message source, so an `INIT` transition should be recorded immediately when `runtime.appState$` is subscribed in `main.ts`.

## Sample: visual time-travel

The debug panel on the right shows the heap timeline.

Click any frame to visually rewind the UI to the `resultingState` stored in that frame.

You can also jump from the DevTools console:

```js
window.jumpToStep(2)
```

Important: this is currently **visual rewind**, not full replay-based state restoration. The internal `scan` accumulator is not rewound. A future version can add true event replay.

## Sample: using the custom operators

### Generic stream inspection

```ts
import { interval, map, take } from 'rxjs';
import { spyOnHeap } from './debug/operators';

const counter$ = interval(1000).pipe(
  take(5),
  map(n => ({ count: n })),
  spyOnHeap('counter-stream', { maxFrames: 10 })
);

counter$.subscribe();
```

Then inspect it in the console:

```js
window.__RXJS_SPY_MCP__.inspectStream('counter-stream')
```

### MVU transition inspection

```ts
const msg$ = new BehaviorSubject<Msg>({ type: 'INIT' });

const transition$ = msg$.pipe(
  scan(
    (acc, msg) => ({ msg, model: update(acc.model, msg) }),
    { msg: { type: 'INIT' }, model: initialModel }
  ),
  spyOnMvuLoop('main-app-state', { maxFrames: 80 })
);
```

This is the key teaching/debugging use case:

```text
Msg flows in over time.
update calculates the next Model.
spyOnMvuLoop stores Msg + Model as a debug frame.
```

## Sample: Chrome DevTools MCP workflow

This project registers a Chrome DevTools third-party developer tools bridge through the page-level `devtoolstooldiscovery` event.

When Chrome DevTools MCP is connected with the experimental third-party tools category enabled, an AI agent can discover tools such as:

```text
rxjs_list_streams
rxjs_inspect_stream
rxjs_get_timeline
rxjs_story
```

A typical AI-agent prompt:

```text
Inspect the active browser tab with Chrome DevTools MCP. Use the rxjs-spy-mcp tools to list RxJS streams, read the main-app-state story, and explain why the latest search failed.
```

Expected agent behavior:

```text
1. list_3p_developer_tools
2. execute_3p_developer_tool: rxjs_list_streams
3. execute_3p_developer_tool: rxjs_story { tag: 'main-app-state', limit: 20 }
4. Explain the Msg -> Model transition that caused the bad state.
```

A fallback MCP approach is script evaluation:

```js
() => globalThis.__RXJS_SPY_MCP__.story('main-app-state', 20)
```

## Corrections applied to the original prototype

| Dimension | Correction applied |
|---|---|
| Concept | Reframed MCP as an inspection bridge, not a replacement for RxJS runtime instrumentation. |
| MVU time-travel teaching value | Added explicit `Msg -> Model` transition tracking and a visual timeline panel. |
| TypeScript correctness | Split app and debug types, fixed invalid imports, removed `any`-based `INITIALIZE`, added strict typed operators. |
| Chrome MCP API correctness | Replaced the invented `navigator.developerTools.registerTool` idea with a `devtoolstooldiscovery` bridge for Chrome DevTools third-party tools. |
| rxjs-spy replacement completeness | Added a foundation for tagged streams, notification frames, subscription IDs, teardown tracking, and stream summaries. Still not a full rxjs-spy replacement. |
| AI-agent usability | Added JSON-friendly `diagnose`, `listStreams`, `inspectStream`, `getTimeline`, and `story` methods. |
| Production safety | Dev-only installation, redaction for secret-like keys, safe snapshot serialization, circular-value tolerance, and size-limited snapshots. |

## Current limitations

This is an experimental prototype. It does not yet implement full rxjs-spy behavior.

Missing or future work:

- monkey-patch-free tagging API comparable to `rxjs-spy` tags
- global Observable subscription graph
- parent/child subscription graph
- higher-order operator visualization
- dedicated debug operators for `switchMap`, `mergeMap`, `concatMap`, `exhaustMap`
- scheduler-aware traces for `asyncScheduler`, `animationFrameScheduler`, virtual time, and drift
- true replay-based time travel
- tests
- package publishing

## Safety notes

The debug registry exposes runtime state on `window.__RXJS_SPY_MCP__` in development mode. Do not expose sensitive production data through debug streams.

The snapshot layer redacts common secret-like keys and limits serialized payload size, but this is not a complete security boundary.

## License

MIT