Skip to content

Commit 695ee7d

Browse files
authored
Merge pull request #57 from angiejones/feat/safari-support
feat: add Safari browser support
2 parents cd02fe3 + 1f918b1 commit 695ee7d

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ A Model Context Protocol (MCP) server implementation for Selenium WebDriver, ena
2828
- Chrome
2929
- Firefox
3030
- MS Edge
31+
- Safari (macOS only)
32+
33+
> **Note:** Safari requires macOS with `safaridriver`, which is included with Safari.
34+
> Run `sudo safaridriver --enable` once, then enable "Allow Remote Automation"
35+
> in Safari → Settings → Developer. Safari does not support headless mode or
36+
> custom browser arguments.
3137
3238
## Use with Goose
3339

@@ -117,7 +123,7 @@ Launches a browser session.
117123
**Parameters:**
118124
- `browser` (required): Browser to launch
119125
- Type: string
120-
- Enum: ["chrome", "firefox"]
126+
- Enum: ["chrome", "firefox", "edge", "safari"]
121127
- `options`: Browser configuration options
122128
- Type: object
123129
- Properties:

src/lib/server.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { Builder, By, Key, until, Actions } = pkg;
88
import { Options as ChromeOptions } from 'selenium-webdriver/chrome.js';
99
import { Options as FirefoxOptions } from 'selenium-webdriver/firefox.js';
1010
import { Options as EdgeOptions } from 'selenium-webdriver/edge.js';
11+
import { Options as SafariOptions } from 'selenium-webdriver/safari.js';
1112

1213

1314
// Create an MCP server
@@ -60,13 +61,14 @@ server.tool(
6061
"start_browser",
6162
"launches browser",
6263
{
63-
browser: z.enum(["chrome", "firefox", "edge"]).describe("Browser to launch (chrome or firefox or microsoft edge)"),
64+
browser: z.enum(["chrome", "firefox", "edge", "safari"]).describe("Browser to launch (chrome, firefox, edge, or safari)"),
6465
options: browserOptionsSchema
6566
},
6667
async ({ browser, options = {} }) => {
6768
try {
6869
let builder = new Builder();
6970
let driver;
71+
let warnings = [];
7072
switch (browser) {
7173
case 'chrome': {
7274
const chromeOptions = new ChromeOptions();
@@ -110,6 +112,20 @@ server.tool(
110112
.build();
111113
break;
112114
}
115+
case 'safari': {
116+
const safariOptions = new SafariOptions();
117+
if (options.headless) {
118+
warnings.push('Safari does not support headless mode — launching with visible window.');
119+
}
120+
if (options.arguments?.length) {
121+
warnings.push('Safari does not support custom arguments — ignoring.');
122+
}
123+
driver = await builder
124+
.forBrowser('safari')
125+
.setSafariOptions(safariOptions)
126+
.build();
127+
break;
128+
}
113129
default: {
114130
throw new Error(`Unsupported browser: ${browser}`);
115131
}
@@ -118,8 +134,13 @@ server.tool(
118134
state.drivers.set(sessionId, driver);
119135
state.currentSession = sessionId;
120136

137+
let message = `Browser started with session_id: ${sessionId}`;
138+
if (warnings.length > 0) {
139+
message += `\nWarnings: ${warnings.join(' ')}`;
140+
}
141+
121142
return {
122-
content: [{ type: 'text', text: `Browser started with session_id: ${sessionId}` }]
143+
content: [{ type: 'text', text: message }]
123144
};
124145
} catch (e) {
125146
return {

test/safari.test.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Safari browser support tests.
3+
*
4+
* Integration tests require macOS with:
5+
* sudo safaridriver --enable
6+
* SAFARI_AVAILABLE=1 npm test
7+
*/
8+
9+
import { describe, it, before, after } from 'node:test';
10+
import assert from 'node:assert/strict';
11+
import { McpClient, getResponseText } from './mcp-client.mjs';
12+
13+
const safariAvailable = process.platform === 'darwin' && process.env.SAFARI_AVAILABLE === '1';
14+
15+
describe('Safari Browser Support', () => {
16+
let client;
17+
let tools;
18+
19+
before(async () => {
20+
client = new McpClient();
21+
await client.start();
22+
tools = await client.listTools();
23+
});
24+
25+
after(async () => {
26+
try { await client.callTool('close_session'); } catch { /* ignore */ }
27+
await client.stop();
28+
});
29+
30+
it('should include "safari" in the start_browser browser enum', () => {
31+
const startBrowser = tools.find(t => t.name === 'start_browser');
32+
assert.ok(startBrowser, 'start_browser tool should exist');
33+
const browserEnum = startBrowser.inputSchema.properties.browser.enum;
34+
assert.ok(browserEnum.includes('safari'),
35+
`Expected "safari" in enum, got: ${JSON.stringify(browserEnum)}`);
36+
});
37+
38+
it('should surface warnings for unsupported options', { skip: !safariAvailable && 'safaridriver not available' }, async () => {
39+
const result = await client.callTool('start_browser', {
40+
browser: 'safari',
41+
options: { headless: true, arguments: ['--some-flag'] },
42+
});
43+
const text = getResponseText(result);
44+
assert.ok(!result.isError, `Expected success, got error: ${text}`);
45+
assert.ok(text.includes('Browser started'), `Expected browser to start, got: ${text}`);
46+
assert.ok(text.includes('does not support headless'),
47+
`Expected headless warning in response, got: ${text}`);
48+
assert.ok(text.includes('does not support custom arguments'),
49+
`Expected arguments warning in response, got: ${text}`);
50+
});
51+
});

0 commit comments

Comments
 (0)