vscode-extension-devtool-mcp
Provides Express middleware (createDevtool, devtoolMiddleware) for embedding the devtool server in an Express host application, exposing backend diagnostics, store tools, and MCP connectivity.
Provides a MobX store adapter (createMobxAdapter) that activates MobX-specific MCP tools for state-store snapshots, history, change events, and actions.
Provides a React <Devtool> component and <DevtoolProvider> for embedding the devtool in a React webview, letting the MCP agent inspect the running webview's DOM, console, state stores, and performance.
Provides a Redux store adapter that activates Redux-specific MCP tools for state-store snapshots, history, change events, and actions.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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-mcpThe 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 |
| Node |
|
| Browser |
|
| Node (stdio) | The multi-target MCP hub used by the agent |
| Node |
|
| Node |
|
| Node | Low-level backend surface (provider helpers, transports, hub) |
| Node | Registry + |
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/frontendAnd 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 toolsAny 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
targetargument;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/surfaceargument — 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
DevtoolServerinstances);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 asconnector).Tool results also carry the same data in MCP
structuredContent.Non-JSON text gets a
[target: label (id)]prefix.list_targetslists all backends;list_frontend_surfaceslists all frontends of one target.When several targets run and no
targetis given, the hub returns an error listing them instead of guessing.
Ports / registry
Environment variable | Default | Purpose |
| OS temp dir | Shared directory of target records |
|
| Set |
| generated | Stable target id |
| workspace/pid | Label shown to the agent |
|
|
|
| — | Extra comma-separated |
| — | 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/wsStore adapters
Framework | Adapter |
MobX-State-Tree |
|
Redux |
|
Zustand |
|
Apollo |
|
Anything else |
|
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_infoFrontend —
find_element,click_element,type_text,scroll_element,select_option,drag_element,drag_from_to,run_command,get_screenshot,get_active_page,list_frontend_surfacesState —
get_store_state,search_state,get_store_actions,filter_actions,search_actions,count_actions,get_store_actions_logHub —
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 hubLicense
Apache-2.0 — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Live browser debugging for AI assistants — DOM, console, network via MCP.
Debug webhooks from your AI agent: inspect and replay captured webhooks on localhost.
Capture, inspect, send and assert on webhooks and inbound email from your coding agent.
Cloud browser, web extraction, screenshots, and remote CDP tools for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables 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 npm8MIT
- AlicenseNot gradedqualityDmaintenanceStreams browser DevTools console, network, storage, and performance data to your IDE's AI agent for real-time debugging and analysis.11 npm1MIT
- AlicenseNot gradedqualityAmaintenanceExposes 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.1MIT
- AlicenseBqualityBmaintenanceEnables 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.24MIT