chrome-bridge-mcp 1.11.0 → 1.11.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chrome Bridge for Claude Code",
4
- "version": "1.11.0",
4
+ "version": "1.11.1",
5
5
  "description": "Bridge your browser to Claude Code: 60 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
6
6
  "homepage_url": "https://github.com/frsorrentino/chrome-bridge",
7
7
  "minimum_chrome_version": "135",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chrome-bridge-mcp",
3
3
  "mcpName": "io.github.frsorrentino/chrome-bridge",
4
- "version": "1.11.0",
4
+ "version": "1.11.1",
5
5
  "type": "module",
6
6
  "description": "MCP server with 60 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
7
7
  "main": "server/index.js",
@@ -82,7 +82,7 @@ export const MessageType = Object.freeze({
82
82
  });
83
83
 
84
84
  // Versione
85
- export const VERSION = '1.11.0';
85
+ export const VERSION = '1.11.1';
86
86
 
87
87
  // Configurazione
88
88
  export const DEFAULT_PORT = 8765;
package/server/tools.js CHANGED
@@ -188,8 +188,9 @@ export const TOOL_CAPS = {
188
188
  // Parametri ubiqui: un solo testo, così `tab_id` non significa una cosa in un
189
189
  // tool e un'altra nel gemello. test/unit/tool-parameters.test.js lo verifica.
190
190
  // save_to: il payload va su disco e nel contesto resta solo il percorso più un
191
- // sommario. L'idea è di superpowers-chrome, che scrive automaticamente a ogni
192
- // azione; qui è opt-in, perché scrivere file per chi non li leggerà è una tassa.
191
+ // sommario. Deliberatamente opt-in per chiamata e non automatico dopo ogni
192
+ // azione: scrivere file per chi non li leggerà è una tassa, e il server non
193
+ // controlla la directory di lavoro del client.
193
194
  const saveToField = (what) => z.string().optional()
194
195
  .describe(`Absolute path: write ${what} there and return the path instead of the content`);
195
196
 
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Carica e pubblica lo zip dell'estensione sul Chrome Web Store via API.
4
+ *
5
+ * Esiste per togliere di mezzo il TOTP: la dev console non è automatizzabile
6
+ * (la gallery non è scriptabile da un'estensione, e via CDP serve comunque il
7
+ * login Google completo), mentre l'API vuole solo un refresh token revocabile.
8
+ *
9
+ * Credenziali in ~/.config/chrome-bridge/cws.json (chmod 600), MAI nel repo:
10
+ * { "client_id": "...", "client_secret": "...", "refresh_token": "...", "item_id": "..." }
11
+ * Sovrascrivibili da CWS_CLIENT_ID / CWS_CLIENT_SECRET / CWS_REFRESH_TOKEN / CWS_ITEM_ID.
12
+ *
13
+ * Uso:
14
+ * node tools/cws-upload.mjs dist/chrome-bridge-extension-1.11.0.zip # carica come bozza
15
+ * node tools/cws-upload.mjs dist/....zip --publish # carica e pubblica
16
+ * node tools/cws-upload.mjs --auth [porta] # URL di consenso + cattura del codice sul loopback
17
+ * node tools/cws-upload.mjs --exchange <code> [--redirect <uri>] # code → refresh token
18
+ */
19
+ import { createServer } from 'node:http';
20
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
21
+ import { homedir } from 'node:os';
22
+ import { join, dirname } from 'node:path';
23
+
24
+ const CONFIG = join(homedir(), '.config', 'chrome-bridge', 'cws.json');
25
+ const SCOPE = 'https://www.googleapis.com/auth/chromewebstore';
26
+ // Google ha dismesso il redirect fuori banda (urn:ietf:wg:oauth:2.0:oob): un
27
+ // client nuovo che lo usa riceve "Errore 400: invalid_request". Per i client
28
+ // desktop la strada supportata è il loopback su una porta qualsiasi, che qui
29
+ // viene aperta al volo e chiusa appena arriva il codice.
30
+ function buildAuthUrl(clientId, redirectUri) {
31
+ return 'https://accounts.google.com/o/oauth2/auth?' + new URLSearchParams({
32
+ client_id: clientId,
33
+ redirect_uri: redirectUri,
34
+ response_type: 'code',
35
+ scope: SCOPE,
36
+ access_type: 'offline',
37
+ prompt: 'consent',
38
+ });
39
+ }
40
+
41
+ // Attende un solo codice sul loopback e poi chiude: nessun server che resta su.
42
+ function waitForCode(port) {
43
+ return new Promise((resolve, reject) => {
44
+ const server = createServer((req, res) => {
45
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
46
+ const code = url.searchParams.get('code');
47
+ const err = url.searchParams.get('error');
48
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
49
+ res.end(`<h2>${code ? 'Autorizzato. Puoi chiudere questa scheda.' : 'Errore: ' + (err || 'nessun codice')}</h2>`);
50
+ server.close();
51
+ if (code) resolve(code); else reject(new Error(err || 'nessun codice nella risposta'));
52
+ });
53
+ server.on('error', reject);
54
+ server.listen(port, '127.0.0.1');
55
+ setTimeout(() => { server.close(); reject(new Error('nessuna autorizzazione entro 5 minuti')); }, 300000);
56
+ });
57
+ }
58
+
59
+ export async function loadConfig(env = process.env) {
60
+ let file = {};
61
+ try { file = JSON.parse(await readFile(CONFIG, 'utf8')); } catch { /* prima esecuzione */ }
62
+ return {
63
+ client_id: env.CWS_CLIENT_ID || file.client_id,
64
+ client_secret: env.CWS_CLIENT_SECRET || file.client_secret,
65
+ refresh_token: env.CWS_REFRESH_TOKEN || file.refresh_token,
66
+ item_id: env.CWS_ITEM_ID || file.item_id,
67
+ };
68
+ }
69
+
70
+ export function requireFields(cfg, fields) {
71
+ const missing = fields.filter((f) => !cfg[f]);
72
+ if (missing.length) {
73
+ throw new Error(`Manca ${missing.join(', ')} — mettili in ${CONFIG} o nelle variabili CWS_*`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Il Web Store risponde 200 anche quando il caricamento è fallito: l'esito vero
79
+ * è in uploadState, e itemError porta il motivo. Trattare 200 come successo è
80
+ * il modo di credere di aver pubblicato senza averlo fatto.
81
+ */
82
+ export function interpretUpload(body) {
83
+ const state = body?.uploadState;
84
+ if (state === 'SUCCESS') return { ok: true, state };
85
+ const reasons = (body?.itemError || []).map((e) => e.error_detail || e.errorDetail || JSON.stringify(e));
86
+ return { ok: false, state: state ?? 'UNKNOWN', reasons };
87
+ }
88
+
89
+ export function interpretPublish(body) {
90
+ const status = body?.status || [];
91
+ const ok = status.includes('OK') || status.includes('PUBLISHED_WITH_FRICTION_WARNING');
92
+ return { ok, status, detail: body?.statusDetail || [] };
93
+ }
94
+
95
+ async function exchange(cfg, code, redirectUri) {
96
+ const res = await fetch('https://oauth2.googleapis.com/token', {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
99
+ body: new URLSearchParams({
100
+ client_id: cfg.client_id, client_secret: cfg.client_secret,
101
+ code, grant_type: 'authorization_code', redirect_uri: redirectUri,
102
+ }),
103
+ });
104
+ const body = await res.json();
105
+ if (!body.refresh_token) throw new Error(`Nessun refresh_token: ${JSON.stringify(body)}`);
106
+ return body.refresh_token;
107
+ }
108
+
109
+ async function saveRefreshToken(cfg, refresh_token) {
110
+ await mkdir(dirname(CONFIG), { recursive: true });
111
+ await writeFile(CONFIG, JSON.stringify({ ...cfg, refresh_token }, null, 2), { mode: 0o600 });
112
+ console.log(`refresh_token salvato in ${CONFIG} (chmod 600)`);
113
+ }
114
+
115
+ async function accessToken(cfg) {
116
+ const res = await fetch('https://oauth2.googleapis.com/token', {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
119
+ body: new URLSearchParams({
120
+ client_id: cfg.client_id,
121
+ client_secret: cfg.client_secret,
122
+ refresh_token: cfg.refresh_token,
123
+ grant_type: 'refresh_token',
124
+ }),
125
+ });
126
+ const body = await res.json();
127
+ if (!res.ok || !body.access_token) {
128
+ // invalid_grant qui significa quasi sempre consent screen in "Testing":
129
+ // in quello stato Google scade i refresh token dopo 7 giorni.
130
+ throw new Error(`Token refresh fallito (${res.status}): ${JSON.stringify(body)}`);
131
+ }
132
+ return body.access_token;
133
+ }
134
+
135
+ async function main() {
136
+ const argv = process.argv.slice(2);
137
+ const cfg = await loadConfig();
138
+
139
+ if (argv.includes('--auth')) {
140
+ requireFields(cfg, ['client_id', 'client_secret']);
141
+ const portArg = argv[argv.indexOf('--auth') + 1];
142
+ const port = Number(portArg) > 0 ? Number(portArg) : 8899;
143
+ const redirect = `http://localhost:${port}`;
144
+ console.log(buildAuthUrl(cfg.client_id, redirect));
145
+ console.error(`in attesa del codice su ${redirect} …`);
146
+
147
+ const code = await waitForCode(port);
148
+ const token = await exchange(cfg, code, redirect);
149
+ await saveRefreshToken(cfg, token);
150
+ return;
151
+ }
152
+
153
+ const exIdx = argv.indexOf('--exchange');
154
+ if (exIdx !== -1) {
155
+ requireFields(cfg, ['client_id', 'client_secret']);
156
+ const code = argv[exIdx + 1];
157
+ if (!code) throw new Error('Uso: --exchange <codice> [--redirect <uri>]');
158
+ const rIdx = argv.indexOf('--redirect');
159
+ const redirect = rIdx !== -1 ? argv[rIdx + 1] : 'http://localhost:8899';
160
+ await saveRefreshToken(cfg, await exchange(cfg, code, redirect));
161
+ return;
162
+ }
163
+
164
+ const zip = argv.find((a) => a.endsWith('.zip'));
165
+ if (!zip) throw new Error('Indica lo zip da caricare, o usa --auth / --exchange');
166
+ requireFields(cfg, ['client_id', 'client_secret', 'refresh_token', 'item_id']);
167
+
168
+ const size = (await stat(zip)).size;
169
+ const token = await accessToken(cfg);
170
+
171
+ const up = await fetch(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${cfg.item_id}`, {
172
+ method: 'PUT',
173
+ headers: { Authorization: `Bearer ${token}`, 'x-goog-api-version': '2' },
174
+ body: await readFile(zip),
175
+ });
176
+ const upBody = await up.json();
177
+ const upRes = interpretUpload(upBody);
178
+ console.log(`upload ${zip} (${size} B): ${upRes.ok ? 'SUCCESS' : `FALLITO [${upRes.state}] ${upRes.reasons.join(' | ')}`}`);
179
+ if (!upRes.ok) process.exit(1);
180
+
181
+ if (!argv.includes('--publish')) {
182
+ console.log('bozza caricata; ripassa con --publish per mandarla in review');
183
+ return;
184
+ }
185
+
186
+ const pub = await fetch(`https://www.googleapis.com/chromewebstore/v1.1/items/${cfg.item_id}/publish`, {
187
+ method: 'POST',
188
+ headers: { Authorization: `Bearer ${token}`, 'x-goog-api-version': '2', 'Content-Length': '0' },
189
+ });
190
+ const pubRes = interpretPublish(await pub.json());
191
+ console.log(`publish: ${pubRes.ok ? 'OK' : 'FALLITO'} ${pubRes.status.join(',')} ${pubRes.detail.join(' | ')}`);
192
+ if (!pubRes.ok) process.exit(1);
193
+ }
194
+
195
+ if (import.meta.url === `file://${process.argv[1]}`) {
196
+ main().catch((e) => { console.error(String(e.message || e)); process.exit(1); });
197
+ }