@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/src/index.ts
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import {
|
|
10
|
+
listProfiles,
|
|
11
|
+
createProfile,
|
|
12
|
+
deleteProfile,
|
|
13
|
+
launchProfile,
|
|
14
|
+
closeProfile,
|
|
15
|
+
getActiveSessions,
|
|
16
|
+
getSessionPage,
|
|
17
|
+
} from "./profiles.js";
|
|
18
|
+
|
|
19
|
+
const server = new Server(
|
|
20
|
+
{
|
|
21
|
+
name: "chrome-pilot",
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
capabilities: {
|
|
26
|
+
tools: {},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// ─── Tool Definitions ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
34
|
+
tools: [
|
|
35
|
+
// Profile Management
|
|
36
|
+
{
|
|
37
|
+
name: "list_profiles",
|
|
38
|
+
description:
|
|
39
|
+
"List all Chrome profiles available in chrome-pilot. Shows profile name and linked Google account.",
|
|
40
|
+
inputSchema: { type: "object", properties: {} },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "create_profile",
|
|
44
|
+
description:
|
|
45
|
+
"Create a new Chrome profile. After creation, launch it to sign into Google and sync your data.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
name: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description:
|
|
52
|
+
'Profile name (e.g. "personal", "work", "client-abc")',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["name"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "delete_profile",
|
|
60
|
+
description: "Delete a Chrome profile and all its data.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
name: { type: "string", description: "Profile name to delete" },
|
|
65
|
+
},
|
|
66
|
+
required: ["name"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "launch",
|
|
71
|
+
description:
|
|
72
|
+
"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.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
profile: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Profile name to launch",
|
|
79
|
+
},
|
|
80
|
+
url: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Optional URL to navigate to after launch",
|
|
83
|
+
},
|
|
84
|
+
headless: {
|
|
85
|
+
type: "boolean",
|
|
86
|
+
description: "Run in headless mode (default: false)",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["profile"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "close",
|
|
94
|
+
description: "Close a Chrome profile session.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
profile: { type: "string", description: "Profile name to close" },
|
|
99
|
+
},
|
|
100
|
+
required: ["profile"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "get_active",
|
|
105
|
+
description:
|
|
106
|
+
"List all currently running Chrome sessions with their profiles and CDP ports.",
|
|
107
|
+
inputSchema: { type: "object", properties: {} },
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Browser Control
|
|
111
|
+
{
|
|
112
|
+
name: "navigate",
|
|
113
|
+
description: "Navigate to a URL in the active page of a profile.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
profile: { type: "string", description: "Profile name" },
|
|
118
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
119
|
+
},
|
|
120
|
+
required: ["profile", "url"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "screenshot",
|
|
125
|
+
description:
|
|
126
|
+
"Take a screenshot of the active page. Returns base64 encoded image.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
profile: { type: "string", description: "Profile name" },
|
|
131
|
+
},
|
|
132
|
+
required: ["profile"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "click",
|
|
137
|
+
description: "Click an element on the page by CSS selector.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
profile: { type: "string", description: "Profile name" },
|
|
142
|
+
selector: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "CSS selector of element to click",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ["profile", "selector"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "type",
|
|
152
|
+
description: "Type text into the currently focused element.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
profile: { type: "string", description: "Profile name" },
|
|
157
|
+
text: { type: "string", description: "Text to type" },
|
|
158
|
+
},
|
|
159
|
+
required: ["profile", "text"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "fill",
|
|
164
|
+
description: "Fill a form field by selector with the given value.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
profile: { type: "string", description: "Profile name" },
|
|
169
|
+
selector: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "CSS selector of the input field",
|
|
172
|
+
},
|
|
173
|
+
value: { type: "string", description: "Value to fill" },
|
|
174
|
+
},
|
|
175
|
+
required: ["profile", "selector", "value"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "eval",
|
|
180
|
+
description: "Evaluate JavaScript in the page context.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
profile: { type: "string", description: "Profile name" },
|
|
185
|
+
expression: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "JavaScript expression to evaluate",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["profile", "expression"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "snap",
|
|
195
|
+
description:
|
|
196
|
+
"Get the accessibility tree of the current page. Useful for understanding page structure without screenshots.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
profile: { type: "string", description: "Profile name" },
|
|
201
|
+
},
|
|
202
|
+
required: ["profile"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Tab Management
|
|
207
|
+
{
|
|
208
|
+
name: "list_tabs",
|
|
209
|
+
description: "List all open tabs in a profile session.",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
profile: { type: "string", description: "Profile name" },
|
|
214
|
+
},
|
|
215
|
+
required: ["profile"],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "new_tab",
|
|
220
|
+
description: "Open a new tab in a profile session.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
profile: { type: "string", description: "Profile name" },
|
|
225
|
+
url: { type: "string", description: "URL to open in the new tab" },
|
|
226
|
+
},
|
|
227
|
+
required: ["profile"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
// ─── Tool Handlers ──────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function getPage(profile: string) {
|
|
236
|
+
const page = getSessionPage(profile);
|
|
237
|
+
if (!page) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`No active session for profile "${profile}". Use launch() first.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return page;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
246
|
+
const { name, arguments: args } = request.params;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
switch (name) {
|
|
250
|
+
// ── Profile Management ──
|
|
251
|
+
|
|
252
|
+
case "list_profiles": {
|
|
253
|
+
const profiles = listProfiles();
|
|
254
|
+
if (profiles.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: 'No profiles found. Use create_profile to create one, or launch("profile-name") to auto-create and launch.',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const active = getActiveSessions();
|
|
265
|
+
const activeNames = new Set(active.map((s) => s.name));
|
|
266
|
+
const text = profiles
|
|
267
|
+
.map(
|
|
268
|
+
(p) =>
|
|
269
|
+
`${activeNames.has(p.name) ? "🟢" : "⚪"} ${p.name}${p.lastUsed ? ` (${p.lastUsed})` : ""}`
|
|
270
|
+
)
|
|
271
|
+
.join("\n");
|
|
272
|
+
return { content: [{ type: "text", text }] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case "create_profile": {
|
|
276
|
+
const profile = createProfile(args!.name as string);
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `Created profile "${profile.name}". Use launch("${profile.name}") to open Chrome and sign into Google.`,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "delete_profile": {
|
|
288
|
+
deleteProfile(args!.name as string);
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: `Deleted profile "${args!.name}".`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
case "launch": {
|
|
300
|
+
const { context, page, port } = await launchProfile(
|
|
301
|
+
args!.profile as string,
|
|
302
|
+
{
|
|
303
|
+
url: args?.url as string | undefined,
|
|
304
|
+
headless: args?.headless as boolean | undefined,
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
const url = page.url();
|
|
308
|
+
return {
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: `Launched profile "${args!.profile}" on CDP port ${port}.\nCurrent page: ${url}\nPages open: ${context.pages().length}`,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case "close": {
|
|
319
|
+
await closeProfile(args!.profile as string);
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{ type: "text", text: `Closed profile "${args!.profile}".` },
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case "get_active": {
|
|
328
|
+
const sessions = getActiveSessions();
|
|
329
|
+
if (sessions.length === 0) {
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: "No active sessions." }],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const text = sessions
|
|
335
|
+
.map(
|
|
336
|
+
(s) => `🟢 ${s.name} — port ${s.port}, ${s.pageCount} tab(s)`
|
|
337
|
+
)
|
|
338
|
+
.join("\n");
|
|
339
|
+
return { content: [{ type: "text", text }] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Browser Control ──
|
|
343
|
+
|
|
344
|
+
case "navigate": {
|
|
345
|
+
const page = getPage(args!.profile as string);
|
|
346
|
+
await page.goto(args!.url as string, {
|
|
347
|
+
waitUntil: "domcontentloaded",
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: `Navigated to ${args!.url}`,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "screenshot": {
|
|
360
|
+
const page = getPage(args!.profile as string);
|
|
361
|
+
const buffer = await page.screenshot();
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "image",
|
|
366
|
+
data: buffer.toString("base64"),
|
|
367
|
+
mimeType: "image/png",
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case "click": {
|
|
374
|
+
const page = getPage(args!.profile as string);
|
|
375
|
+
await page.click(args!.selector as string);
|
|
376
|
+
return {
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: "text",
|
|
380
|
+
text: `Clicked "${args!.selector}"`,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case "type": {
|
|
387
|
+
const page = getPage(args!.profile as string);
|
|
388
|
+
await page.keyboard.type(args!.text as string);
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `Typed "${(args!.text as string).substring(0, 50)}${(args!.text as string).length > 50 ? "..." : ""}"`,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case "fill": {
|
|
400
|
+
const page = getPage(args!.profile as string);
|
|
401
|
+
await page.fill(args!.selector as string, args!.value as string);
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: `Filled "${args!.selector}" with value`,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case "eval": {
|
|
413
|
+
const page = getPage(args!.profile as string);
|
|
414
|
+
const result = await page.evaluate(args!.expression as string);
|
|
415
|
+
return {
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: "text",
|
|
419
|
+
text:
|
|
420
|
+
typeof result === "string"
|
|
421
|
+
? result
|
|
422
|
+
: JSON.stringify(result, null, 2),
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case "snap": {
|
|
429
|
+
const page = getPage(args!.profile as string);
|
|
430
|
+
const snapshot = await page.evaluate(() => {
|
|
431
|
+
function getAccessibilityTree(el: Element, depth = 0): string {
|
|
432
|
+
const role = el.getAttribute("role") || el.tagName.toLowerCase();
|
|
433
|
+
const text = el.textContent?.trim().substring(0, 80) || "";
|
|
434
|
+
const label = el.getAttribute("aria-label") || "";
|
|
435
|
+
const indent = " ".repeat(depth);
|
|
436
|
+
let line = `${indent}[${role}]`;
|
|
437
|
+
if (label) line += ` "${label}"`;
|
|
438
|
+
else if (text && !el.children.length) line += ` "${text}"`;
|
|
439
|
+
const lines = [line];
|
|
440
|
+
for (const child of el.children) {
|
|
441
|
+
lines.push(...getAccessibilityTree(child, depth + 1).split("\n"));
|
|
442
|
+
}
|
|
443
|
+
return lines.filter(Boolean).join("\n");
|
|
444
|
+
}
|
|
445
|
+
return getAccessibilityTree(document.body);
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
content: [
|
|
449
|
+
{
|
|
450
|
+
type: "text",
|
|
451
|
+
text: snapshot,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Tab Management ──
|
|
458
|
+
|
|
459
|
+
case "list_tabs": {
|
|
460
|
+
const page = getPage(args!.profile as string);
|
|
461
|
+
const context = page.context();
|
|
462
|
+
const tabs = context.pages().map((p, i) => `${i}: ${p.url()}`);
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: "text", text: tabs.join("\n") }],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case "new_tab": {
|
|
469
|
+
const page = getPage(args!.profile as string);
|
|
470
|
+
const context = page.context();
|
|
471
|
+
const newPage = await context.newPage();
|
|
472
|
+
if (args?.url) {
|
|
473
|
+
await newPage.goto(args.url as string, {
|
|
474
|
+
waitUntil: "domcontentloaded",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
content: [
|
|
479
|
+
{
|
|
480
|
+
type: "text",
|
|
481
|
+
text: `Opened new tab${args?.url ? `: ${args.url}` : ""}`,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
default:
|
|
488
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
content: [
|
|
493
|
+
{
|
|
494
|
+
type: "text",
|
|
495
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
isError: true,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ─── Start Server ───────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
async function main() {
|
|
506
|
+
const transport = new StdioServerTransport();
|
|
507
|
+
await server.connect(transport);
|
|
508
|
+
console.error("chrome-pilot MCP server running");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
main().catch((error) => {
|
|
512
|
+
console.error("Fatal:", error);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
});
|