Skip to content

Commit 9581f89

Browse files
committed
feat: Add Microsoft Edge support
1 parent d082ca4 commit 9581f89

12 files changed

Lines changed: 1079 additions & 22 deletions

File tree

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,12 @@ The Chrome DevTools MCP server supports the following configuration option:
508508
- **Type:** string
509509
- **Choices:** `stable`, `canary`, `beta`, `dev`
510510

511+
- **`--browser`**
512+
Specify which browser to use. Defaults to Chrome.
513+
- **Type:** string
514+
- **Choices:** `chrome`, `edge`
515+
- **Default:** `chrome`
516+
511517
- **`--logFile`/ `--log-file`**
512518
Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
513519
- **Type:** string
@@ -750,6 +756,95 @@ For more details on remote debugging, see the [Chrome DevTools documentation](ht
750756

751757
Please consult [these instructions](./docs/debugging-android.md).
752758

759+
### Additional browser support
760+
761+
In addition to Chrome, Chrome DevTools MCP supports Microsoft Edge. Edge must be installed separately. It is not bundled or downloaded by the MCP server.
762+
763+
#### Microsoft Edge
764+
765+
Pass `--browser=edge` to use the Microsoft Edge browser:
766+
767+
```json
768+
{
769+
"mcpServers": {
770+
"chrome-devtools": {
771+
"command": "npx",
772+
"args": ["-y", "chrome-devtools-mcp@latest", "--browser=edge"]
773+
}
774+
}
775+
}
776+
```
777+
778+
You can combine `--browser=edge` with other flags such as `--channel`, `--headless`, `--autoConnect`, and `--executablePath`.
779+
780+
#### Edge channels
781+
782+
Edge supports `stable`, `beta`, `dev`, and `canary` channels selected via `--channel`.
783+
784+
```bash
785+
npx chrome-devtools-mcp@latest --browser=edge --channel=beta
786+
```
787+
788+
#### Edge executable paths
789+
790+
The MCP server automatically detects Edge installations in the standard locations:
791+
792+
| Platform | Stable channel path |
793+
|----------|-------------|
794+
| **Windows** | `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe` |
795+
| **macOS** | `/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge` |
796+
| **Linux** | `/opt/microsoft/msedge/msedge` or `/usr/bin/microsoft-edge` |
797+
798+
If Edge is installed in a non-standard location, use `--executablePath` to specify the path manually.
799+
800+
#### Edge user data directory
801+
802+
When using `--browser=edge`, the user data directory pattern changes from `chrome-profile-$CHANNEL` to `edge-profile-$CHANNEL`:
803+
804+
- Linux / macOS: `$HOME/.cache/chrome-devtools-mcp/edge-profile-$CHANNEL`
805+
- Windows: `%HOMEPATH%/.cache/chrome-devtools-mcp/edge-profile-$CHANNEL`
806+
807+
#### Auto-connecting to a running Edge instance
808+
809+
Use `--autoConnect` with `--browser=edge` to connect to an already-running Edge instance:
810+
811+
```json
812+
{
813+
"mcpServers": {
814+
"chrome-devtools": {
815+
"command": "npx",
816+
"args": ["-y", "chrome-devtools-mcp@latest", "--autoConnect", "--browser=edge"]
817+
}
818+
}
819+
}
820+
```
821+
822+
Before connecting, enable remote debugging in Edge by navigating to `edge://inspect/#remote-debugging`.
823+
824+
#### Manual connection to a running Edge instance
825+
826+
Edge uses the same remote debugging protocol as Chrome. Start Edge with a remote debugging port and connect via `--browser-url`:
827+
828+
**Windows**
829+
830+
```bash
831+
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --remote-debugging-port=9222 --user-data-dir="%TEMP%\edge-profile-stable"
832+
```
833+
834+
**macOS**
835+
836+
```bash
837+
/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --remote-debugging-port=9222 --user-data-dir=/tmp/edge-profile-stable
838+
```
839+
840+
**Linux**
841+
842+
```bash
843+
/opt/microsoft/msedge/msedge --remote-debugging-port=9222 --user-data-dir=/tmp/edge-profile-stable
844+
```
845+
846+
Then configure the MCP server with `--browser-url=http://127.0.0.1:9222`.
847+
753848
## Known limitations
754849

755850
See [Troubleshooting](./docs/troubleshooting.md).

skills/chrome-devtools/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ description: Uses Chrome DevTools via MCP for efficient debugging, troubleshooti
55

66
## Core Concepts
77

8-
**Browser lifecycle**: Browser starts automatically on first tool call using a persistent Chrome profile. Configure via CLI args in the MCP server configuration: `npx chrome-devtools-mcp@latest --help`.
8+
**Browser lifecycle**: Browser starts automatically on first tool call using a persistent browser profile. Configure via CLI args in the MCP server configuration: `npx chrome-devtools-mcp@latest --help`.
9+
10+
**Browser selection**: With no browser selection arguments, stable channel Chrome browser is the default. You can use `--browser=edge` to specify the Microsoft Edge browser. You can also use `--channel=stable|beta|dev|canary` to specify a channel. Alternatively, use `--executablePath` to specify the binary path of the browser you want to use.
911

1012
**Page selection**: Tools operate on the currently selected page. Use `list_pages` to see available pages, then `select_page` to switch context.
1113

src/McpContext.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import fs from 'node:fs/promises';
88
import path from 'node:path';
99

10+
import {isExtensionUrl} from './browser.js';
1011
import type {TargetUniverse} from './DevtoolsUtils.js';
1112
import {UniverseManager} from './DevtoolsUtils.js';
1213
import {McpPage} from './McpPage.js';
@@ -508,7 +509,7 @@ export class McpContext implements Context {
508509
const serviceWorkers = allTargets.filter(target => {
509510
return (
510511
target.type() === 'service_worker' &&
511-
target.url().includes('chrome-extension://')
512+
isExtensionUrl(target.url()
512513
);
513514
});
514515

@@ -589,7 +590,7 @@ export class McpContext implements Context {
589590
const allTargets = this.browser.targets();
590591
const extensionTargets = allTargets.filter(target => {
591592
return (
592-
target.url().startsWith('chrome-extension://') &&
593+
isExtensionUrl(target.url()) &&
593594
target.type() === 'page'
594595
);
595596
});

src/McpResponse.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js';
8+
import {isExtensionUrl} from './browser.js';
89
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
910
import {IssueFormatter} from './formatters/IssueFormatter.js';
1011
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
@@ -567,7 +568,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
567568

568569
const {regularPages, extensionPages} = allPages.reduce(
569570
(acc: {regularPages: Page[]; extensionPages: Page[]}, page: Page) => {
570-
if (page.url().startsWith('chrome-extension://')) {
571+
if (isExtensionUrl(page.url())) {
571572
acc.extensionPages.push(page);
572573
} else {
573574
acc.regularPages.push(page);

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import os from 'node:os';
8+
79
import type {YargsOptions} from '../third_party/index.js';
810
import {yargs, hideBin} from '../third_party/index.js';
911

@@ -116,6 +118,12 @@ export const cliOptions = {
116118
choices: ['stable', 'canary', 'beta', 'dev'] as const,
117119
conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
118120
},
121+
browser: {
122+
type: 'string',
123+
description: 'Specify which browser to use. Defaults to Chrome.',
124+
choices: ['chrome', 'edge'] as const,
125+
default: 'chrome',
126+
},
119127
logFile: {
120128
type: 'string',
121129
describe:
@@ -272,6 +280,12 @@ export function parseArguments(version: string, argv = process.argv) {
272280
) {
273281
args.channel = 'stable';
274282
}
283+
// Edge Canary is not available on Linux.
284+
if (args.browser === 'edge' && args.channel === 'canary' && os.platform() === 'linux') {
285+
throw new Error(
286+
`Edge Canary is not available on Linux. Use --executablePath to specify a custom Edge binary.`,
287+
);
288+
}
275289
return true;
276290
})
277291
.example([
@@ -291,6 +305,7 @@ export function parseArguments(version: string, argv = process.argv) {
291305
['$0 --channel canary', 'Use Chrome Canary installed on this system'],
292306
['$0 --channel dev', 'Use Chrome Dev installed on this system'],
293307
['$0 --channel stable', 'Use stable Chrome installed on this system'],
308+
['$0 --browser edge', 'Use Microsoft Edge instead of Chrome'],
294309
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
295310
['$0 --help', 'Print CLI options'],
296311
[
@@ -323,6 +338,10 @@ export function parseArguments(version: string, argv = process.argv) {
323338
'$0 --auto-connect --channel=canary',
324339
'Connect to a canary Chrome instance (Chrome 144+) running instead of launching a new instance',
325340
],
341+
[
342+
'$0 --auto-connect --browser=edge',
343+
'Connect to a stable Edge instance running instead of launching a new instance',
344+
],
326345
[
327346
'$0 --no-usage-statistics',
328347
'Do not send usage statistics https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics.',

src/browser.ts

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import fs from 'node:fs';
99
import os from 'node:os';
1010
import path from 'node:path';
1111

12+
import {
13+
resolveEdgeExecutablePath,
14+
resolveEdgeUserDataDir,
15+
} from './edgePaths.js';
1216
import {logger} from './logger.js';
1317
import type {
1418
Browser,
@@ -21,17 +25,23 @@ import {puppeteer} from './third_party/index.js';
2125
let browser: Browser | undefined;
2226

2327
function makeTargetFilter(enableExtensions = false) {
24-
const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
28+
const ignoredPrefixes = new Set([
29+
'chrome://',
30+
'chrome-untrusted://',
31+
'edge://',
32+
'edge-untrusted://',
33+
]);
2534
if (!enableExtensions) {
2635
ignoredPrefixes.add('chrome-extension://');
36+
ignoredPrefixes.add('edge-extension://');
2737
}
2838

2939
return function targetFilter(target: Target): boolean {
30-
if (target.url() === 'chrome://newtab/') {
40+
if (isBrowserNewTabUrl(target.url())) {
3141
return true;
3242
}
3343
// Could be the only page opened in the browser.
34-
if (target.url().startsWith('chrome://inspect')) {
44+
if (isBrowserInspectUrl(target.url())) {
3545
return true;
3646
}
3747
for (const prefix of ignoredPrefixes) {
@@ -49,10 +59,11 @@ export async function ensureBrowserConnected(options: {
4959
wsHeaders?: Record<string, string>;
5060
devtools: boolean;
5161
channel?: Channel;
62+
browserKind?: BrowserKind;
5263
userDataDir?: string;
5364
enableExtensions?: boolean;
5465
}) {
55-
const {channel, enableExtensions} = options;
66+
const {channel, enableExtensions, browserKind = 'chrome'} = options;
5667
if (browser?.connected) {
5768
return browser;
5869
}
@@ -72,7 +83,12 @@ export async function ensureBrowserConnected(options: {
7283
} else if (options.browserURL) {
7384
connectOptions.browserURL = options.browserURL;
7485
} else if (channel || options.userDataDir) {
75-
const userDataDir = options.userDataDir;
86+
let userDataDir = options.userDataDir;
87+
// Puppeteer's runtime does not resolve Edge channels, so we find
88+
// Edge's default user data dir ourselves for auto-connect.
89+
if (!userDataDir && browserKind === 'edge' && channel) {
90+
userDataDir = resolveEdgeUserDataDir(channel);
91+
}
7692
if (userDataDir) {
7793
autoConnect = true;
7894
// TODO: re-expose this logic via Puppeteer.
@@ -98,7 +114,7 @@ export async function ensureBrowserConnected(options: {
98114
connectOptions.browserWSEndpoint = browserWSEndpoint;
99115
} catch (error) {
100116
throw new Error(
101-
`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`,
117+
`Could not connect to ${browserName(browserKind)} in ${userDataDir}. Check if ${browserName(browserKind)} is running and remote debugging is enabled by going to ${inspectUrl(browserKind)}.`,
102118
{
103119
cause: error,
104120
},
@@ -123,7 +139,7 @@ export async function ensureBrowserConnected(options: {
123139
browser = await puppeteer.connect(connectOptions);
124140
} catch (err) {
125141
throw new Error(
126-
`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`,
142+
`Could not connect to ${browserName(browserKind)}. ${autoConnect ? `Check if ${browserName(browserKind)} is running and remote debugging is enabled by going to ${inspectUrl(browserKind)}.` : `Check if ${browserName(browserKind)} is running.`}`,
127143
{
128144
cause: err,
129145
},
@@ -137,6 +153,7 @@ interface McpLaunchOptions {
137153
acceptInsecureCerts?: boolean;
138154
executablePath?: string;
139155
channel?: Channel;
156+
browserKind?: BrowserKind;
140157
userDataDir?: string;
141158
headless: boolean;
142159
isolated: boolean;
@@ -171,11 +188,18 @@ export function detectDisplay(): void {
171188
}
172189

173190
export async function launch(options: McpLaunchOptions): Promise<Browser> {
174-
const {channel, executablePath, headless, isolated} = options;
191+
const {
192+
channel,
193+
executablePath,
194+
headless,
195+
isolated,
196+
browserKind = 'chrome',
197+
} = options;
198+
const browserPrefix = browserKind === 'edge' ? 'edge' : 'chrome';
175199
const profileDirName =
176200
channel && channel !== 'stable'
177-
? `chrome-profile-${channel}`
178-
: 'chrome-profile';
201+
? `${browserPrefix}-profile-${channel}`
202+
: `${browserPrefix}-profile`;
179203

180204
let userDataDir = options.userDataDir;
181205
if (!isolated && !userDataDir) {
@@ -204,11 +228,19 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
204228
if (options.devtools) {
205229
args.push('--auto-open-devtools-for-tabs');
206230
}
207-
if (!executablePath) {
208-
puppeteerChannel =
209-
channel && channel !== 'stable'
210-
? (`chrome-${channel}` as ChromeReleaseChannel)
211-
: 'chrome';
231+
let resolvedExecutablePath = executablePath;
232+
if (!resolvedExecutablePath) {
233+
if (browserKind === 'edge') {
234+
// Puppeteer's runtime does not resolve Edge channels, so we find the
235+
// Edge executable ourselves and pass it as executablePath.
236+
const edgeChannel = channel ?? 'stable';
237+
resolvedExecutablePath = resolveEdgeExecutablePath(edgeChannel);
238+
} else {
239+
puppeteerChannel =
240+
channel && channel !== 'stable'
241+
? (`chrome-${channel}` as ChromeReleaseChannel)
242+
: 'chrome';
243+
}
212244
}
213245

214246
if (!headless) {
@@ -219,7 +251,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
219251
const browser = await puppeteer.launch({
220252
channel: puppeteerChannel,
221253
targetFilter: makeTargetFilter(options.enableExtensions),
222-
executablePath,
254+
executablePath: resolvedExecutablePath,
223255
defaultViewport: null,
224256
userDataDir,
225257
pipe: true,
@@ -271,3 +303,33 @@ export async function ensureBrowserLaunched(
271303
}
272304

273305
export type Channel = 'stable' | 'canary' | 'beta' | 'dev';
306+
307+
export type BrowserKind = 'chrome' | 'edge';
308+
309+
export function browserName(browserKind: BrowserKind): string {
310+
return browserKind === 'edge' ? 'Edge' : 'Chrome';
311+
}
312+
313+
export function inspectUrl(browserKind: BrowserKind): string {
314+
return browserKind === 'edge'
315+
? 'edge://inspect/#remote-debugging'
316+
: 'chrome://inspect/#remote-debugging';
317+
}
318+
319+
export function isExtensionUrl(url: string): boolean {
320+
return url.startsWith('chrome-extension://') ||
321+
url.startsWith('edge-extension://');
322+
}
323+
324+
export function isBrowserNewTabUrl(url: string): boolean {
325+
return url === 'chrome://newtab/' || url === 'edge://newtab/';
326+
}
327+
328+
export function isBrowserInspectUrl(url: string): boolean {
329+
return url.startsWith('chrome://inspect') || url.startsWith('edge://inspect');
330+
}
331+
332+
export {
333+
resolveEdgeExecutablePath,
334+
resolveEdgeUserDataDir,
335+
} from './edgePaths.js';

0 commit comments

Comments
 (0)