@mehmoodqureshi/chrome-mcp 0.9.5 → 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.
@@ -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
- throw new types_1.ExecutorError('NO_BACKEND', 'No Chrome available: pair the extension, attach a --cdp-endpoint, or enable the CDP fallback.');
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
@@ -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;
@@ -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(['profile_use', 'task_new', 'tasks_list', 'task_status']);
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 (_a, ctx) => (0, envelopes_1.jsonResult)(ctx.ex.status()),
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;
@@ -1859,6 +1861,8 @@
1859
1861
  currentPolicy = policy;
1860
1862
  void syncObserverScript(policy, (m) => console.debug("[chrome-mcp]", m));
1861
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 }),
1862
1866
  log: (m) => console.debug("[chrome-mcp]", m)
1863
1867
  });
1864
1868
  var router = new CommandRouter({
@@ -1903,11 +1907,22 @@
1903
1907
  } catch {
1904
1908
  }
1905
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
+ }
1906
1916
  async function getConfig() {
1907
- const { wsPort, token, profile } = await chrome.storage.local.get(["wsPort", "token", "profile"]);
1917
+ const { wsPort, token, profile, installId } = await chrome.storage.local.get([
1918
+ "wsPort",
1919
+ "token",
1920
+ "profile",
1921
+ "installId"
1922
+ ]);
1908
1923
  if (typeof wsPort === "number" && wsPort > 0 && typeof token === "string" && token.length > 0) {
1909
- const name = typeof profile === "string" && profile.trim() ? profile.trim() : "default";
1910
- return { wsPort, token, profile: name };
1924
+ const label = typeof profile === "string" ? profile.trim() : "";
1925
+ return { wsPort, token, profile: label, installId: await ensureInstallId(installId) };
1911
1926
  }
1912
1927
  return null;
1913
1928
  }
@@ -1934,7 +1949,7 @@
1934
1949
  if (ws.isConnected() || ws.state === "unauthorized") return;
1935
1950
  const cfg = await getConfig();
1936
1951
  if (!cfg) return;
1937
- ws.connect(cfg.wsPort, cfg.token, cfg.profile);
1952
+ ws.connect(cfg.wsPort, cfg.token, cfg.profile, cfg.installId);
1938
1953
  }
1939
1954
  async function keepalivePulse() {
1940
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.5",
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="paste the handshake token" />
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="default" />
30
+ <input id="profile" type="text" placeholder="automatic" />
31
31
  <p class="hint">
32
- A name for this browser (e.g. <code>mehmood</code>). Several browsers can pair to the
33
- same port &amp; token at once, each with its own profile — switch between them with
34
- <code>profile_use</code>. Leave blank for <code>default</code>.
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 token = tokenEl.value.trim();
37
- const profile = profileEl.value.trim() || "default";
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.5",
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",