@glittering0717/ai-browser 1.3.1 → 2.0.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.
@@ -42,37 +42,81 @@ export const MCP_TOOLS = [
42
42
  {
43
43
  name: 'browse_act',
44
44
  title: 'Act on element',
45
- description: 'Act on an element by data-ai-id: click, type (append at caret), setContent (replace all), clear, focus, hover, scroll_to, upload, get_value (read-only), drag, press, wheel, paste.',
45
+ description: 'Act on a data-ai-id element. type appends at caret, setContent replaces all. Verb-specific params live in the group of the same name (click/read/press/wheel/drag).',
46
46
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
47
47
  inputSchema: {
48
48
  type: 'object',
49
49
  properties: {
50
- action: { type: 'string', enum: ['click', 'type', 'clear', 'focus', 'hover', 'scroll_to', 'upload', 'get_value', 'drag', 'press', 'wheel', 'paste'], description: 'Action.' },
51
- html: { type: 'string', description: 'paste: HTML to inject (preferred). Editors convert it through their own paste pipeline (Draft.js/ProseMirror/Quill), so headings/code blocks/bold/lists survive. Plain text loses all formatting.' },
52
- target: { type: 'string', description: 'Element data-ai-id from the semantic tree (e.g. e:button-12, axf-3-1) — NOT the visible label. Omit for press (keys go to the current focus); for drag you may also omit it and pass from_x/from_y to start from a bare point. upload also accepts a CSS selector (descendant selectors with spaces included), which is the only way to reach upload widgets that have no semantic node.' },
53
- text: { type: 'string', description: 'Text to type (type/setContent). For paste: plain-text fallback used when html is absent.' },
54
- value: { type: 'string', description: 'Value for select.' },
55
- offset: { type: 'integer', description: 'get_value: start offset in Unicode code points (default 0).' },
56
- limit: { type: 'integer', description: 'get_value: max code points to return (default 20000, hard cap 1000000).' },
57
- url: { type: 'string', description: 'For click: click the link whose href matches this URL instead of guessing the target label.' },
58
- keep_tab: { type: 'boolean', description: 'For click: keep the current tab active instead of auto-following a newly opened tab.' },
59
- file: { type: 'string', description: 'Absolute path to upload (upload only). target may be the file input itself, the control that opens the picker, or — when the control has no semantic node at all — a CSS selector such as ".upload-button". Works inside cross-process iframes.' },
60
- timeout_ms: { type: 'integer', description: 'Override the action deadline in ms (default 15000; type/setContent/drag/press scale with text length / duration / repeat; 1s floor, 300s cap). A timeout returns success=false, timed_out=true and warns the action may have partially applied.' },
61
- to_target: { type: 'string', description: 'drag: data-ai-id to drop onto (e.g. drag from one node\'s output to another node to connect them).' },
62
- dx: { type: 'number', description: 'drag: horizontal distance to move (use with dy instead of to_target).' },
63
- dy: { type: 'number', description: 'drag: vertical distance to move.' },
64
- to_x: { type: 'number', description: 'drag: absolute viewport x to end at (overrides dx).' },
65
- to_y: { type: 'number', description: 'drag: absolute viewport y to end at.' },
66
- from_x: { type: 'number', description: 'drag: explicit viewport x to start the drag at (no target needed — e.g. panning empty canvas).' },
67
- from_y: { type: 'number', description: 'drag: explicit viewport y to start the drag at.' },
68
- from_anchor: { type: 'string', enum: ['center', 'left', 'right', 'top', 'bottom'], description: 'drag: which point of target to grab; a side prefers that node\'s connection handle (default center).' },
69
- to_anchor: { type: 'string', enum: ['center', 'left', 'right', 'top', 'bottom'], description: 'drag: which point of to_target to drop on.' },
70
- hold: { type: 'array', items: { type: 'string' }, description: 'drag: modifier keys held during the drag (e.g. ["Shift"]).' },
71
- button: { type: 'string', enum: ['left', 'middle', 'right'], description: 'drag: mouse button (default left). Canvas apps often pan on middle/right drag.' },
72
- key: { type: 'string', description: 'press: a key name such as Delete, Escape, ArrowRight, Enter, or a combo like "Meta+a".' },
73
- delta_x: { type: 'number', description: 'wheel: horizontal scroll amount (positive scrolls right).' },
74
- delta_y: { type: 'number', description: 'wheel: vertical scroll amount. Negative scrolls up / zooms out; add hold:["Control"] for canvas zoom.' },
75
- keys: { type: 'array', items: { type: 'string' }, description: 'press: sequence of keys to press in order.' },
50
+ action: { type: 'string', enum: ['click', 'type', 'setContent', 'clear', 'focus', 'hover', 'scroll_to', 'upload', 'get_value', 'drag', 'press', 'wheel', 'paste'], description: 'Verb. Verb-specific params live in the group of the same name.' },
51
+ target: { type: 'string', description: 'data-ai-id from the tree (e.g. e:button-12) — NOT the visible label. Optional for press; drag may use drag.from; upload also takes a CSS selector.' },
52
+ text: { type: 'string', description: 'type: appended at the caret. setContent: replaces all. paste: plain fallback when html is absent.' },
53
+ html: { type: 'string', description: 'paste: HTML, preferred — editors keep headings/lists/bold. Plain text loses formatting.' },
54
+ value: { type: 'string', description: 'Option value for a <select>.' },
55
+ file: { type: 'string', description: 'upload: absolute path. target may be the input, the trigger, or a CSS selector.' },
56
+ timeout_ms: { type: 'integer', description: 'Deadline ms (default 15000, 1s..300s). On timeout: success=false, timed_out=true, may have partially applied.' },
57
+ click: {
58
+ type: 'object',
59
+ description: 'click only.',
60
+ properties: {
61
+ url: { type: 'string', description: 'Hit the link whose href matches this URL (for composite cards with several links).' },
62
+ keep_tab: { type: 'boolean', description: 'Stay on this tab instead of following a newly opened one.' },
63
+ opened_tab_wait_ms: { type: 'integer', description: 'Extra wait ms for a site-opened tab (default 250 on link targets; pass it to force, 0 off).' }
64
+ }
65
+ },
66
+ read: {
67
+ type: 'object',
68
+ description: 'get_value only — read-only: does not focus the element or fire input events.',
69
+ properties: {
70
+ offset: { type: 'integer', description: 'Start offset in Unicode code points (default 0).' },
71
+ limit: { type: 'integer', description: 'Max code points to return (default 20000, hard cap 1000000).' }
72
+ }
73
+ },
74
+ press: {
75
+ type: 'object',
76
+ description: 'press only. Keys go to the current focus, so target is optional.',
77
+ properties: {
78
+ key: { type: 'string', description: 'Delete, Escape, ArrowRight, Enter, or a combo like "Meta+a".' },
79
+ keys: { type: 'array', items: { type: 'string' }, description: 'Sequence to press in order.' }
80
+ }
81
+ },
82
+ wheel: {
83
+ type: 'object',
84
+ description: 'wheel only. Lands on target, or the viewport centre when target is omitted.',
85
+ properties: {
86
+ dx: { type: 'number', description: 'Horizontal delta (positive scrolls right).' },
87
+ dy: { type: 'number', description: 'Vertical delta. Negative scrolls up / zooms out.' },
88
+ hold: { type: 'array', items: { type: 'string' }, description: 'Modifiers held, e.g. ["Control"] to zoom a canvas.' }
89
+ }
90
+ },
91
+ drag: {
92
+ type: 'object',
93
+ description: 'drag only. Give from/to for absolute points, dx/dy for a relative move, or target_to to drop onto another element.',
94
+ properties: {
95
+ from: {
96
+ type: 'object',
97
+ description: 'Start point. Omit to start from target.',
98
+ properties: {
99
+ x: { type: 'number' },
100
+ y: { type: 'number' },
101
+ anchor: { type: 'string', enum: ['center', 'left', 'right', 'top', 'bottom'], description: 'A side prefers that node\'s connection handle.' }
102
+ }
103
+ },
104
+ to: {
105
+ type: 'object',
106
+ description: 'End point in viewport coords.',
107
+ properties: {
108
+ x: { type: 'number' },
109
+ y: { type: 'number' },
110
+ anchor: { type: 'string', enum: ['center', 'left', 'right', 'top', 'bottom'] }
111
+ }
112
+ },
113
+ dx: { type: 'number', description: 'Horizontal distance instead of to.x.' },
114
+ dy: { type: 'number', description: 'Vertical distance instead of to.y.' },
115
+ target_to: { type: 'string', description: 'data-ai-id to drop onto (e.g. connect one node\'s output to another).' },
116
+ hold: { type: 'array', items: { type: 'string' }, description: 'Modifiers held during the drag, e.g. ["Shift"].' },
117
+ button: { type: 'string', enum: ['left', 'middle', 'right'], description: 'Canvas apps often pan on middle/right drag.' }
118
+ }
119
+ },
76
120
  tab: { type: 'integer', description: 'Tab id (default: active tab).' }
77
121
  },
78
122
  required: ['action']
@@ -164,81 +208,78 @@ export const MCP_TOOLS = [
164
208
  }
165
209
  },
166
210
  {
167
- name: 'browse_list_tabs',
168
- title: 'List tabs',
169
- description: 'List open tabs (id, url, title, active).',
170
- annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
171
- inputSchema: { type: 'object', properties: {} }
172
- },
173
- {
174
- name: 'browse_new_tab',
175
- title: 'New tab',
176
- description: 'Open a new tab, optionally at a URL.',
211
+ name: 'browse_tabs',
212
+ title: 'Tabs',
213
+ description: 'Tabs: op=list / new / close / switch. switch makes a tab the default target for later calls, so you rarely pass tab explicitly.',
214
+ // 合并前是 4 个工具(list / new / close / set_active),每个只承载一个动词。
215
+ // 合并后 destructiveHint 只能是 false —— 否则 op=list 也会被标成破坏性操作;
216
+ // close 的破坏性写在 description 里。这是合并的唯一语义代价,接受。
177
217
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
178
218
  inputSchema: {
179
219
  type: 'object',
180
220
  properties: {
181
- url: { type: 'string', description: 'Optional URL.' }
182
- }
183
- }
184
- },
185
- {
186
- name: 'browse_close_tab',
187
- title: 'Close tab',
188
- description: 'Close a tab by id.',
189
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
190
- inputSchema: {
191
- type: 'object',
192
- properties: {
193
- tab: { type: 'integer', description: 'Tab id.' }
194
- },
195
- required: ['tab']
196
- }
197
- },
198
- {
199
- name: 'browse_set_active_tab',
200
- title: 'Switch active tab',
201
- description: 'Switch the active tab.',
202
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
203
- inputSchema: {
204
- type: 'object',
205
- properties: {
206
- tab: { type: 'integer', description: 'Tab id.' }
221
+ op: { type: 'string', enum: ['list', 'new', 'close', 'switch'], description: 'list = id/url/title/active for every tab. switch = make `tab` the default target.' },
222
+ tab: { type: 'integer', description: 'close / switch: the target tab id.' },
223
+ url: { type: 'string', description: 'new: open at this URL (default about:blank).' }
207
224
  },
208
- required: ['tab']
225
+ required: ['op']
209
226
  }
210
227
  },
211
228
  {
212
229
  name: 'browse_network',
213
230
  title: 'Inspect network',
214
- description: 'Query captured HTTP requests (Chrome Network panel): list/get/clear/configure. Captures method, headers and POST bodies; sensitive headers redacted by default.',
231
+ description: 'Query captured HTTP requests: list/get/clear/configure. Captures headers and POST bodies; sensitive headers redacted. Each op reads its own group: filters / include / body_offset / configure.',
215
232
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
216
233
  inputSchema: {
217
234
  type: 'object',
218
235
  properties: {
219
236
  operation: { type: 'string', enum: ['list', 'get', 'clear', 'configure'], description: 'Operation.' },
220
- tab: { type: 'integer', description: 'Tab id (default: active tab).' },
221
237
  network_id: { type: 'string', description: 'get: id from list.' },
222
- method: { type: 'array', items: { type: 'string' }, description: 'list: filter by HTTP method.' },
223
- url_contains: { type: 'string', description: 'list: URL substring filter.' },
224
- status_min: { type: 'integer', description: 'list: minimum status.' },
225
- status_max: { type: 'integer', description: 'list: maximum status.' },
226
- resource_type: { type: 'array', items: { type: 'string' }, description: 'list: XHR/Fetch/Document/...' },
227
- state: { type: 'array', items: { type: 'string' }, description: 'list: pending/finished/failed.' },
228
- started_after: { type: 'string', description: 'list: ISO timestamp.' },
229
- started_before: { type: 'string', description: 'list: ISO timestamp.' },
230
- limit: { type: 'integer', description: 'list: page size (default 50, max 200).' },
231
- before_seq: { type: 'integer', description: 'list: cursor from pagination.next_before_seq.' },
232
- include_request_headers: { type: 'boolean' },
233
- include_request_body: { type: 'boolean' },
234
- include_response_headers: { type: 'boolean' },
235
- include_response_body: { type: 'boolean' },
236
- include_sensitive_headers: { type: 'boolean', description: 'Requires AI_BROWSER_NETWORK_SENSITIVE=1.' },
237
- request_body_offset: { type: 'integer' },
238
- response_body_offset: { type: 'integer' },
239
- body_limit: { type: 'integer', description: 'Body page size (default 65536, max 262144).' },
240
- enabled: { type: 'boolean', description: 'configure: turn capture on/off.' },
241
- capture_bodies: { type: 'string', enum: ['none', 'api', 'all'], description: 'configure.' }
238
+ filters: {
239
+ type: 'object',
240
+ description: 'list only.',
241
+ properties: {
242
+ method: { type: 'array', items: { type: 'string' }, description: 'HTTP methods.' },
243
+ url_contains: { type: 'string', description: 'URL substring.' },
244
+ status_min: { type: 'integer' },
245
+ status_max: { type: 'integer' },
246
+ resource_type: { type: 'array', items: { type: 'string' }, description: 'XHR / Fetch / Document / …' },
247
+ state: { type: 'array', items: { type: 'string' }, description: 'pending / finished / failed.' },
248
+ started_after: { type: 'string', description: 'ISO timestamp.' },
249
+ started_before: { type: 'string', description: 'ISO timestamp.' },
250
+ limit: { type: 'integer', description: 'Page size (default 50, max 200).' },
251
+ before_seq: { type: 'integer', description: 'Cursor from pagination.next_before_seq.' }
252
+ }
253
+ },
254
+ include: {
255
+ type: 'object',
256
+ description: 'get only — what to bring back.',
257
+ properties: {
258
+ request_headers: { type: 'boolean' },
259
+ response_headers: { type: 'boolean' },
260
+ request_body: { type: 'boolean' },
261
+ response_body: { type: 'boolean' },
262
+ sensitive_headers: { type: 'boolean', description: 'Requires AI_BROWSER_NETWORK_SENSITIVE=1.' },
263
+ body_limit: { type: 'integer', description: 'Body page size (default 65536, max 262144).' }
264
+ }
265
+ },
266
+ body_offset: {
267
+ type: 'object',
268
+ description: 'get only — paginate a large body.',
269
+ properties: {
270
+ request: { type: 'integer' },
271
+ response: { type: 'integer' }
272
+ }
273
+ },
274
+ configure: {
275
+ type: 'object',
276
+ description: 'configure only.',
277
+ properties: {
278
+ enabled: { type: 'boolean', description: 'Turn capture on/off.' },
279
+ capture_bodies: { type: 'string', enum: ['none', 'api', 'all'] }
280
+ }
281
+ },
282
+ tab: { type: 'integer', description: 'Tab id (default: active tab).' }
242
283
  },
243
284
  required: ['operation']
244
285
  }
@@ -102,7 +102,13 @@ export function createWsClient({
102
102
 
103
103
  // Attempt failed — release this socket's connect-phase listeners fully so
104
104
  // its later error/close can't touch live state (MW-4).
105
- try { ws.removeAllListeners(); ws.close(); } catch (_e) {}
105
+ // ⚠️ close() 一个还在 CONNECTING 的 socket 会异步再发一次 'error'
106
+ // ("WebSocket was closed before the connection was established")。
107
+ // removeAllListeners 把 connect-phase 的 once('error') 也摘了,这个迟到
108
+ // 的 error 就没有监听器 → EventEmitter 直接 throw → 整个 MCP server 进程
109
+ // 崩掉,lazy launch 重试循环根本走不到(表现为"首次调用必崩")。
110
+ // 挂一个 no-op error 兜底:迟到事件只被吞掉,不影响 MW-4 的状态隔离。
111
+ try { ws.removeAllListeners(); ws.on('error', () => {}); ws.close(); } catch (_e) {}
106
112
  const err = connectionErr || new Error('connection refused');
107
113
  // eslint-disable-next-line no-console
108
114
  if (attempt > 0 && attempt % 4 === 0) console.error('[mcp] ws reconnect attempt', attempt, '-', err.message);