
Company News
Free Business Plan Upgrades for Open Source Maintainers
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.
@mario.andreschak/mcp-vscode
Advanced tools
A self-hosted VS Code workbench embedded as an interactive MCP App.
mcp-vscode-mcpapp in FLUJO
mcp-vscode-mcpapp in Goose
MCP VS Code embeds a self-hosted Code OSS/OpenVSCode workbench inside an MCP App. The model and the human operate the same workspace, open editors, diagnostics, commands, and terminal sessions in real time.
This is not remote control of a separately installed VS Code. The standalone distribution contains the editor server, Node.js runtime, MCP server, bridge extension, and web UI. Microsoft-hosted vscode.dev is not used because it disallows framing.
Project status: functional v0.2 implementation. Prerequisite-free releases target Windows x64, Linux x64, and Linux ARM64. Linux packages use verified upstream OpenVSCode archives; the Windows package is built in Windows CI from the pinned OpenVSCode source commit and exercised against the real workbench and bridge.
flowchart LR
H["MCP host"] <-->|"stdio or Streamable HTTPS"| S["MCP VS Code server"]
H --> A["Sandboxed MCP App view"]
A -->|"nested iframe on an allowed frameDomain"| V["Bundled OpenVSCode workbench"]
S --> W["Confined workspace"]
S --> T["Shared PTYs"]
V <-->|"authenticated local WebSocket"| B["Bridge extension"]
B <--> S
V --> W
B --> T
The OpenVSCode process binds only to loopback. A gateway exposes it under a random, high-entropy path, avoiding third-party-cookie authentication inside the MCP sandbox. Remote MCP deployments must use TLS and bearer authentication.
MCP VS Code automatically picks the best rendering strategy the host allows, in a fixed order, with nothing to configure: no CLI flag, no environment variable, no tool input, and no persisted preference influences the choice. The probe (src/app/tier.ts) is driven only by the live sessionPayload() (gatewayOrigin, ideUrl, uiToken, assetsUrl, openVscode.state) and the DOM, re-run for every session.
native — tried first. Requires both the authenticated /ui WebSocket to open and the static asset manifest at /assets/manifest.json to be fetchable (a 5 s budget). If both succeed, Monaco (editor) and xterm (terminal) render directly inside this document, talking to the gateway over /ui. This tier needs only connectDomains and resourceDomains — it works even in hosts (Claude Desktop, today) that silently drop frameDomains, because it never asks to frame anything.embedded — tried only if native fails. The app points a same-document iframe at the OpenVSCode workbench URL and waits (6 s, extended while the runtime reports state === "starting") for a postMessage liveness marker (mcp-vscode:workbench-alive) injected into the proxied workbench HTML. A CSP-blocked frame fires neither load nor error, so only that positive message counts as success; a timeout is treated as "blocked", never as "still loading". On success, the real OpenVSCode workbench is shown in the iframe.browser — the terminal fallback, always reachable. If neither of the above committed, the app renders an "open in your browser" card with app.openLink({ url: ideUrl }), a selectable URL, and a Retry button that re-runs the whole probe from native.Correction to a common misreading of the underlying issue: the order is native → embedded → browser, and it is fixed — never reordered or skipped by configuration. In particular, a host blocking iframe framing does not, by itself, cause native to be selected: native is decided first, before framing is ever probed, purely from the /ui socket and /assets bundle. A host that blocks framing but serves those two lands on native regardless of framing. Framing is only probed once native has already failed, and blocked framing at that point (with no native surface available) is what produces browser — not native.
| Capability | native | embedded | browser |
|---|---|---|---|
| Browse the file tree, open/edit/save files | ✅ Monaco over /ui (falls back to callServerTool if the socket is refused) | ✅ real workbench | ✅ real workbench, separate tab |
| Terminal | ✅ xterm over /ui (degrades to polled reads if the socket is refused) | ✅ | ✅ |
| Webviews, extension UI (custom panels), Markdown preview | ❌ | ✅ | ✅ |
Webviews, extension UI, and Markdown preview exist only in embedded and browser. Both load the real OpenVSCode workbench, whose webview host relies on a service worker that must be same-origin with the sandbox/browser document it runs inside. native renders Monaco/xterm directly in the MCP App's own document, so there is no second same-origin document for that service worker to attach to — this is a structural limit of the tier, not a missing feature, and it is out of scope for this project to change (see issue #10 §9).
There is no CLI flag, environment variable, tool input, or persisted setting that selects or biases the tier — only the capabilities the host has actually granted (probed live, every session) and the DOM. Re-probing from native happens automatically when the gateway origin changes (for example after a restart on a new ephemeral port), when the /ui transport gives up after repeated reconnect failures, or when the user presses Retry on the browser card.
Upstream OpenVSCode Server publishes no darwin server build (see "Run from npm" below), so the embedded and browser tiers — which both need a real OpenVSCode workbench to point at — are unavailable on macOS. Because native never depends on OpenVSCode being installed or running (it only needs the gateway's own /ui socket and /assets bundle, both served directly by the MCP VS Code process), the app still reaches native automatically on macOS, with no switch, flag, or workaround required.
The same HTTP(S) server used for /mcp also exposes:
| Route | Auth | Purpose |
|---|---|---|
GET /healthz | none | Liveness probe. |
GET /session.json | ?token= (if --auth-token set) | JSON session payload (workspace, OpenVSCode, bridge, uiToken, assetsUrl). |
GET /app | ?token= (if --auth-token set) | The MCP App HTML shell. |
ALL /mcp | Authorization: Bearer (if --auth-token set) | Streamable HTTP MCP transport. |
GET /assets/* | none (public, read-only, static bundle) | Monaco/xterm/UI bundle for a future native renderer. Reachable even before OpenVSCode finishes starting. |
GET /ide/<random>/... | none beyond the unguessable path | Proxied OpenVSCode workbench. |
WS /bridge | first-message { type: "hello", token } | The VS Code bridge extension's JSON-RPC channel. |
WS /ui | ?token= query parameter | Direct JSON-RPC channel (same framing as /bridge) for workspace/terminal/editor operations, bypassing the VS Code extension. Loopback-only, single client, reuses the same bridgeToken — see SECURITY.md. |
uiToken in the session payload is currently identical to the bridge token (one shared secret, zero new configuration, per the design in issue #6). assetsUrl is ${gatewayOrigin}/assets.
vscode_execute_command escape hatch for every command registered in the live workbench.The server currently exposes 27 tools across these groups:
| Group | Tools |
|---|---|
| App/session | vscode_open, workspace_status |
| Files | fs_list, fs_read, fs_write, fs_delete, fs_move, fs_search |
| Editor | editor_open, editor_state, editor_set_selection, editor_apply_edits |
| Language services | diagnostics_get |
| Commands | vscode_list_commands, vscode_execute_command |
| Extensions | extensions_list, extensions_install, extensions_uninstall |
| Terminals | terminal_create, terminal_list, terminal_read, terminal_write, terminal_resize, terminal_kill |
| Git | git_status, git_diff, git_run |
Destructive and open-world tools are annotated accordingly so compatible MCP hosts can apply their approval policy.
Download and extract the release archive for your platform.
Windows x64:
.\bin\mcp-vscode.cmd --stdio --workspace C:\path\to\repository
Linux x64 or ARM64:
./bin/mcp-vscode --stdio --workspace /absolute/path/to/repository
The archive contains its own Node.js and OpenVSCode runtimes. It does not require VS Code, Node.js, Docker, or a system-wide package installation.
With Node.js 22 or newer, npx starts the bundled stdio server on Windows x64, Linux x64, and Linux ARM64:
npx -y @mario.andreschak/mcp-vscode@0.2.1 --stdio --workspace "C:\path\to\repository"
npx -y @mario.andreschak/mcp-vscode@0.2.1 --stdio --workspace "/path/to/repository"
For MCP clients that configure the workspace through an environment variable:
{
"mcpServers": {
"vscode": {
"command": "npx",
"args": ["-y", "@mario.andreschak/mcp-vscode@0.2.1", "--stdio"],
"env": {
"MCP_VSCODE_WORKSPACE": "C:\\path\\to\\repository"
}
}
}
}
Pin the workspace explicitly. With neither --workspace nor MCP_VSCODE_WORKSPACE, the server falls back to the working directory it was spawned in and reports that fallback on stderr. Hosts that spawn MCP servers from a mounted volume root (for example /data on Fly.io) would otherwise expose that whole volume as the workspace, including root-owned entries such as lost+found. Such entries are ignored, and a directory the server may not read no longer aborts startup, but an explicit workspace keeps the file watcher scoped to the repository you meant.
@mario.andreschak/mcp-vscode itself contains no editor runtime. It declares one optionalDependencies entry per supported platform — @mario.andreschak/mcp-vscode-win32-x64, -linux-x64, and -linux-arm64 — each gated by os/cpu, so npm downloads only the OpenVSCode runtime matching the host. macOS is not supported: upstream publishes no darwin server build.
Example MCP client configuration:
{
"mcpServers": {
"vscode": {
"command": "/opt/mcp-vscode/bin/mcp-vscode",
"args": ["--stdio", "--workspace", "/work/my-repository"]
}
}
}
Calling vscode_open renders the workbench. The app requests fullscreen mode when the user selects Fullscreen.
./bin/mcp-vscode \
--http \
--https \
--host 0.0.0.0 \
--port 8443 \
--workspace /work/my-repository \
--public-url https://editor.example.com:8443 \
--auth-token "$MCP_VSCODE_TOKEN" \
--cert /run/secrets/tls.crt \
--key /run/secrets/tls.key
The MCP endpoint is https://editor.example.com:8443/mcp. Binding beyond loopback is rejected unless both TLS and a bearer token are configured.
The host must support the stable MCP Apps extension io.modelcontextprotocol/ui and:
text/html;profile=mcp-app resources;frameDomains, connectDomains, and resourceDomains;If a host blocks the OpenVSCode frame, the app displays the exact runtime or policy error instead of silently opening an external browser tab.
Requirements for development only: Node.js 22+.
npm ci
npm run check
Useful commands:
npm run dev -- --http --port 3001 --workspace . --ide-url http://127.0.0.1:3999
npm run dev:mock-ide -- --port 3999
npm run runtime:fetch -- linux-x64
npm run runtime:build -- win32-x64
npm run package:standalone -- linux-x64
npm run package:standalone -- win32-x64
npm run npm:platform-package -- linux-x64
npm run npm:platform-package -- <target> stages the publishable runtime package for one platform in platform-packages/<target>/, using whichever runtime is currently installed in runtime/. It refuses to run when runtime/openvscode-runtime.json reports a different target, so a Linux runtime can never be published under the Windows package. npm run npm:prepare-manifest stages the platform-neutral dispatcher manifest that pins those packages, and npm run npm:verify-runtime asserts the dispatcher stays free of os/cpu gates and of the bundled runtime directory.
The runtime fetcher pins OpenVSCode 1.109.5 and verifies upstream Linux SHA-256 digests before extraction. Native Windows builds pin and verify upstream commit 4ffe2270acdf711bbefecc3e8c79f4b3631640e5, then invoke Code OSS's vscode-reh-web-win32-x64 build target. Building that runtime locally requires Windows x64, Git, Node.js 22.21.1 or newer, and the Visual Studio 2022 C++ build tools with Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre. Release archives contain the resulting runtime and do not require those development tools. Runtime and standalone output directories are ignored by Git.
Release CI publishes automatically once npm trusted publishing authorizes this repository's release.yml workflow and the GitHub npm-publish environment is configured. To publish by hand instead — for example when the account requires an interactive passkey — download the *.npm.tgz assets and their .sha256 sidecars from the GitHub Release into release-artifacts-v<version>/, then run:
npm run npm:publish -- release-artifacts-v0.2.1 --dry-run # verify only, upload nothing
npm run npm:publish -- release-artifacts-v0.2.1 # publish
Authentication happens in the same terminal: when no npm session exists, the publish script hands the terminal to npm login --auth-type=web, which prints a URL and opens the browser for passkey / WebAuthn sign-in, then resumes publishing once the session is stored. Nothing else is required.
npm run npm:whoami # check the current identity
npm run npm:login # sign in ahead of time (optional)
npm run npm:publish:wait -- release-artifacts-v0.2.1 # don't log in here; poll for a login from another terminal
npm run npm:publish -- release-artifacts-v0.2.1 --no-login # fail fast when no session exists (CI / token auth)
npm run npm:publish verifies every tarball before asking for credentials, so a bad artifact set fails before any browser opens. It publishes the three runtime packages before the dispatcher that pins them, skips versions already on the registry so an interrupted run can simply be re-run, and refuses to upload a tarball whose internal package.json disagrees with the name and version its filename claims. It never builds a tarball: the published bytes are exactly the audited release artifacts.
The MCP Registry stores metadata only, so the npm packages must be live before this step:
npm run mcp:validate # check server.json + npm state, download nothing else, publish nothing
npm run mcp:publish # log in if needed, then publish server.json
npm run mcp:publish (scripts/publish-mcp.mjs) works through the quickstart steps in order, and refuses to continue if any of them is off:
server.json and package.json must agree — mcpName versus name, and one shared version across package.json, server.json and its npm package entry.@mario.andreschak/mcp-vscode@<version> must already be on npm, and the published package must declare the matching mcpName. That mismatch is what produces the registry's "Registry validation failed for package", so it is checked against the registry copy rather than the working tree.io.github.<user>/, which is verified before a browser opens.io.github.<authorized-account>/*). That claim is compared with server.json's name before the upload, because the device flow silently authorizes whichever account your browser happens to be signed in as — publishing io.github.mario-andreschak/... with, say, the flujo-app account can only ever return 403, and no amount of re-authenticating fixes it.--force to attempt the upload anyway.mcp-publisher 1.8.0 binary is downloaded into .tools/ (git-ignored) and verified against a recorded SHA-256 digest — no Homebrew, curl | tar pipeline or Go toolchain needed. Set MCP_PUBLISHER_BIN to use your own build.mcp-publisher validate runs first, so a malformed server.json fails before authentication.Authentication again happens in the same terminal: the GitHub device-code flow prints a URL and a code, and publishing continues automatically once you approve it. A saved registry token is reused while it is still valid (they live only ~5 minutes), and an expired token triggers one silent re-login and retry. A permission failure never does — it aborts with the authorized identity and its granted namespaces, so the script can no longer appear to hang at Waiting for authorization... behind a second device code nobody was told to enter.
To publish as a specific account without fighting the browser session, hand the flow a personal access token (scopes read:user, read:org):
$env:MCP_GITHUB_TOKEN = "<pat of the namespace owner>"; npm run mcp:publish
npm run mcp:publish -- --login github-oidc # GitHub Actions OIDC
npm run mcp:publish -- --login dns --domain example.com --private-key <hex>
npm run mcp:publish -- --relogin # force a fresh login
npm run mcp:publish -- --token <github-pat> # skip the device flow, publish as that account
npm run mcp:publish -- --no-login # require an existing token, never prompt
npm run mcp:publish -- --registry http://localhost:8080 # publish against a local registry
Verify a publish with curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.mario-andreschak/mcp-vscode".
git_run; arbitrary VS Code commands and shell input remain powerful and should require host approval.See SECURITY.md for reporting and deployment guidance.
OpenVSCode Server and Code OSS are MIT-licensed upstream projects. MCP VS Code is independent and is not affiliated with or endorsed by Microsoft or Gitpod. “Visual Studio Code” and “VS Code” are trademarks of Microsoft Corporation.
MIT. See LICENSE and THIRD_PARTY_NOTICES.md.
FAQs
A self-hosted VS Code workbench embedded as an interactive MCP App.
The npm package @mario.andreschak/mcp-vscode receives a total of 399 weekly downloads. As such, @mario.andreschak/mcp-vscode popularity was classified as not popular.
We found that @mario.andreschak/mcp-vscode demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.