Skip to content

Commit 74caf79

Browse files
authored
Merge pull request #59 from angiejones/feat/cookie-management
feat: add cookie management tools
2 parents 695ee7d + fbcd2c1 commit 74caf79

5 files changed

Lines changed: 349 additions & 2 deletions

File tree

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ A Model Context Protocol (MCP) server implementation for Selenium WebDriver, ena
2222
- Take screenshots
2323
- Upload files
2424
- Support for headless mode
25+
- Manage browser cookies (add, get, delete)
2526

2627
## Supported Browsers
2728

@@ -707,6 +708,89 @@ Types text into a browser prompt dialog and accepts it.
707708
}
708709
```
709710

711+
712+
### add_cookie
713+
Adds a cookie to the current browser session.
714+
715+
**Parameters:**
716+
717+
| Parameter | Type | Required | Description |
718+
|-----------|------|----------|-------------|
719+
| name | string | Yes | Cookie name |
720+
| value | string | Yes | Cookie value |
721+
| domain | string | No | Cookie domain |
722+
| path | string | No | Cookie path (default: /) |
723+
| secure | boolean | No | Whether the cookie is secure |
724+
| httpOnly | boolean | No | Whether the cookie is HTTP-only |
725+
| expiry | number | No | Cookie expiry as Unix timestamp |
726+
727+
**Example:**
728+
```json
729+
{
730+
"tool": "add_cookie",
731+
"parameters": {
732+
"name": "session_id",
733+
"value": "abc123",
734+
"path": "/",
735+
"httpOnly": true
736+
}
737+
}
738+
```
739+
740+
### get_cookies
741+
Retrieves cookies from the current browser session. Returns all cookies or a specific cookie by name.
742+
743+
**Parameters:**
744+
745+
| Parameter | Type | Required | Description |
746+
|-----------|------|----------|-------------|
747+
| name | string | No | Cookie name to retrieve. If omitted, returns all cookies. |
748+
749+
**Example — get all cookies:**
750+
```json
751+
{
752+
"tool": "get_cookies",
753+
"parameters": {}
754+
}
755+
```
756+
757+
**Example — get a specific cookie:**
758+
```json
759+
{
760+
"tool": "get_cookies",
761+
"parameters": {
762+
"name": "session_id"
763+
}
764+
}
765+
```
766+
767+
### delete_cookie
768+
Deletes cookies from the current browser session. Deletes a specific cookie by name, or all cookies if no name is provided.
769+
770+
**Parameters:**
771+
772+
| Parameter | Type | Required | Description |
773+
|-----------|------|----------|-------------|
774+
| name | string | No | Cookie name to delete. If omitted, deletes all cookies. |
775+
776+
**Example — delete a specific cookie:**
777+
```json
778+
{
779+
"tool": "delete_cookie",
780+
"parameters": {
781+
"name": "session_id"
782+
}
783+
}
784+
```
785+
786+
**Example — delete all cookies:**
787+
```json
788+
{
789+
"tool": "delete_cookie",
790+
"parameters": {}
791+
}
792+
```
793+
710794
## License
711795

712796
MIT

src/lib/server.js

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
import { z } from "zod";
66
import pkg from 'selenium-webdriver';
7-
const { Builder, By, Key, until, Actions } = pkg;
7+
const { Builder, By, Key, until, Actions, error } = 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';
@@ -847,6 +847,116 @@ server.tool(
847847
}
848848
);
849849

850+
851+
// Cookie Management Tools
852+
server.tool(
853+
"add_cookie",
854+
"adds a cookie to the current browser session. The browser must be on a page from the cookie's domain before setting it.",
855+
{
856+
name: z.string().describe("Name of the cookie"),
857+
value: z.string().describe("Value of the cookie"),
858+
domain: z.string().optional().describe("Domain the cookie is visible to"),
859+
path: z.string().optional().describe("Path the cookie is visible to"),
860+
secure: z.boolean().optional().describe("Whether the cookie is a secure cookie"),
861+
httpOnly: z.boolean().optional().describe("Whether the cookie is HTTP only"),
862+
expiry: z.number().optional().describe("Expiry date of the cookie as a Unix timestamp (seconds since epoch)")
863+
},
864+
async ({ name, value, domain, path, secure, httpOnly, expiry }) => {
865+
try {
866+
const driver = getDriver();
867+
const cookie = { name, value };
868+
if (domain !== undefined) cookie.domain = domain;
869+
if (path !== undefined) cookie.path = path;
870+
if (secure !== undefined) cookie.secure = secure;
871+
if (httpOnly !== undefined) cookie.httpOnly = httpOnly;
872+
if (expiry !== undefined) cookie.expiry = expiry;
873+
await driver.manage().addCookie(cookie);
874+
return {
875+
content: [{ type: 'text', text: `Cookie "${name}" added` }]
876+
};
877+
} catch (e) {
878+
return {
879+
content: [{ type: 'text', text: `Error adding cookie: ${e.message}` }],
880+
isError: true
881+
};
882+
}
883+
}
884+
);
885+
886+
server.tool(
887+
"get_cookies",
888+
"retrieves cookies from the current browser session. Returns all cookies or a specific cookie by name.",
889+
{
890+
name: z.string().optional().describe("Name of a specific cookie to retrieve. If omitted, all cookies are returned.")
891+
},
892+
async ({ name }) => {
893+
try {
894+
const driver = getDriver();
895+
if (name) {
896+
try {
897+
const cookie = await driver.manage().getCookie(name);
898+
if (!cookie) {
899+
return {
900+
content: [{ type: 'text', text: `Cookie "${name}" not found` }],
901+
isError: true
902+
};
903+
}
904+
return {
905+
content: [{ type: 'text', text: JSON.stringify(cookie, null, 2) }]
906+
};
907+
} catch (cookieError) {
908+
if (cookieError instanceof error.NoSuchCookieError) {
909+
return {
910+
content: [{ type: 'text', text: `Cookie "${name}" not found` }],
911+
isError: true
912+
};
913+
}
914+
throw cookieError;
915+
}
916+
} else {
917+
const cookies = await driver.manage().getCookies();
918+
return {
919+
content: [{ type: 'text', text: JSON.stringify(cookies, null, 2) }]
920+
};
921+
}
922+
} catch (e) {
923+
return {
924+
content: [{ type: 'text', text: `Error getting cookies: ${e.message}` }],
925+
isError: true
926+
};
927+
}
928+
}
929+
);
930+
931+
server.tool(
932+
"delete_cookie",
933+
"deletes cookies from the current browser session. Can delete a specific cookie by name or all cookies.",
934+
{
935+
name: z.string().optional().describe("Name of the cookie to delete. If omitted, all cookies are deleted.")
936+
},
937+
async ({ name }) => {
938+
try {
939+
const driver = getDriver();
940+
if (name) {
941+
await driver.manage().deleteCookie(name);
942+
return {
943+
content: [{ type: 'text', text: `Cookie "${name}" deleted` }]
944+
};
945+
} else {
946+
await driver.manage().deleteAllCookies();
947+
return {
948+
content: [{ type: 'text', text: 'All cookies deleted' }]
949+
};
950+
}
951+
} catch (e) {
952+
return {
953+
content: [{ type: 'text', text: `Error deleting cookie: ${e.message}` }],
954+
isError: true
955+
};
956+
}
957+
}
958+
);
959+
850960
// Resources
851961
server.resource(
852962
"browser-status",

test/cookies.test.mjs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Cookie management tests — add_cookie, get_cookies, delete_cookie.
3+
*
4+
* Cookies require an HTTP domain (file:// URLs don't support cookies),
5+
* so we spin up a tiny local HTTP server for the duration of the suite.
6+
*/
7+
8+
import { describe, it, before, after, beforeEach } from 'node:test';
9+
import assert from 'node:assert/strict';
10+
import http from 'node:http';
11+
import fs from 'node:fs';
12+
import path from 'node:path';
13+
import { fileURLToPath } from 'node:url';
14+
import { McpClient, getResponseText } from './mcp-client.mjs';
15+
16+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
17+
18+
function startServer() {
19+
return new Promise((resolve, reject) => {
20+
const html = fs.readFileSync(path.join(__dirname, 'fixtures', 'cookies.html'), 'utf-8');
21+
const server = http.createServer((req, res) => {
22+
res.writeHead(200, { 'Content-Type': 'text/html' });
23+
res.end(html);
24+
});
25+
server.once('error', reject);
26+
server.listen(0, '127.0.0.1', () => {
27+
const { port } = server.address();
28+
resolve({ server, url: `http://127.0.0.1:${port}` });
29+
});
30+
});
31+
}
32+
33+
describe('Cookie Management', () => {
34+
let client;
35+
let httpServer;
36+
let baseUrl;
37+
38+
before(async () => {
39+
const srv = await startServer();
40+
httpServer = srv.server;
41+
baseUrl = srv.url;
42+
43+
client = new McpClient();
44+
await client.start();
45+
await client.callTool('start_browser', {
46+
browser: 'chrome',
47+
options: { headless: true, arguments: ['--no-sandbox', '--disable-dev-shm-usage'] },
48+
});
49+
await client.callTool('navigate', { url: baseUrl });
50+
});
51+
52+
after(async () => {
53+
try { await client.callTool('close_session'); } catch { /* ignore */ }
54+
await client.stop();
55+
await new Promise((resolve) => httpServer.close(resolve));
56+
});
57+
58+
beforeEach(async () => {
59+
await client.callTool('delete_cookie', {});
60+
});
61+
62+
describe('add_cookie', () => {
63+
it('should add a cookie and verify it was set', async () => {
64+
const result = await client.callTool('add_cookie', {
65+
name: 'test_cookie',
66+
value: 'hello123',
67+
});
68+
assert.ok(getResponseText(result).includes('Cookie "test_cookie" added'));
69+
70+
const getResult = await client.callTool('get_cookies', { name: 'test_cookie' });
71+
const cookie = JSON.parse(getResponseText(getResult));
72+
assert.equal(cookie.name, 'test_cookie');
73+
assert.equal(cookie.value, 'hello123');
74+
});
75+
76+
it('should respect optional properties', async () => {
77+
await client.callTool('add_cookie', {
78+
name: 'opts_cookie',
79+
value: 'secret',
80+
path: '/',
81+
httpOnly: true,
82+
});
83+
84+
const getResult = await client.callTool('get_cookies', { name: 'opts_cookie' });
85+
const cookie = JSON.parse(getResponseText(getResult));
86+
assert.equal(cookie.path, '/');
87+
assert.equal(cookie.httpOnly, true);
88+
});
89+
});
90+
91+
describe('get_cookies', () => {
92+
it('should return all cookies as an array', async () => {
93+
await client.callTool('add_cookie', { name: 'a', value: '1' });
94+
await client.callTool('add_cookie', { name: 'b', value: '2' });
95+
96+
const result = await client.callTool('get_cookies', {});
97+
const cookies = JSON.parse(getResponseText(result));
98+
assert.ok(Array.isArray(cookies));
99+
const names = cookies.map(c => c.name);
100+
assert.ok(names.includes('a') && names.includes('b'));
101+
});
102+
103+
it('should return empty array when no cookies exist', async () => {
104+
const result = await client.callTool('get_cookies', {});
105+
const cookies = JSON.parse(getResponseText(result));
106+
assert.equal(cookies.length, 0);
107+
});
108+
109+
it('should error when a named cookie is not found', async () => {
110+
const result = await client.callTool('get_cookies', { name: 'ghost' });
111+
assert.strictEqual(result.isError, true);
112+
assert.ok(getResponseText(result).includes('not found'));
113+
});
114+
});
115+
116+
describe('delete_cookie', () => {
117+
it('should delete a specific cookie and leave others', async () => {
118+
await client.callTool('add_cookie', { name: 'delete_me', value: 'bye' });
119+
await client.callTool('add_cookie', { name: 'keep_me', value: 'stay' });
120+
121+
await client.callTool('delete_cookie', { name: 'delete_me' });
122+
123+
const gone = await client.callTool('get_cookies', { name: 'delete_me' });
124+
assert.strictEqual(gone.isError, true);
125+
126+
const kept = JSON.parse(getResponseText(
127+
await client.callTool('get_cookies', { name: 'keep_me' })
128+
));
129+
assert.equal(kept.name, 'keep_me');
130+
});
131+
132+
it('should delete all cookies when no name is provided', async () => {
133+
await client.callTool('add_cookie', { name: 'x', value: '1' });
134+
await client.callTool('add_cookie', { name: 'y', value: '2' });
135+
136+
const result = await client.callTool('delete_cookie', {});
137+
assert.ok(getResponseText(result).includes('All cookies deleted'));
138+
139+
const cookies = JSON.parse(getResponseText(
140+
await client.callTool('get_cookies', {})
141+
));
142+
assert.equal(cookies.length, 0);
143+
});
144+
});
145+
});

test/fixtures/cookies.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head><meta charset="UTF-8"><title>Cookie Test Page</title></head>
4+
<body><h1 id="title">Cookie Test Page</h1></body>
5+
</html>

test/server.test.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('MCP Server', () => {
2525
assert.ok(tools.length > 0, 'Server should have tools registered');
2626
});
2727

28-
it('should register all 28 expected tools', async () => {
28+
it('should register all expected tools', async () => {
2929
const tools = await client.listTools();
3030
const names = tools.map((t) => t.name);
3131

@@ -58,6 +58,9 @@ describe('MCP Server', () => {
5858
'dismiss_alert',
5959
'get_alert_text',
6060
'send_alert_text',
61+
'add_cookie',
62+
'get_cookies',
63+
'delete_cookie',
6164
];
6265

6366
for (const name of expected) {

0 commit comments

Comments
 (0)