@mcpware/chrome-pilot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +454 -0
- package/dist/profiles.d.ts +53 -0
- package/dist/profiles.js +176 -0
- package/package.json +41 -0
- package/src/index.ts +514 -0
- package/src/profiles.ts +224 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# chrome-pilot
|
|
2
|
+
|
|
3
|
+
AI-controlled Chrome with real profile sync. Like Playwright, but with your bookmarks, passwords, and extensions.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Playwright is great for browser automation, but every session starts with a **clean browser** — no logins, no cookies, no bookmarks. You have to re-authenticate every time.
|
|
8
|
+
|
|
9
|
+
**chrome-pilot** solves this by launching your **real Chrome** with persistent profiles. Sign into Google once, and Chrome Sync brings over all your data. Every subsequent launch retains your sessions.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Playwright: Clean Chromium → re-login every time
|
|
13
|
+
chrome-pilot: Real Chrome + your profile → sessions persist forever
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mcpware/chrome-pilot
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### As an MCP Server (for Claude, etc.)
|
|
25
|
+
|
|
26
|
+
Add to your MCP config:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"chrome-pilot": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["@mcpware/chrome-pilot"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then ask your AI:
|
|
40
|
+
- *"Open my personal Chrome and go to GitHub"*
|
|
41
|
+
- *"Take a screenshot of the current page"*
|
|
42
|
+
- *"List my Chrome profiles"*
|
|
43
|
+
|
|
44
|
+
### As a Library
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { launchProfile, listProfiles, closeProfile } from "@mcpware/chrome-pilot";
|
|
48
|
+
|
|
49
|
+
// Launch Chrome with a persistent profile
|
|
50
|
+
const { page, port } = await launchProfile("personal", {
|
|
51
|
+
url: "https://github.com",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// First time? Chrome sign-in page appears.
|
|
55
|
+
// Sign into Google → Chrome Sync pulls your bookmarks, passwords, extensions.
|
|
56
|
+
// Next time? Everything is already there.
|
|
57
|
+
|
|
58
|
+
await page.screenshot({ path: "screenshot.png" });
|
|
59
|
+
await closeProfile("personal");
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## MCP Tools
|
|
63
|
+
|
|
64
|
+
### Profile Management
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `list_profiles` | List all Chrome profiles with linked Google accounts |
|
|
69
|
+
| `create_profile` | Create a new profile |
|
|
70
|
+
| `delete_profile` | Delete a profile and its data |
|
|
71
|
+
| `launch` | Launch Chrome with a profile (auto-creates if needed) |
|
|
72
|
+
| `close` | Close a profile session |
|
|
73
|
+
| `get_active` | List running sessions with CDP ports |
|
|
74
|
+
|
|
75
|
+
### Browser Control
|
|
76
|
+
|
|
77
|
+
| Tool | Description |
|
|
78
|
+
|------|-------------|
|
|
79
|
+
| `navigate` | Go to a URL |
|
|
80
|
+
| `screenshot` | Capture the page as PNG |
|
|
81
|
+
| `click` | Click an element by CSS selector |
|
|
82
|
+
| `type` | Type text into focused element |
|
|
83
|
+
| `fill` | Fill a form field by selector |
|
|
84
|
+
| `eval` | Run JavaScript in page context |
|
|
85
|
+
| `snap` | Get accessibility tree of the page |
|
|
86
|
+
|
|
87
|
+
### Tab Management
|
|
88
|
+
|
|
89
|
+
| Tool | Description |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `list_tabs` | List all open tabs |
|
|
92
|
+
| `new_tab` | Open a new tab |
|
|
93
|
+
|
|
94
|
+
## How It Works
|
|
95
|
+
|
|
96
|
+
1. **Uses your real Chrome** (not Playwright's bundled Chromium)
|
|
97
|
+
2. **Persistent profiles** stored in `~/.chrome-pilot/profiles/`
|
|
98
|
+
3. **Chrome Sync** brings your bookmarks, passwords, and extensions
|
|
99
|
+
4. **Multi-profile** support — run personal and work Chrome simultaneously
|
|
100
|
+
5. **MCP interface** — AI agents control Chrome through standard MCP tools
|
|
101
|
+
6. **CDP bridge** — each profile gets its own debugging port
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- Google Chrome installed on your system
|
|
106
|
+
- Node.js 20+
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { listProfiles, createProfile, deleteProfile, launchProfile, closeProfile, getActiveSessions, getSessionPage, } from "./profiles.js";
|
|
6
|
+
const server = new Server({
|
|
7
|
+
name: "chrome-pilot",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
}, {
|
|
10
|
+
capabilities: {
|
|
11
|
+
tools: {},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
// ─── Tool Definitions ───────────────────────────────────────────────
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16
|
+
tools: [
|
|
17
|
+
// Profile Management
|
|
18
|
+
{
|
|
19
|
+
name: "list_profiles",
|
|
20
|
+
description: "List all Chrome profiles available in chrome-pilot. Shows profile name and linked Google account.",
|
|
21
|
+
inputSchema: { type: "object", properties: {} },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "create_profile",
|
|
25
|
+
description: "Create a new Chrome profile. After creation, launch it to sign into Google and sync your data.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
name: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: 'Profile name (e.g. "personal", "work", "client-abc")',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["name"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "delete_profile",
|
|
39
|
+
description: "Delete a Chrome profile and all its data.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
name: { type: "string", description: "Profile name to delete" },
|
|
44
|
+
},
|
|
45
|
+
required: ["name"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "launch",
|
|
50
|
+
description: "Launch Chrome with a specific profile. First launch opens Chrome sign-in page for Google Sync. All subsequent launches retain your sessions, bookmarks, and passwords.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
profile: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Profile name to launch",
|
|
57
|
+
},
|
|
58
|
+
url: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Optional URL to navigate to after launch",
|
|
61
|
+
},
|
|
62
|
+
headless: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Run in headless mode (default: false)",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["profile"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "close",
|
|
72
|
+
description: "Close a Chrome profile session.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
profile: { type: "string", description: "Profile name to close" },
|
|
77
|
+
},
|
|
78
|
+
required: ["profile"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "get_active",
|
|
83
|
+
description: "List all currently running Chrome sessions with their profiles and CDP ports.",
|
|
84
|
+
inputSchema: { type: "object", properties: {} },
|
|
85
|
+
},
|
|
86
|
+
// Browser Control
|
|
87
|
+
{
|
|
88
|
+
name: "navigate",
|
|
89
|
+
description: "Navigate to a URL in the active page of a profile.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
profile: { type: "string", description: "Profile name" },
|
|
94
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
95
|
+
},
|
|
96
|
+
required: ["profile", "url"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "screenshot",
|
|
101
|
+
description: "Take a screenshot of the active page. Returns base64 encoded image.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
profile: { type: "string", description: "Profile name" },
|
|
106
|
+
},
|
|
107
|
+
required: ["profile"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "click",
|
|
112
|
+
description: "Click an element on the page by CSS selector.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
profile: { type: "string", description: "Profile name" },
|
|
117
|
+
selector: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "CSS selector of element to click",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["profile", "selector"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "type",
|
|
127
|
+
description: "Type text into the currently focused element.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
profile: { type: "string", description: "Profile name" },
|
|
132
|
+
text: { type: "string", description: "Text to type" },
|
|
133
|
+
},
|
|
134
|
+
required: ["profile", "text"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "fill",
|
|
139
|
+
description: "Fill a form field by selector with the given value.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
profile: { type: "string", description: "Profile name" },
|
|
144
|
+
selector: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "CSS selector of the input field",
|
|
147
|
+
},
|
|
148
|
+
value: { type: "string", description: "Value to fill" },
|
|
149
|
+
},
|
|
150
|
+
required: ["profile", "selector", "value"],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "eval",
|
|
155
|
+
description: "Evaluate JavaScript in the page context.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
profile: { type: "string", description: "Profile name" },
|
|
160
|
+
expression: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "JavaScript expression to evaluate",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
required: ["profile", "expression"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "snap",
|
|
170
|
+
description: "Get the accessibility tree of the current page. Useful for understanding page structure without screenshots.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
profile: { type: "string", description: "Profile name" },
|
|
175
|
+
},
|
|
176
|
+
required: ["profile"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
// Tab Management
|
|
180
|
+
{
|
|
181
|
+
name: "list_tabs",
|
|
182
|
+
description: "List all open tabs in a profile session.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
profile: { type: "string", description: "Profile name" },
|
|
187
|
+
},
|
|
188
|
+
required: ["profile"],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "new_tab",
|
|
193
|
+
description: "Open a new tab in a profile session.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
profile: { type: "string", description: "Profile name" },
|
|
198
|
+
url: { type: "string", description: "URL to open in the new tab" },
|
|
199
|
+
},
|
|
200
|
+
required: ["profile"],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}));
|
|
205
|
+
// ─── Tool Handlers ──────────────────────────────────────────────────
|
|
206
|
+
function getPage(profile) {
|
|
207
|
+
const page = getSessionPage(profile);
|
|
208
|
+
if (!page) {
|
|
209
|
+
throw new Error(`No active session for profile "${profile}". Use launch() first.`);
|
|
210
|
+
}
|
|
211
|
+
return page;
|
|
212
|
+
}
|
|
213
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
214
|
+
const { name, arguments: args } = request.params;
|
|
215
|
+
try {
|
|
216
|
+
switch (name) {
|
|
217
|
+
// ── Profile Management ──
|
|
218
|
+
case "list_profiles": {
|
|
219
|
+
const profiles = listProfiles();
|
|
220
|
+
if (profiles.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: 'No profiles found. Use create_profile to create one, or launch("profile-name") to auto-create and launch.',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const active = getActiveSessions();
|
|
231
|
+
const activeNames = new Set(active.map((s) => s.name));
|
|
232
|
+
const text = profiles
|
|
233
|
+
.map((p) => `${activeNames.has(p.name) ? "🟢" : "⚪"} ${p.name}${p.lastUsed ? ` (${p.lastUsed})` : ""}`)
|
|
234
|
+
.join("\n");
|
|
235
|
+
return { content: [{ type: "text", text }] };
|
|
236
|
+
}
|
|
237
|
+
case "create_profile": {
|
|
238
|
+
const profile = createProfile(args.name);
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: `Created profile "${profile.name}". Use launch("${profile.name}") to open Chrome and sign into Google.`,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
case "delete_profile": {
|
|
249
|
+
deleteProfile(args.name);
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: `Deleted profile "${args.name}".`,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
case "launch": {
|
|
260
|
+
const { context, page, port } = await launchProfile(args.profile, {
|
|
261
|
+
url: args?.url,
|
|
262
|
+
headless: args?.headless,
|
|
263
|
+
});
|
|
264
|
+
const url = page.url();
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: `Launched profile "${args.profile}" on CDP port ${port}.\nCurrent page: ${url}\nPages open: ${context.pages().length}`,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case "close": {
|
|
275
|
+
await closeProfile(args.profile);
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{ type: "text", text: `Closed profile "${args.profile}".` },
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
case "get_active": {
|
|
283
|
+
const sessions = getActiveSessions();
|
|
284
|
+
if (sessions.length === 0) {
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: "text", text: "No active sessions." }],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const text = sessions
|
|
290
|
+
.map((s) => `🟢 ${s.name} — port ${s.port}, ${s.pageCount} tab(s)`)
|
|
291
|
+
.join("\n");
|
|
292
|
+
return { content: [{ type: "text", text }] };
|
|
293
|
+
}
|
|
294
|
+
// ── Browser Control ──
|
|
295
|
+
case "navigate": {
|
|
296
|
+
const page = getPage(args.profile);
|
|
297
|
+
await page.goto(args.url, {
|
|
298
|
+
waitUntil: "domcontentloaded",
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: `Navigated to ${args.url}`,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case "screenshot": {
|
|
310
|
+
const page = getPage(args.profile);
|
|
311
|
+
const buffer = await page.screenshot();
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "image",
|
|
316
|
+
data: buffer.toString("base64"),
|
|
317
|
+
mimeType: "image/png",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
case "click": {
|
|
323
|
+
const page = getPage(args.profile);
|
|
324
|
+
await page.click(args.selector);
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: "text",
|
|
329
|
+
text: `Clicked "${args.selector}"`,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
case "type": {
|
|
335
|
+
const page = getPage(args.profile);
|
|
336
|
+
await page.keyboard.type(args.text);
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: `Typed "${args.text.substring(0, 50)}${args.text.length > 50 ? "..." : ""}"`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
case "fill": {
|
|
347
|
+
const page = getPage(args.profile);
|
|
348
|
+
await page.fill(args.selector, args.value);
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: `Filled "${args.selector}" with value`,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case "eval": {
|
|
359
|
+
const page = getPage(args.profile);
|
|
360
|
+
const result = await page.evaluate(args.expression);
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: "text",
|
|
365
|
+
text: typeof result === "string"
|
|
366
|
+
? result
|
|
367
|
+
: JSON.stringify(result, null, 2),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case "snap": {
|
|
373
|
+
const page = getPage(args.profile);
|
|
374
|
+
const snapshot = await page.evaluate(() => {
|
|
375
|
+
function getAccessibilityTree(el, depth = 0) {
|
|
376
|
+
const role = el.getAttribute("role") || el.tagName.toLowerCase();
|
|
377
|
+
const text = el.textContent?.trim().substring(0, 80) || "";
|
|
378
|
+
const label = el.getAttribute("aria-label") || "";
|
|
379
|
+
const indent = " ".repeat(depth);
|
|
380
|
+
let line = `${indent}[${role}]`;
|
|
381
|
+
if (label)
|
|
382
|
+
line += ` "${label}"`;
|
|
383
|
+
else if (text && !el.children.length)
|
|
384
|
+
line += ` "${text}"`;
|
|
385
|
+
const lines = [line];
|
|
386
|
+
for (const child of el.children) {
|
|
387
|
+
lines.push(...getAccessibilityTree(child, depth + 1).split("\n"));
|
|
388
|
+
}
|
|
389
|
+
return lines.filter(Boolean).join("\n");
|
|
390
|
+
}
|
|
391
|
+
return getAccessibilityTree(document.body);
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
content: [
|
|
395
|
+
{
|
|
396
|
+
type: "text",
|
|
397
|
+
text: snapshot,
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// ── Tab Management ──
|
|
403
|
+
case "list_tabs": {
|
|
404
|
+
const page = getPage(args.profile);
|
|
405
|
+
const context = page.context();
|
|
406
|
+
const tabs = context.pages().map((p, i) => `${i}: ${p.url()}`);
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: "text", text: tabs.join("\n") }],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
case "new_tab": {
|
|
412
|
+
const page = getPage(args.profile);
|
|
413
|
+
const context = page.context();
|
|
414
|
+
const newPage = await context.newPage();
|
|
415
|
+
if (args?.url) {
|
|
416
|
+
await newPage.goto(args.url, {
|
|
417
|
+
waitUntil: "domcontentloaded",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: "text",
|
|
424
|
+
text: `Opened new tab${args?.url ? `: ${args.url}` : ""}`,
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return {
|
|
435
|
+
content: [
|
|
436
|
+
{
|
|
437
|
+
type: "text",
|
|
438
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
isError: true,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// ─── Start Server ───────────────────────────────────────────────────
|
|
446
|
+
async function main() {
|
|
447
|
+
const transport = new StdioServerTransport();
|
|
448
|
+
await server.connect(transport);
|
|
449
|
+
console.error("chrome-pilot MCP server running");
|
|
450
|
+
}
|
|
451
|
+
main().catch((error) => {
|
|
452
|
+
console.error("Fatal:", error);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type BrowserContext, type Page } from "playwright-core";
|
|
2
|
+
export interface Profile {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
lastUsed?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Find Chrome executable on the system
|
|
9
|
+
*/
|
|
10
|
+
export declare function findChrome(): string;
|
|
11
|
+
/**
|
|
12
|
+
* List all available profiles
|
|
13
|
+
*/
|
|
14
|
+
export declare function listProfiles(): Profile[];
|
|
15
|
+
/**
|
|
16
|
+
* Create a new profile
|
|
17
|
+
*/
|
|
18
|
+
export declare function createProfile(name: string): Profile;
|
|
19
|
+
/**
|
|
20
|
+
* Delete a profile
|
|
21
|
+
*/
|
|
22
|
+
export declare function deleteProfile(name: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Launch Chrome with a profile — the core function
|
|
25
|
+
*
|
|
26
|
+
* Uses Playwright's launch_persistent_context with the real Chrome binary.
|
|
27
|
+
* First launch: user sees Chrome sign-in page → signs into Google → Chrome Sync pulls data.
|
|
28
|
+
* Subsequent launches: all data persists (cookies, passwords, bookmarks, extensions).
|
|
29
|
+
*/
|
|
30
|
+
export declare function launchProfile(name: string, options?: {
|
|
31
|
+
url?: string;
|
|
32
|
+
headless?: boolean;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
context: BrowserContext;
|
|
35
|
+
page: Page;
|
|
36
|
+
port: number;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Get active sessions
|
|
40
|
+
*/
|
|
41
|
+
export declare function getActiveSessions(): Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
port: number;
|
|
44
|
+
pageCount: number;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Get a page from an active session
|
|
48
|
+
*/
|
|
49
|
+
export declare function getSessionPage(name: string): Page | null;
|
|
50
|
+
/**
|
|
51
|
+
* Close a profile session
|
|
52
|
+
*/
|
|
53
|
+
export declare function closeProfile(name: string): Promise<void>;
|