Skip to main content
Glama
vscode-extension-devtool-mcp

vscode-extension-devtool-mcp

by DrBoria

vscode-extension-devtool-mcp

Give an AI agent live eyes and hands inside your running app.

vscode-extension-devtool-mcp is a small, standalone devtool bridge. Your app (or VS Code extension) publishes itself as a target; a single MCP entry then lets the agent inspect DOM, console logs, state stores, screenshots and diagnostics — and click/type/scroll in the real UI.

  • Host-neutral — VS Code webviews, Express, Nest, plain Node, any browser.

  • Store-agnostic — MobX-State-Tree, Redux, Zustand, Apollo, or your own adapter.

  • Any number of targets — 1 backend, 2 backends + 1 frontend, 3 backends + 4 frontends… all behind one MCP connection.

  • No host framework required — the package depends on nothing from your app.

Install

npm install vscode-extension-devtool-mcp

The package ships compiled JavaScript + type declarations (dist/); no TypeScript build step is required in the consumer.

Related MCP server: mcp-console-hub

Entry points

Import

Runtime

What you get

vscode-extension-devtool-mcp

Node

createDevtool, DevtoolServer, store adapters, diagnostics, hub + registry

vscode-extension-devtool-mcp/webview

Browser

DevtoolProvider, connectWebFrontend, DOM/store dispatch helpers

vscode-extension-devtool-mcp/mcp-entry

Node (stdio)

The multi-target MCP hub used by the agent

vscode-extension-devtool-mcp/express

Node

createDevtool, devtoolMiddleware

vscode-extension-devtool-mcp/nest

Node

createDevtool, DevtoolNestMiddleware

vscode-extension-devtool-mcp/backend

Node

Low-level backend surface (provider helpers, transports, hub)

vscode-extension-devtool-mcp/hub

Node

Registry + DevtoolHub on their own

Quick start — VS Code extension

Backend (extension host)

import * as vscode from "vscode"
import { createDevtool, createMessageBridgeProvider, createMobxAdapter } from "vscode-extension-devtool-mcp"

const panel = vscode.window.createWebviewPanel(/* … */)
const pending = (handler: (message: Record<string, unknown>) => void) => {
	const sub = panel.webview.onDidReceiveMessage(handler)
	return () => sub.dispose()
}

const provider = createMessageBridgeProvider({
	postMessageToWebview: (message) => void panel.webview.postMessage(message),
	onWebviewMessage: pending,
	listSurfaces: () => [{ id: "main", label: "Main panel" }],
	getExtensionVersion: () => vscode.extensions.getExtension("publisher.my-ext")?.packageJSON.version,
	executeCommand: (command, args) =>
		vscode.commands.executeCommand(command, ...(Array.isArray(args) ? args : [])),
})

const devtool = createDevtool({
	provider,
	config: {
		backend: { store: createMobxAdapter(rootStore) }, // optional
		port: 0, // 0 = pick a free port automatically
		statusPort: 0,
	},
	target: {
		label: vscode.workspace.name ?? "my-extension",
		kind: "vscode-extension",
	},
})

const { url, targetId } = await devtool.start()
console.log(`[devtool] ${targetId} → ${url}`)
// on deactivate: await devtool.stop()

Frontend (webview)

import { DevtoolProvider } from "vscode-extension-devtool-mcp/webview"

const vscode = acquireVsCodeApi()

export const App = () => (
	<DevtoolProvider
		postMessage={(message) => vscode.postMessage(message)}
		config={{ frontend: { framework: "mobx" } }}
		rootStore={frontendRootStore}
		surface="main">
		<YourApp />
	</DevtoolProvider>
)

surface is optional; use it when one extension has several panels so the agent can address each one.

Quick start — any web / Electron frontend

No VS Code required. Start a WebSocket frontend endpoint on the backend:

import { createDevtool, createWebFrontendServer, createMobxAdapter } from "vscode-extension-devtool-mcp"

const frontends = createWebFrontendServer({ port: 0 }) // 127.0.0.1 by default
const { url, port } = await frontends.start()

const devtool = createDevtool({
	provider: frontends.createProvider(), // ready-made DOM/provider wiring
	config: {
		frontend: { framework: "mobx" }, // the store lives in the browser
		port: 0,
		statusPort: 0,
	},
	frontendBridge: frontends,
	target: { label: "my-spa" },
})
await devtool.start()

console.log(`frontend endpoint: ${url}`) // ws://127.0.0.1:PORT/frontend

And in the browser bundle:

import { connectWebFrontend } from "vscode-extension-devtool-mcp/webview"

connectWebFrontend({
	url: `ws://127.0.0.1:${port}/frontend`,
	surface: "main",
	label: "Main tab",
	rootStore,                       // MST root: enables store tools
	// or: store: createReduxAdapter(store) for Redux/Zustand/Apollo
	getActionBuffer: () => [],       // optional: enables action tools
})

Open several tabs and give each a distinct surface — all of them attach to the same target and are individually addressable.

Quick start — Express / Nest

import { createDevtool, devtoolMiddleware } from "vscode-extension-devtool-mcp/express"

const devtool = createDevtool({
	provider: frontends.createProvider(),
	config: { frontend: { framework: "mobx" }, port: 0, statusPort: 0 },
	frontendBridge: frontends,
	target: { label: "api", kind: "express" },
})
await devtool.start()
app.use(devtoolMiddleware(devtool))

Nest exposes the same factory plus DevtoolNestMiddleware from vscode-extension-devtool-mcp/nest.

Configuration matrix

The three common setups. config is the single switch that decides which tool groups exist; declaring a side without a store still gives you its non-store tools (DOM, console, diagnostics, commands).

1. Backend only, with an MST store

No webview and no frontend transport is required — omit postMessageToWebview and onWebviewMessage:

import * as vscode from "vscode"
import { createDevtool, createMessageBridgeProvider, createMobxAdapter } from "vscode-extension-devtool-mcp"

const devtool = createDevtool({
	provider: createMessageBridgeProvider({
		getExtensionVersion: () => vscode.extensions.getExtension("publisher.my-ext")?.packageJSON.version,
		executeCommand: (command, args) =>
			vscode.commands.executeCommand(command, ...(Array.isArray(args) ? args : [])),
	}),
	config: {
		backend: { store: createMobxAdapter(backendRootStore) },
		port: 0, // ephemeral
		statusPort: 0,
	},
	target: { label: vscode.workspace.name ?? "my-extension", kind: "vscode-extension" },
})

await devtool.start()
// Frontend tools (click_element, …) now fail fast with
// "No frontend is configured for this target" instead of timing out.

If you do not even need command execution, you can omit the provider entirely:

const devtool = createDevtool({
	config: { backend: { store: createMobxAdapter(backendRootStore) }, port: 0, statusPort: 0 },
	target: { label: "my-extension" },
})

2. Backend + frontend, both with MST stores

Backend (extension host):

import * as vscode from "vscode"
import { createDevtool, createMessageBridgeProvider, createMobxAdapter } from "vscode-extension-devtool-mcp"

const webviews = new Map<string, vscode.Webview>() // surface id -> panel webview
const allWebviews = () => [...webviews.values()]

const provider = createMessageBridgeProvider({
	postMessageToWebview: (message, surface) => {
		const webview = surface ? webviews.get(surface) : allWebviews()[0]
		void webview?.postMessage(message)
	},
	onWebviewMessage: (handler) => {
		const subscriptions = allWebviews().map((webview) => webview.onDidReceiveMessage(handler))
		return () => subscriptions.forEach((subscription) => subscription.dispose())
	},
	listSurfaces: () => [...webviews.keys()].map((id) => ({ id, label: id })),
	getExtensionVersion: () => vscode.extensions.getExtension("publisher.my-ext")?.packageJSON.version,
	executeCommand: (command, args) =>
		vscode.commands.executeCommand(command, ...(Array.isArray(args) ? args : [])),
})

const devtool = createDevtool({
	provider,
	config: {
		backend: { store: createMobxAdapter(backendRootStore) },
		// The frontend store lives in the webview, so the backend only declares
		// its framework. The webview supplies the actual store below.
		frontend: { framework: "mobx" },
		port: 0,
		statusPort: 0,
	},
	target: { label: vscode.workspace.name ?? "my-extension", kind: "vscode-extension" },
})

await devtool.start()

Frontend (webview):

import { DevtoolProvider } from "vscode-extension-devtool-mcp/webview"

const vscode = acquireVsCodeApi()

export const App = () => (
	<DevtoolProvider
		postMessage={(message) => vscode.postMessage(message)}
		config={{ frontend: { framework: "mobx" } }}
		rootStore={frontendRootStore}
		surface="main">
		<YourApp />
	</DevtoolProvider>
)

3. Backend + frontend without MST (or without any store)

Declare the sides but do not pass a store. You still get DOM, console, diagnostics and command tools; store tools are not registered at all.

const devtool = createDevtool({
	provider,
	config: {
		backend: {},  // backend console/logs/commands, no state tools
		frontend: {}, // DOM/console tools, no state tools
		port: 0,
		statusPort: 0,
	},
	target: { label: "my-app" },
})

If the frontend uses Redux/Zustand/Apollo, declare the framework on the backend and pass the matching adapter on the frontend:

// backend
config: { backend: {}, frontend: { framework: "redux" }, port: 0, statusPort: 0 }

// webview
import { createReduxAdapter } from "vscode-extension-devtool-mcp"
<DevtoolProvider
	postMessage={(message) => vscode.postMessage(message)}
	config={{ frontend: { store: createReduxAdapter(reduxStore) } }}
	surface="main" />

For a plain web/Electron frontend the same choice is made at the connector:

connectWebFrontend({ url, surface: "main", rootStore })          // MST
connectWebFrontend({ url, surface: "main", store: reduxAdapter }) // any framework
connectWebFrontend({ url, surface: "main" })                     // no state tools

Any number of backends and frontends

Every devtool.start() publishes a record to a shared registry directory (default: <os tmp>/vscode-extension-devtool-mcp/targets). The MCP hub reads that directory, so targets are discovered automatically — one port per target, no configuration conflicts. If two processes ask for the same port, the second one transparently binds a free port.

              ┌──────────────────────────┐
              │  MCP client (agent)      │
              └────────────┬─────────────┘
                           │ stdio
              ┌────────────▼─────────────┐
              │  mcp-entry (DevtoolHub)  │
              └───┬──────────┬───────────┘
        ws://…:A  │          │  ws://…:B
        ┌─────────▼──┐   ┌───▼────────┐
        │ backend 1  │   │ backend 2  │
        │ frontend 1 │   │ frontend 2 │
        │ frontend 2 │   │ frontend 3 │
        └────────────┘   └────────────┘

The hub exposes:

  • every target tool, plus an optional target argument;

  • list_targets — every running target, its label, ports and capabilities;

  • select_target — set the default target for subsequent calls;

  • refresh_targets — re-scan after starting/stopping a target;

  • get_target_info — identity, ports and liveness of one target;

  • list_frontend_surfaces / surface argument — pick one of several frontends.

// tools/list excerpt
{ "name": "get_store_state", "inputSchema": { "properties": { "target": { "type": "string" } } } }

When only one target is running, target can be omitted. When several are running without a selection, the hub returns a clear error listing them instead of guessing.

Record attribution (2, 3, N backends/frontends)

A target is one backend process (one extension host, one server). A target can expose any number of frontend surfaces. So:

  • 3 backends → 3 targets (3 processes or 3 DevtoolServer instances);

  • 4 frontends → 4 surfaces, distributed across targets however you like.

Nothing is mixed together. Every proxied result is attributed:

// get_store_state { target: "app-a", env: "frontend", surface: "surface-app-a-side" }
{
	"stores": { "alphaSideFrontend": 20 },
	"target": { "id": "app-a", "label": "App A", "kind": "node", "backend": true, "frontend": true },
	"surface": "surface-app-a-side"
}
  • JSON results get target (id, label, kind, capabilities) and, for frontend tools, surface (the surface that answered, also mirrored as connector).

  • Tool results also carry the same data in MCP structuredContent.

  • Non-JSON text gets a [target: label (id)] prefix.

  • list_targets lists all backends; list_frontend_surfaces lists all frontends of one target.

  • When several targets run and no target is given, the hub returns an error listing them instead of guessing.

Ports / registry

Environment variable

Default

Purpose

DEVTOOL_REGISTRY_DIR

OS temp dir

Shared directory of target records

DEVTOOL_REGISTRY

1

Set 0 to stop publishing a target

DEVTOOL_TARGET_ID

generated

Stable target id

DEVTOOL_TARGET_LABEL

workspace/pid

Label shown to the agent

DEVTOOL_TARGET_KIND

other

vscode-extension, express, nest, node

DEVTOOL_TARGETS

—

Extra comma-separated ws://… URLs for the hub

DEVTOOL_TARGET

—

Default target id/label for the hub

The hub and the targets must see the same DEVTOOL_REGISTRY_DIR. If a target runs in a container or a different machine, pass its URL with DEVTOOL_TARGETS or --target ws://host:port/ws.

MCP client configuration

Point the agent at the hub entry (no build step, no fixed port):

{
	"mcpServers": {
		"devtool": {
			"command": "node",
			"args": ["./node_modules/vscode-extension-devtool-mcp/dist/mcp/transport/stdio.js"],
			"env": { "DEVTOOL_REGISTRY_DIR": "/tmp/vscode-extension-devtool-mcp/targets" }
		}
	}
}

or, for a single explicit target:

node ./node_modules/vscode-extension-devtool-mcp/dist/mcp/transport/stdio.js --target ws://127.0.0.1:60060/ws

Store adapters

Framework

Adapter

MobX-State-Tree

createMobxAdapter(rootStore)

Redux

createReduxAdapter(store)

Zustand

createZustandAdapter(useStore)

Apollo

createApolloAdapter(client)

Anything else

createPlainAdapter({ framework, getState })

Passing an adapter enables snapshot, search, action and history tools for that side. Without an adapter those tools are simply not registered — the agent never sees them. Custom adapters implement the small IStoreAdapter interface.

What the agent can do

  • Backend — get_logs, get_diagnostics_snapshot, clear_diagnostics, get_console, search_console, execute_vscode_command, get_extension_info

  • Frontend — find_element, click_element, type_text, scroll_element, select_option, drag_element, drag_from_to, run_command, get_screenshot, get_active_page, list_frontend_surfaces

  • State — get_store_state, search_state, get_store_actions, filter_actions, search_actions, count_actions, get_store_actions_log

  • Hub — list_targets, select_target, refresh_targets, get_target_info, get_renderer_console

See docs/architecture.md for the internal layout.

Development

npm install
npm run check-types
npm run lint
npm test              # unit tests + real-process integration tests (builds first)
npm run test:unit     # fast vitest suite in src/
npm run test:integration  # tests/: real stdio hub, real sockets, real jsdom DOM
npm run build         # emits dist/ (JS + .d.ts)
node examples/e2e-multi-target.mjs   # 2 backends + 2 frontends over one hub

License

Apache-2.0 — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to programmatically control and automate VSCode by interacting with its UI, executing commands, and inspecting the DOM structure. It supports advanced workflows like UI testing, extension development, and debugging through a standalone VSCode instance.
    9 npm
    8
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Streams browser DevTools console, network, storage, and performance data to your IDE's AI agent for real-time debugging and analysis.
    11 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Exposes the React DevTools and TanStack Query/Router state of a Chrome tab to coding agents as MCP tools, enabling inspection, profiling, and mutation of components, queries, and routes from any MCP client.
    1
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables AI coding agents to inspect and operate Chrome as a developer, including reading DevTools console/network data, evaluating JavaScript, and managing unpacked extensions during an edit-test-debug loop.
    24
    MIT