@mehmoodqureshi/chrome-mcp 0.9.4 → 0.9.6
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 +35 -17
- package/dist/shared/page-fns.js +64 -0
- package/dist/shared/protocol.d.ts +7 -0
- package/dist/src/bridge/evict.d.ts +5 -5
- package/dist/src/bridge/evict.js +5 -5
- package/dist/src/bridge/peer.d.ts +94 -0
- package/dist/src/bridge/peer.js +187 -0
- package/dist/src/bridge/profiles.d.ts +44 -0
- package/dist/src/bridge/profiles.js +121 -0
- package/dist/src/bridge/server.d.ts +84 -4
- package/dist/src/bridge/server.js +267 -14
- package/dist/src/cli.js +34 -18
- package/dist/src/config.js +4 -2
- package/dist/src/executor/extension-executor.d.ts +3 -0
- package/dist/src/executor/extension-executor.js +9 -0
- package/dist/src/executor/select.js +5 -1
- package/dist/src/mcp/tools.d.ts +2 -0
- package/dist/src/mcp/tools.js +72 -4
- package/extension-dist/background.js +71 -6
- package/extension-dist/manifest.json +1 -1
- package/extension-dist/options.html +7 -5
- package/extension-dist/options.js +12 -4
- package/package.json +1 -1
|
@@ -81,6 +81,15 @@ class ExtensionExecutor {
|
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
/** Why this executor can't serve the active profile right now: not paired, or
|
|
85
|
+
* paired but not answering pings. Used for the selector's NO_BACKEND message. */
|
|
86
|
+
unavailableReason() {
|
|
87
|
+
const profile = this.activeProfile();
|
|
88
|
+
if (!this.bridge.hasConnection(profile))
|
|
89
|
+
return this.bridge.noPairMessage(profile);
|
|
90
|
+
return (`The browser paired for profile "${profile}" is not responding. Open that Chrome, ` +
|
|
91
|
+
`or reload the chrome-mcp extension in chrome://extensions, then retry.`);
|
|
92
|
+
}
|
|
84
93
|
async dispose() {
|
|
85
94
|
// Never close the user's Chrome.
|
|
86
95
|
}
|
|
@@ -73,7 +73,11 @@ function createSelector(deps) {
|
|
|
73
73
|
if (c)
|
|
74
74
|
return c;
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
// CDP is off in the published build, so the extension is the only way in:
|
|
77
|
+
// name the profile and the exact Options to set rather than suggest flags
|
|
78
|
+
// that are ignored.
|
|
79
|
+
const reason = ext.unavailableReason?.();
|
|
80
|
+
throw new types_1.ExecutorError('NO_BACKEND', reason ?? 'No Chrome available: pair the chrome-mcp extension (Options: Port, token, Profile).');
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
//# sourceMappingURL=select.js.map
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import type { Executor } from '../executor/types';
|
|
17
17
|
import { type Policy } from '../security/policy';
|
|
18
|
+
import type { BridgeServer } from '../bridge/server';
|
|
18
19
|
export interface ToolDefinition {
|
|
19
20
|
name: string;
|
|
20
21
|
description: string;
|
|
@@ -48,6 +49,7 @@ interface ToolCtx {
|
|
|
48
49
|
policy: Policy;
|
|
49
50
|
}
|
|
50
51
|
type ToolHandler = (args: Record<string, unknown>, ctx: ToolCtx) => Promise<CallToolResult>;
|
|
52
|
+
export declare function setProfileBridge(bridge: BridgeServer | null): void;
|
|
51
53
|
export declare const TOOL_HANDLERS: Record<string, ToolHandler>;
|
|
52
54
|
/** Reset limiter state — for tests that exercise the ceiling. */
|
|
53
55
|
export declare function resetRateLimiter(): void;
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -16,10 +16,12 @@ exports.TOOL_HANDLERS = exports.TOOL_NAMES = exports.TOOL_DEFINITIONS = void 0;
|
|
|
16
16
|
exports.setToolAllowlist = setToolAllowlist;
|
|
17
17
|
exports.isToolEnabled = isToolEnabled;
|
|
18
18
|
exports.enabledToolNames = enabledToolNames;
|
|
19
|
+
exports.setProfileBridge = setProfileBridge;
|
|
19
20
|
exports.resetRateLimiter = resetRateLimiter;
|
|
20
21
|
exports.dispatchToolCall = dispatchToolCall;
|
|
21
22
|
exports.assertNoDrift = assertNoDrift;
|
|
22
23
|
exports.registerTools = registerTools;
|
|
24
|
+
const node_fs_1 = require("node:fs");
|
|
23
25
|
const node_path_1 = require("node:path");
|
|
24
26
|
const zod_1 = require("zod");
|
|
25
27
|
const types_1 = require("../executor/types");
|
|
@@ -36,6 +38,7 @@ const auth_wall_1 = require("../../shared/auth-wall");
|
|
|
36
38
|
const audit_1 = require("./audit");
|
|
37
39
|
const log_1 = require("./log");
|
|
38
40
|
const tasks_1 = require("../bridge/tasks");
|
|
41
|
+
const config_1 = require("../config");
|
|
39
42
|
const workspace_1 = require("../bridge/workspace");
|
|
40
43
|
const validators_1 = require("./validators");
|
|
41
44
|
/**
|
|
@@ -179,9 +182,10 @@ exports.TOOL_DEFINITIONS = [
|
|
|
179
182
|
tabId: tabIdField,
|
|
180
183
|
},
|
|
181
184
|
},
|
|
182
|
-
{ name: 'chrome_status', description: 'Report backend/session status.', inputSchema: {} },
|
|
185
|
+
{ name: 'chrome_status', description: 'Report backend/session status, including every paired browser profile and how it was named.', inputSchema: {} },
|
|
183
186
|
{ name: 'auth_check', description: 'Is the tab sitting on a sign-in wall? Reads the page (URL, title, password fields, sign-in controls) and returns { authRequired, confidence, signals }. Use it after a navigate, or whenever a step fails unexpectedly, to tell "the session expired" apart from "the agent got lost". Pass failOnAuthWall:true to get an [AUTH_REQUIRED] error instead of a verdict, so a harness can bucket the run as an auth failure.', inputSchema: { failOnAuthWall: authWallField, ...FRAME_PROPS, tabId: tabIdField } },
|
|
184
187
|
{ name: 'profile_use', description: 'Switch the active browser profile (identity). Subsequent downloads, results, screenshots, and the action log are stored under profiles/<name>/. Resets the active task to "default" unless you then call task_new.', inputSchema: { name: zod_1.z.string().describe('Profile name (becomes a folder; sanitized to a safe path segment).') } },
|
|
188
|
+
{ name: 'profile_rename', description: 'Rename an automatically named browser profile (e.g. "profile-2" -> "work"). The name sticks across restarts; its saved artifacts move with it.', inputSchema: { from: zod_1.z.string().describe('Current profile name'), to: zod_1.z.string().describe('New profile name') } },
|
|
185
189
|
{ name: 'task_new', description: 'Start a new task (run) under the active profile. Creates profiles/<profile>/tasks/<name>/ with downloads/, results/, screenshots/ and makes it the active task so all captured artifacts land there.', inputSchema: { name: zod_1.z.string().describe('Task name (becomes a folder; sanitized to a safe path segment).') } },
|
|
186
190
|
{ name: 'tasks_list', description: 'List every task across all profiles under the data dir, with sizes and download counts.', inputSchema: {} },
|
|
187
191
|
{ name: 'task_status', description: 'Report the active profile/task and the folder paths where this run\'s artifacts are stored.', inputSchema: {} },
|
|
@@ -480,10 +484,21 @@ const waitUntil = (args) => (0, validators_1.optionalString)(args, 'waitUntil');
|
|
|
480
484
|
* the active tab when `tabId` is omitted, which races under concurrency. */
|
|
481
485
|
const PARALLEL_TAB_EXEMPT = new Set([
|
|
482
486
|
'tabs_list', 'tab_new', 'chrome_status', 'batch',
|
|
483
|
-
'profile_use', 'task_new', 'tasks_list', 'task_status',
|
|
487
|
+
'profile_use', 'profile_rename', 'task_new', 'tasks_list', 'task_status',
|
|
484
488
|
]);
|
|
485
489
|
/** Server-side tools that manage the task workspace and need no browser backend. */
|
|
486
|
-
const NO_BACKEND_TOOLS = new Set([
|
|
490
|
+
const NO_BACKEND_TOOLS = new Set([
|
|
491
|
+
'profile_use', 'profile_rename', 'task_new', 'tasks_list', 'task_status',
|
|
492
|
+
// Must answer even when the active profile has no browser — that is exactly
|
|
493
|
+
// when you need it to see which profiles ARE paired.
|
|
494
|
+
'chrome_status',
|
|
495
|
+
]);
|
|
496
|
+
/** The bridge, for the profile tools that need live connection state. Set by
|
|
497
|
+
* the CLI once the bridge is up; null in tests that don't run one. */
|
|
498
|
+
let profileBridge = null;
|
|
499
|
+
function setProfileBridge(bridge) {
|
|
500
|
+
profileBridge = bridge;
|
|
501
|
+
}
|
|
487
502
|
/** Project a Workspace to the path fields worth returning to the caller. */
|
|
488
503
|
function workspaceView(w) {
|
|
489
504
|
return {
|
|
@@ -911,7 +926,22 @@ exports.TOOL_HANDLERS = {
|
|
|
911
926
|
...(path ? {} : { note: 'no active task workspace, so the PDF was not saved to disk' }),
|
|
912
927
|
});
|
|
913
928
|
},
|
|
914
|
-
chrome_status: async (
|
|
929
|
+
chrome_status: async () => {
|
|
930
|
+
const profiles = profileBridge ? { profiles: profileBridge.pairedProfiles() } : {};
|
|
931
|
+
try {
|
|
932
|
+
const ex = await (0, manager_1.getManager)().ensureReady();
|
|
933
|
+
return (0, envelopes_1.jsonResult)({ ...ex.status(), ...profiles });
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
return (0, envelopes_1.jsonResult)({
|
|
937
|
+
ready: false,
|
|
938
|
+
backend: null,
|
|
939
|
+
detail: errMessage(err),
|
|
940
|
+
activeProfile: (0, workspace_1.peekActiveWorkspace)()?.profile ?? 'default',
|
|
941
|
+
...profiles,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
},
|
|
915
945
|
auth_check: async (a, ctx) => {
|
|
916
946
|
await gate(ctx, 'get_text', { tabId: tabId(a) }); // read of page structure
|
|
917
947
|
const snap = await ctx.ex.snapshot({ tabId: tabId(a), ...frameOpts(a), interactiveOnly: true, max: 200 });
|
|
@@ -932,6 +962,44 @@ exports.TOOL_HANDLERS = {
|
|
|
932
962
|
(0, snapdiff_1.resetSnapshots)();
|
|
933
963
|
return (0, envelopes_1.jsonResult)(workspaceView((0, workspace_1.switchWorkspace)({ profile: (0, validators_1.requireString)(a, 'name') })));
|
|
934
964
|
},
|
|
965
|
+
profile_rename: async (a) => {
|
|
966
|
+
if (!profileBridge)
|
|
967
|
+
throw new types_1.ExecutorError('NO_BACKEND', 'profile_rename needs the chrome-mcp bridge running');
|
|
968
|
+
let from;
|
|
969
|
+
let to;
|
|
970
|
+
try {
|
|
971
|
+
// Sanitize once so the folder move and the active-profile check below
|
|
972
|
+
// use the same name the bridge routes by.
|
|
973
|
+
from = (0, config_1.sanitizeName)((0, validators_1.requireString)(a, 'from'), 'profile');
|
|
974
|
+
to = await profileBridge.renameProfile(from, (0, validators_1.requireString)(a, 'to'));
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
// A refusal (typed-in name, name taken, bad name) is the caller's to fix, not a crash.
|
|
978
|
+
throw new validators_1.McpToolError(err instanceof Error ? err.message : String(err));
|
|
979
|
+
}
|
|
980
|
+
// Carry the profile's saved artifacts over, unless the new name already has
|
|
981
|
+
// a folder of its own (never merge or overwrite).
|
|
982
|
+
const ws = (0, workspace_1.getActiveWorkspace)();
|
|
983
|
+
const oldDir = (0, config_1.resolveProfileDir)(ws.dataDir, from);
|
|
984
|
+
const newDir = (0, config_1.resolveProfileDir)(ws.dataDir, to);
|
|
985
|
+
let artifactsMoved = false;
|
|
986
|
+
if (to !== from && (0, node_fs_1.existsSync)(oldDir) && !(0, node_fs_1.existsSync)(newDir)) {
|
|
987
|
+
try {
|
|
988
|
+
(0, node_fs_1.renameSync)(oldDir, newDir);
|
|
989
|
+
artifactsMoved = true;
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
/* leave them where they are; the rename itself already succeeded */
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Keep routing to the same browser if it was the active one.
|
|
996
|
+
let active;
|
|
997
|
+
if (ws.profile === from) {
|
|
998
|
+
(0, snapdiff_1.resetSnapshots)();
|
|
999
|
+
active = workspaceView((0, workspace_1.switchWorkspace)({ profile: to }));
|
|
1000
|
+
}
|
|
1001
|
+
return (0, envelopes_1.jsonResult)({ from, to, artifactsMoved, ...(active ? { active } : {}) });
|
|
1002
|
+
},
|
|
935
1003
|
task_new: async (a) => (0, envelopes_1.jsonResult)(workspaceView((0, workspace_1.switchWorkspace)({ task: (0, validators_1.requireString)(a, 'name') }))),
|
|
936
1004
|
task_status: async () => (0, envelopes_1.jsonResult)(workspaceView((0, workspace_1.getActiveWorkspace)())),
|
|
937
1005
|
tasks_list: async () => (0, envelopes_1.jsonResult)((0, tasks_1.listTasks)((0, workspace_1.getActiveWorkspace)().dataDir)),
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
isConnected() {
|
|
49
49
|
return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
|
|
50
50
|
}
|
|
51
|
-
connect(port, token, profile) {
|
|
51
|
+
connect(port, token, profile, installId) {
|
|
52
52
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
token,
|
|
69
69
|
ext: { id: chrome.runtime.id, version: chrome.runtime.getManifest().version, chrome: chromeVersion() },
|
|
70
70
|
profile: profile && profile.trim() ? profile.trim() : void 0,
|
|
71
|
+
installId,
|
|
71
72
|
// This build gates fail-closed and reports tab URLs on results, so the
|
|
72
73
|
// server may skip its pre-flight tabs_list. An older build omits this and
|
|
73
74
|
// the server keeps fetching the URL itself.
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
switch (frame.type) {
|
|
86
87
|
case "welcome":
|
|
87
88
|
this.deps.onPolicy(frame.policy);
|
|
89
|
+
if (typeof frame.profile === "string") this.deps.onProfile?.(frame.profile);
|
|
88
90
|
this.setState("connected");
|
|
89
91
|
this.deps.log("paired with server");
|
|
90
92
|
break;
|
|
@@ -500,6 +502,26 @@
|
|
|
500
502
|
else node.value = next;
|
|
501
503
|
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
502
504
|
};
|
|
505
|
+
const clearEditable = (host) => {
|
|
506
|
+
host.focus();
|
|
507
|
+
const sel2 = window.getSelection();
|
|
508
|
+
if (!sel2) return;
|
|
509
|
+
const range = document.createRange();
|
|
510
|
+
range.selectNodeContents(host);
|
|
511
|
+
sel2.removeAllRanges();
|
|
512
|
+
sel2.addRange(range);
|
|
513
|
+
document.execCommand("delete");
|
|
514
|
+
};
|
|
515
|
+
const caretToEnd = (host) => {
|
|
516
|
+
host.focus();
|
|
517
|
+
const sel2 = window.getSelection();
|
|
518
|
+
if (!sel2) return;
|
|
519
|
+
const range = document.createRange();
|
|
520
|
+
range.selectNodeContents(host);
|
|
521
|
+
range.collapse(false);
|
|
522
|
+
sel2.removeAllRanges();
|
|
523
|
+
sel2.addRange(range);
|
|
524
|
+
};
|
|
503
525
|
const perform = (el2) => {
|
|
504
526
|
const missing = sel !== null && el2 === null;
|
|
505
527
|
switch (a.op) {
|
|
@@ -524,6 +546,14 @@
|
|
|
524
546
|
return { found: true };
|
|
525
547
|
}
|
|
526
548
|
case "type": {
|
|
549
|
+
if (!el2) return { found: false };
|
|
550
|
+
if (el2.isContentEditable) {
|
|
551
|
+
if (a.clear) clearEditable(el2);
|
|
552
|
+
else caretToEnd(el2);
|
|
553
|
+
if (a.text) document.execCommand("insertText", false, a.text);
|
|
554
|
+
el2.dispatchEvent(new Event("change", { bubbles: true }));
|
|
555
|
+
return { found: true };
|
|
556
|
+
}
|
|
527
557
|
const node = el2;
|
|
528
558
|
if (!node) return { found: false };
|
|
529
559
|
node.focus();
|
|
@@ -533,6 +563,15 @@
|
|
|
533
563
|
return { found: true };
|
|
534
564
|
}
|
|
535
565
|
case "focus": {
|
|
566
|
+
if (!el2) return { found: false };
|
|
567
|
+
if (el2.isContentEditable) {
|
|
568
|
+
el2.focus();
|
|
569
|
+
if (a.clear) {
|
|
570
|
+
clearEditable(el2);
|
|
571
|
+
caretToEnd(el2);
|
|
572
|
+
}
|
|
573
|
+
return { found: true };
|
|
574
|
+
}
|
|
536
575
|
const node = el2;
|
|
537
576
|
if (!node) return { found: false };
|
|
538
577
|
node.focus();
|
|
@@ -1041,9 +1080,22 @@
|
|
|
1041
1080
|
});
|
|
1042
1081
|
}
|
|
1043
1082
|
async function trustedType(tabId, selector, text, clear, frameIds, pressEnter = false) {
|
|
1044
|
-
const focused = await execOp(tabId, withWait({ op: "focus", selector, clear }), frameIds);
|
|
1083
|
+
const focused = await execOp(tabId, withWait({ op: "focus", selector, clear: false }), frameIds);
|
|
1045
1084
|
if (!focused.found) return false;
|
|
1046
1085
|
await withDebugger(tabId, async (t) => {
|
|
1086
|
+
if (clear) {
|
|
1087
|
+
for (const type of ["keyDown", "keyUp"]) {
|
|
1088
|
+
await chrome.debugger.sendCommand(t, "Input.dispatchKeyEvent", {
|
|
1089
|
+
type,
|
|
1090
|
+
key: "a",
|
|
1091
|
+
code: "KeyA",
|
|
1092
|
+
windowsVirtualKeyCode: 65,
|
|
1093
|
+
modifiers: 8,
|
|
1094
|
+
// Meta
|
|
1095
|
+
...type === "keyDown" ? { commands: ["selectAll"] } : {}
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1047
1099
|
await chrome.debugger.sendCommand(t, "Input.insertText", { text });
|
|
1048
1100
|
if (!pressEnter) return;
|
|
1049
1101
|
for (const type of ["keyDown", "keyUp"]) {
|
|
@@ -1809,6 +1861,8 @@
|
|
|
1809
1861
|
currentPolicy = policy;
|
|
1810
1862
|
void syncObserverScript(policy, (m) => console.debug("[chrome-mcp]", m));
|
|
1811
1863
|
},
|
|
1864
|
+
// Shown on the Options page, so you can tell which name this browser got.
|
|
1865
|
+
onProfile: (profile) => void chrome.storage.local.set({ pairedProfile: profile }),
|
|
1812
1866
|
log: (m) => console.debug("[chrome-mcp]", m)
|
|
1813
1867
|
});
|
|
1814
1868
|
var router = new CommandRouter({
|
|
@@ -1853,11 +1907,22 @@
|
|
|
1853
1907
|
} catch {
|
|
1854
1908
|
}
|
|
1855
1909
|
}
|
|
1910
|
+
async function ensureInstallId(installId) {
|
|
1911
|
+
if (typeof installId === "string" && installId.length > 0) return installId;
|
|
1912
|
+
const fresh = crypto.randomUUID();
|
|
1913
|
+
await chrome.storage.local.set({ installId: fresh });
|
|
1914
|
+
return fresh;
|
|
1915
|
+
}
|
|
1856
1916
|
async function getConfig() {
|
|
1857
|
-
const { wsPort, token, profile } = await chrome.storage.local.get([
|
|
1917
|
+
const { wsPort, token, profile, installId } = await chrome.storage.local.get([
|
|
1918
|
+
"wsPort",
|
|
1919
|
+
"token",
|
|
1920
|
+
"profile",
|
|
1921
|
+
"installId"
|
|
1922
|
+
]);
|
|
1858
1923
|
if (typeof wsPort === "number" && wsPort > 0 && typeof token === "string" && token.length > 0) {
|
|
1859
|
-
const
|
|
1860
|
-
return { wsPort, token, profile:
|
|
1924
|
+
const label = typeof profile === "string" ? profile.trim() : "";
|
|
1925
|
+
return { wsPort, token, profile: label, installId: await ensureInstallId(installId) };
|
|
1861
1926
|
}
|
|
1862
1927
|
return null;
|
|
1863
1928
|
}
|
|
@@ -1884,7 +1949,7 @@
|
|
|
1884
1949
|
if (ws.isConnected() || ws.state === "unauthorized") return;
|
|
1885
1950
|
const cfg = await getConfig();
|
|
1886
1951
|
if (!cfg) return;
|
|
1887
|
-
ws.connect(cfg.wsPort, cfg.token, cfg.profile);
|
|
1952
|
+
ws.connect(cfg.wsPort, cfg.token, cfg.profile, cfg.installId);
|
|
1888
1953
|
}
|
|
1889
1954
|
async function keepalivePulse() {
|
|
1890
1955
|
await chrome.storage.local.get("connState");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "MCP Extension for Chrome",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.6",
|
|
5
5
|
"description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
|
|
6
6
|
"homepage_url": "https://chrome-mcp-omega.vercel.app",
|
|
7
7
|
"minimum_chrome_version": "116",
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
<label for="port">Port</label>
|
|
26
26
|
<input id="port" type="number" inputmode="numeric" placeholder="38017" />
|
|
27
27
|
<label for="token">Token</label>
|
|
28
|
-
<input id="token" type="password" placeholder="
|
|
28
|
+
<input id="token" type="password" placeholder="leave blank to keep the current token" />
|
|
29
29
|
<label for="profile">Profile</label>
|
|
30
|
-
<input id="profile" type="text" placeholder="
|
|
30
|
+
<input id="profile" type="text" placeholder="automatic" />
|
|
31
31
|
<p class="hint">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Optional. Leave blank and the server names this browser for you — the first is
|
|
33
|
+
<code>default</code>, then <code>profile-2</code>, <code>profile-3</code>… — and
|
|
34
|
+
remembers it. Rename one with <code>profile_rename</code>, switch with
|
|
35
|
+
<code>profile_use</code>. Type a name here only to pin one yourself.
|
|
35
36
|
</p>
|
|
37
|
+
<p class="hint" id="paired-as"></p>
|
|
36
38
|
<button id="save">Save & connect</button>
|
|
37
39
|
<div class="status" id="status">Status: unknown</div>
|
|
38
40
|
<script src="options.js"></script>
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
var saveEl = document.getElementById("save");
|
|
11
11
|
var statusEl = document.getElementById("status");
|
|
12
12
|
var sourceEl = document.getElementById("source");
|
|
13
|
+
var pairedAsEl = document.getElementById("paired-as");
|
|
14
|
+
function renderPairedAs(name) {
|
|
15
|
+
pairedAsEl.textContent = typeof name === "string" && name ? `This browser is paired as "${name}".` : "";
|
|
16
|
+
}
|
|
13
17
|
async function loadExisting() {
|
|
14
|
-
const { wsPort, profile, connState, pairingSource } = await chrome.storage.local.get([
|
|
18
|
+
const { wsPort, profile, connState, pairingSource, pairedProfile } = await chrome.storage.local.get([
|
|
15
19
|
"wsPort",
|
|
16
20
|
"profile",
|
|
17
21
|
"connState",
|
|
18
|
-
"pairingSource"
|
|
22
|
+
"pairingSource",
|
|
23
|
+
"pairedProfile"
|
|
19
24
|
]);
|
|
25
|
+
renderPairedAs(pairedProfile);
|
|
20
26
|
sourceEl.textContent = pairingSource === "auto" ? "Paired automatically from the pairing.json the server wrote into this extension folder. Saving here overrides it." : pairingSource === "manual" ? "Paired by hand. Saved values take precedence over the bundled pairing.json." : typeof wsPort === "number" && wsPort > 0 && connState !== "idle" ? "Paired with values saved by an earlier version. Saving here keeps them manual." : "Not paired yet. If you loaded this extension from the chrome-mcp package folder, start the server once and it pairs itself; otherwise paste the values below.";
|
|
21
27
|
portEl.value = typeof wsPort === "number" && wsPort > 0 ? String(wsPort) : String(DEFAULT_WS_PORT);
|
|
22
28
|
profileEl.value = typeof profile === "string" ? profile : "";
|
|
@@ -33,8 +39,9 @@
|
|
|
33
39
|
}
|
|
34
40
|
saveEl.addEventListener("click", async () => {
|
|
35
41
|
const wsPort = Number(portEl.value);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
42
|
+
const stored = await chrome.storage.local.get("token");
|
|
43
|
+
const token = tokenEl.value.trim() || (typeof stored.token === "string" ? stored.token : "");
|
|
44
|
+
const profile = profileEl.value.trim();
|
|
38
45
|
if (!Number.isInteger(wsPort) || wsPort <= 0 || !token) {
|
|
39
46
|
statusEl.textContent = "Status: enter a valid port (> 0) and token";
|
|
40
47
|
return;
|
|
@@ -45,6 +52,7 @@
|
|
|
45
52
|
});
|
|
46
53
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
47
54
|
if (area === "local" && changes.connState) render(String(changes.connState.newValue));
|
|
55
|
+
if (area === "local" && changes.pairedProfile) renderPairedAs(changes.pairedProfile.newValue);
|
|
48
56
|
});
|
|
49
57
|
void loadExisting();
|
|
50
58
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Drive your real Chrome browser over MCP — real logins, real cookies. A stdio MCP server (CLI) plus an MV3 extension, driving Chrome via chrome.scripting/chrome.tabs. Multi-tab batch automation, accessibility snapshots, deny-all security by default.",
|
|
5
5
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
6
|
"license": "MIT",
|