@mehmoodqureshi/chrome-mcp 0.9.6 → 0.9.7

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 CHANGED
@@ -520,6 +520,31 @@ group/other-accessible. Windows has no such bits — `chmod` there only toggles
520
520
  read-only attribute — so the check is skipped and the token's confidentiality
521
521
  rests on the per-user ACL of `%USERPROFILE%\.chrome-mcp`.
522
522
 
523
+ ## Telemetry
524
+
525
+ The chrome-mcp **server** sends anonymous usage statistics to PostHog, so the
526
+ project can see how many installs are active, which versions and platforms are
527
+ in use, and which tools fail most. A notice is printed the first time it runs.
528
+
529
+ What is sent: a random install id (kept in `~/.chrome-mcp/telemetry.json`), the
530
+ chrome-mcp version, OS, CPU architecture and Node major version, whether the
531
+ session owns the bridge port or shares it, how many browsers are paired, and
532
+ per-tool call and error **counts** with error codes — batched every 10 minutes.
533
+
534
+ What is never sent: URLs, domains, tool arguments, page content, screenshots,
535
+ cookies, profile names, tokens, file paths, or anything you type. Events are
536
+ personless and GeoIP lookup is disabled.
537
+
538
+ The **browser extension sends nothing** — it only ever talks to `127.0.0.1`.
539
+
540
+ Turn it off with any of:
541
+
542
+ ```bash
543
+ CHROME_MCP_TELEMETRY=0 # or false / off
544
+ DO_NOT_TRACK=1
545
+ --no-telemetry # server flag
546
+ ```
547
+
523
548
  ## Develop
524
549
 
525
550
  ```
package/dist/src/cli.js CHANGED
@@ -22,6 +22,7 @@ const workspace_1 = require("./bridge/workspace");
22
22
  const auth_1 = require("./bridge/auth");
23
23
  const server_2 = require("./mcp/server");
24
24
  const tools_1 = require("./mcp/tools");
25
+ const telemetry_1 = require("./telemetry");
25
26
  const extension_install_1 = require("./extension-install");
26
27
  /** Hard deadline for clean shutdown before we force-exit (a stuck socket must not hang us). */
27
28
  const SHUTDOWN_DEADLINE_MS = 3000;
@@ -242,6 +243,13 @@ async function main() {
242
243
  };
243
244
  const port = await bridge.start();
244
245
  (0, tools_1.setProfileBridge)(bridge);
246
+ (0, telemetry_1.initTelemetry)({
247
+ dataDir,
248
+ version: version(),
249
+ disabledByFlag: cfg.noTelemetry,
250
+ log: (m) => (0, server_2.logErr)(m),
251
+ context: () => ({ role: bridge.role, browsers: bridge.connectedProfiles().length }),
252
+ });
245
253
  // Never includes the pairing token — only the resolved, non-secret config.
246
254
  (0, server_2.logDebug)(`resolved config: ${JSON.stringify({
247
255
  wsPort: port,
@@ -309,7 +317,9 @@ async function main() {
309
317
  return;
310
318
  shuttingDown = true;
311
319
  cleanup();
312
- exitWithDeadline(Promise.allSettled([(0, server_2.stopMcpServer)(), bridge.stop()]));
320
+ // Telemetry first: its final summary reads the bridge's role and paired
321
+ // browsers, which bridge.stop() clears synchronously.
322
+ exitWithDeadline(Promise.allSettled([(0, telemetry_1.stopTelemetry)(), (0, server_2.stopMcpServer)(), bridge.stop()]));
313
323
  };
314
324
  process.on('SIGINT', shutdown);
315
325
  process.on('SIGTERM', shutdown);
@@ -34,6 +34,8 @@ export interface CliConfig {
34
34
  showExtensionPath: boolean;
35
35
  /** `--persist-token`: reuse a stable on-disk token so the extension never re-pairs. */
36
36
  persistToken: boolean;
37
+ /** `--no-telemetry`: send no anonymous usage statistics (see src/telemetry.ts). */
38
+ noTelemetry: boolean;
37
39
  /**
38
40
  * `--tools`: advertise only these tools. `undefined` = the whole catalog.
39
41
  * Names are validated against the catalog by `setToolAllowlist` at startup —
@@ -78,6 +78,7 @@ function parseArgs(argv) {
78
78
  let headless = false;
79
79
  let printPairing = false;
80
80
  let persistToken = false;
81
+ let noTelemetry = false;
81
82
  let showHelp = false;
82
83
  let showVersion = false;
83
84
  let showExtensionPath = false;
@@ -178,6 +179,9 @@ function parseArgs(argv) {
178
179
  case '--persist-token':
179
180
  persistToken = true;
180
181
  break;
182
+ case '--no-telemetry':
183
+ noTelemetry = true;
184
+ break;
181
185
  case '--tools':
182
186
  toolsFlagSeen = true;
183
187
  for (const name of splitList(requireValue(argv[++i], '--tools')))
@@ -234,6 +238,7 @@ function parseArgs(argv) {
234
238
  headless,
235
239
  printPairing,
236
240
  persistToken,
241
+ noTelemetry,
237
242
  showHelp,
238
243
  showVersion,
239
244
  showExtensionPath,
@@ -309,6 +314,10 @@ Connection:
309
314
  --persist-token Reuse a stable on-disk token across restarts so the
310
315
  extension never has to re-pair (default: fresh per boot).
311
316
  CHROME_MCP_TOKEN env, if set, pins the token explicitly.
317
+ --no-telemetry Send no anonymous usage statistics. Same as
318
+ CHROME_MCP_TELEMETRY=0 or DO_NOT_TRACK=1. Only counts are
319
+ ever sent (version, OS, tool calls, error codes) — never
320
+ URLs, page content or arguments.
312
321
 
313
322
  Backend:
314
323
  This build is EXTENSION-ONLY — it drives ONLY your real Chrome via the paired
@@ -38,6 +38,7 @@ const auth_wall_1 = require("../../shared/auth-wall");
38
38
  const audit_1 = require("./audit");
39
39
  const log_1 = require("./log");
40
40
  const tasks_1 = require("../bridge/tasks");
41
+ const telemetry_1 = require("../telemetry");
41
42
  const config_1 = require("../config");
42
43
  const workspace_1 = require("../bridge/workspace");
43
44
  const validators_1 = require("./validators");
@@ -1075,6 +1076,8 @@ function summarizeArgs(rawArgs) {
1075
1076
  */
1076
1077
  function recordHistory(tool, rawArgs, ok, extra = {}) {
1077
1078
  const a = extra.audit ?? {};
1079
+ // Counts only — the tool name and error code, never the args or URL below.
1080
+ (0, telemetry_1.noteToolCall)(tool, ok, extra.error);
1078
1081
  (0, workspace_1.appendHistory)({
1079
1082
  ts: new Date().toISOString(),
1080
1083
  tool,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * src/telemetry.ts — anonymous usage statistics from the chrome-mcp SERVER.
3
+ *
4
+ * What it sends, to PostHog: a random per-install id, the chrome-mcp version,
5
+ * OS, CPU architecture and Node major version, whether this session owns the
6
+ * port or shares it, how many browsers are paired, and per-tool call and error
7
+ * COUNTS. Never URLs, domains, tool arguments, page content, profile names,
8
+ * tokens, file paths, or anything typed. Events are marked personless and ask
9
+ * PostHog not to geolocate them.
10
+ *
11
+ * The browser extension sends nothing; this lives only in the npm server.
12
+ *
13
+ * On by default with a one-time notice on first run. Off with
14
+ * CHROME_MCP_TELEMETRY=0 (or false/off), DO_NOT_TRACK=1, or --no-telemetry.
15
+ * Every failure is swallowed: telemetry can never break or slow a tool call.
16
+ */
17
+ /** PostHog project key. Public by design: it can only WRITE events, never read them. */
18
+ export declare const POSTHOG_KEY = "phc_CG5QX5JEkRokfUN87rZQnPa3URdLWePCZnfqW6MCyCdU";
19
+ export declare const POSTHOG_HOST = "https://us.i.posthog.com";
20
+ export declare const TELEMETRY_NOTICE: string;
21
+ interface Event {
22
+ event: string;
23
+ distinct_id: string;
24
+ timestamp: string;
25
+ properties: Record<string, unknown>;
26
+ }
27
+ export interface TelemetryOptions {
28
+ dataDir: string;
29
+ version: string;
30
+ /** --no-telemetry */
31
+ disabledByFlag?: boolean;
32
+ env?: NodeJS.ProcessEnv;
33
+ /** Override the project key (tests, forks). Empty = telemetry off. */
34
+ key?: string;
35
+ log?: (message: string) => void;
36
+ /** Test seam: replaces the HTTP POST. */
37
+ send?: (events: Event[]) => Promise<void>;
38
+ /** Live context added to each summary (role, paired browsers). */
39
+ context?: () => Record<string, unknown>;
40
+ }
41
+ /** Whether the user has turned telemetry off, by env or flag. */
42
+ export declare function telemetryDisabled(env: NodeJS.ProcessEnv, disabledByFlag?: boolean): boolean;
43
+ /** Pull the `[CODE]` prefix off a tool error message, if any. */
44
+ export declare function errorCodeOf(message: string | undefined): string;
45
+ /** Start telemetry for this server process, unless turned off or unconfigured. */
46
+ export declare function initTelemetry(opts: TelemetryOptions): boolean;
47
+ /** Count one tool call. A no-op when telemetry is off. */
48
+ export declare function noteToolCall(tool: string, ok: boolean, error?: string): void;
49
+ /** Flush and send session_ended. Safe to call when telemetry is off. */
50
+ export declare function stopTelemetry(): Promise<void>;
51
+ /** Test seam: forget the active instance. */
52
+ export declare function resetTelemetryForTesting(): void;
53
+ export {};
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ /**
3
+ * src/telemetry.ts — anonymous usage statistics from the chrome-mcp SERVER.
4
+ *
5
+ * What it sends, to PostHog: a random per-install id, the chrome-mcp version,
6
+ * OS, CPU architecture and Node major version, whether this session owns the
7
+ * port or shares it, how many browsers are paired, and per-tool call and error
8
+ * COUNTS. Never URLs, domains, tool arguments, page content, profile names,
9
+ * tokens, file paths, or anything typed. Events are marked personless and ask
10
+ * PostHog not to geolocate them.
11
+ *
12
+ * The browser extension sends nothing; this lives only in the npm server.
13
+ *
14
+ * On by default with a one-time notice on first run. Off with
15
+ * CHROME_MCP_TELEMETRY=0 (or false/off), DO_NOT_TRACK=1, or --no-telemetry.
16
+ * Every failure is swallowed: telemetry can never break or slow a tool call.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.TELEMETRY_NOTICE = exports.POSTHOG_HOST = exports.POSTHOG_KEY = void 0;
20
+ exports.telemetryDisabled = telemetryDisabled;
21
+ exports.errorCodeOf = errorCodeOf;
22
+ exports.initTelemetry = initTelemetry;
23
+ exports.noteToolCall = noteToolCall;
24
+ exports.stopTelemetry = stopTelemetry;
25
+ exports.resetTelemetryForTesting = resetTelemetryForTesting;
26
+ const node_crypto_1 = require("node:crypto");
27
+ const node_fs_1 = require("node:fs");
28
+ const node_os_1 = require("node:os");
29
+ const node_path_1 = require("node:path");
30
+ /** PostHog project key. Public by design: it can only WRITE events, never read them. */
31
+ exports.POSTHOG_KEY = 'phc_CG5QX5JEkRokfUN87rZQnPa3URdLWePCZnfqW6MCyCdU';
32
+ exports.POSTHOG_HOST = 'https://us.i.posthog.com';
33
+ /** How often the aggregated counts are sent while a session runs. */
34
+ const FLUSH_INTERVAL_MS = 10 * 60_000;
35
+ /** A send never holds up the process longer than this. */
36
+ const SEND_TIMEOUT_MS = 3_000;
37
+ const STATE_FILE = 'telemetry.json';
38
+ exports.TELEMETRY_NOTICE = 'chrome-mcp collects anonymous usage statistics (version, OS, tool call and error counts; never URLs, ' +
39
+ 'page content or arguments) to see how it is used. Turn it off with CHROME_MCP_TELEMETRY=0 or DO_NOT_TRACK=1. ' +
40
+ 'Details: https://github.com/Mehmoodqureshi/chrome-mcp#telemetry';
41
+ /** Whether the user has turned telemetry off, by env or flag. */
42
+ function telemetryDisabled(env, disabledByFlag = false) {
43
+ if (disabledByFlag)
44
+ return true;
45
+ const own = (env.CHROME_MCP_TELEMETRY ?? '').trim().toLowerCase();
46
+ if (['0', 'false', 'off', 'no'].includes(own))
47
+ return true;
48
+ const dnt = (env.DO_NOT_TRACK ?? '').trim().toLowerCase();
49
+ return dnt !== '' && dnt !== '0' && dnt !== 'false';
50
+ }
51
+ /** Pull the `[CODE]` prefix off a tool error message, if any. */
52
+ function errorCodeOf(message) {
53
+ const m = /^\[([A-Z_]+)\]/.exec(message ?? '');
54
+ return m ? m[1] : 'OTHER';
55
+ }
56
+ class Telemetry {
57
+ opts;
58
+ installId;
59
+ calls = new Map();
60
+ errorCodes = new Map();
61
+ timer = null;
62
+ base;
63
+ /** Sends still on the wire, so a quick exit doesn't cut session_started off. */
64
+ inflight = new Set();
65
+ constructor(opts) {
66
+ this.opts = opts;
67
+ this.installId = this.loadInstallId();
68
+ this.base = {
69
+ version: opts.version,
70
+ os: (0, node_os_1.platform)(),
71
+ arch: (0, node_os_1.arch)(),
72
+ node: process.versions.node.split('.')[0],
73
+ // Anonymous: no person profile, no GeoIP lookup from the request IP.
74
+ $process_person_profile: false,
75
+ $geoip_disable: true,
76
+ };
77
+ }
78
+ start() {
79
+ void this.capture('session_started', this.opts.context?.() ?? {});
80
+ this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
81
+ this.timer.unref();
82
+ }
83
+ noteCall(tool, ok, error) {
84
+ const c = this.calls.get(tool) ?? { calls: 0, errors: 0 };
85
+ c.calls++;
86
+ if (!ok) {
87
+ c.errors++;
88
+ const code = errorCodeOf(error);
89
+ this.errorCodes.set(code, (this.errorCodes.get(code) ?? 0) + 1);
90
+ }
91
+ this.calls.set(tool, c);
92
+ }
93
+ /** Send the counts gathered since the last flush (skipped when idle). */
94
+ async flush() {
95
+ const summary = this.takeSummary();
96
+ if (summary)
97
+ await this.send([summary]);
98
+ }
99
+ /** The counts since the last summary as an event, resetting them; null when idle. */
100
+ takeSummary() {
101
+ if (this.calls.size === 0)
102
+ return null;
103
+ const tools = Object.fromEntries(this.calls);
104
+ const errors = Object.fromEntries(this.errorCodes);
105
+ let total = 0;
106
+ let failed = 0;
107
+ for (const c of this.calls.values()) {
108
+ total += c.calls;
109
+ failed += c.errors;
110
+ }
111
+ this.calls = new Map();
112
+ this.errorCodes = new Map();
113
+ return this.event('usage_summary', { ...(this.opts.context?.() ?? {}), calls: total, errors: failed, tools, error_codes: errors });
114
+ }
115
+ /** Last summary and session_ended in ONE request, so both fit the shutdown deadline. */
116
+ async stop() {
117
+ if (this.timer)
118
+ clearInterval(this.timer);
119
+ this.timer = null;
120
+ const summary = this.takeSummary();
121
+ const final = this.send([...(summary ? [summary] : []), this.event('session_ended', {})]);
122
+ // Also wait for anything already sent (session_started on a short session).
123
+ await Promise.allSettled([...this.inflight, final]);
124
+ }
125
+ capture(event, props) {
126
+ return this.send([this.event(event, props)]);
127
+ }
128
+ event(event, props) {
129
+ return {
130
+ event,
131
+ distinct_id: this.installId,
132
+ timestamp: new Date().toISOString(),
133
+ properties: { ...this.base, ...props },
134
+ };
135
+ }
136
+ send(batch) {
137
+ const p = (async () => {
138
+ try {
139
+ await (this.opts.send ?? ((b) => this.post(b)))(batch);
140
+ }
141
+ catch {
142
+ /* offline, blocked, or PostHog down — never matters to the user */
143
+ }
144
+ })();
145
+ this.inflight.add(p);
146
+ void p.finally(() => this.inflight.delete(p));
147
+ return p;
148
+ }
149
+ async post(batch) {
150
+ await fetch(`${exports.POSTHOG_HOST}/batch/`, {
151
+ method: 'POST',
152
+ headers: { 'content-type': 'application/json' },
153
+ body: JSON.stringify({ api_key: this.opts.key, batch }),
154
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
155
+ });
156
+ }
157
+ /** The random install id, created (with the first-run notice) on first use. */
158
+ loadInstallId() {
159
+ const path = (0, node_path_1.join)(this.opts.dataDir, STATE_FILE);
160
+ try {
161
+ if ((0, node_fs_1.existsSync)(path)) {
162
+ const saved = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
163
+ if (typeof saved.installId === 'string' && saved.installId.length > 0)
164
+ return saved.installId;
165
+ }
166
+ }
167
+ catch {
168
+ /* unreadable: start over with a new id */
169
+ }
170
+ const installId = (0, node_crypto_1.randomUUID)();
171
+ this.opts.log?.(exports.TELEMETRY_NOTICE);
172
+ try {
173
+ const tmp = `${path}.tmp`;
174
+ (0, node_fs_1.writeFileSync)(tmp, JSON.stringify({ installId, noticeShownAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
175
+ (0, node_fs_1.renameSync)(tmp, path);
176
+ }
177
+ catch {
178
+ /* read-only data dir: a new id (and notice) next boot is harmless */
179
+ }
180
+ return installId;
181
+ }
182
+ }
183
+ let active = null;
184
+ /** Start telemetry for this server process, unless turned off or unconfigured. */
185
+ function initTelemetry(opts) {
186
+ const key = opts.key ?? exports.POSTHOG_KEY;
187
+ if (!key || telemetryDisabled(opts.env ?? process.env, opts.disabledByFlag))
188
+ return false;
189
+ active = new Telemetry({ ...opts, key });
190
+ active.start();
191
+ return true;
192
+ }
193
+ /** Count one tool call. A no-op when telemetry is off. */
194
+ function noteToolCall(tool, ok, error) {
195
+ active?.noteCall(tool, ok, error);
196
+ }
197
+ /** Flush and send session_ended. Safe to call when telemetry is off. */
198
+ async function stopTelemetry() {
199
+ const t = active;
200
+ active = null;
201
+ await t?.stop();
202
+ }
203
+ /** Test seam: forget the active instance. */
204
+ function resetTelemetryForTesting() {
205
+ active = null;
206
+ }
207
+ //# sourceMappingURL=telemetry.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "MCP Extension for Chrome",
4
- "version": "0.9.6",
4
+ "version": "0.9.7",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehmoodqureshi/chrome-mcp",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
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",