@attson/atwebpilot-mcp 0.0.19

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.
Files changed (4) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -0
  3. package/dist/index.js +1467 -0
  4. package/package.json +34 -0
package/dist/index.js ADDED
@@ -0,0 +1,1467 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // ../coordinator/src/types.ts
7
+ var QUOTA_DEFAULTS = {
8
+ max_steps_per_session: 200,
9
+ max_dangerous_per_session: 50
10
+ };
11
+
12
+ // ../coordinator/src/clock.ts
13
+ var DefaultClock = class {
14
+ now() {
15
+ return Date.now();
16
+ }
17
+ };
18
+ var DefaultIdGen = class {
19
+ counter = 0;
20
+ next(prefix = "") {
21
+ this.counter += 1;
22
+ return `${prefix}${prefix ? "_" : ""}${Date.now().toString(36)}_${this.counter}`;
23
+ }
24
+ };
25
+
26
+ // ../shared/src/url-pattern.ts
27
+ var SPECIAL = /[.+?^${}()|[\]\\]/g;
28
+ var PLACEHOLDER_DOUBLE = "";
29
+ var PLACEHOLDER_SINGLE = "";
30
+ function compilePattern(pattern) {
31
+ const replaced = pattern.replace(/\*\*/g, PLACEHOLDER_DOUBLE).replace(/\*/g, PLACEHOLDER_SINGLE);
32
+ const escaped = replaced.replace(SPECIAL, "\\$&");
33
+ const expanded = escaped.replace(new RegExp(PLACEHOLDER_DOUBLE, "g"), ".*").replace(new RegExp(PLACEHOLDER_SINGLE, "g"), "[^/]*");
34
+ return new RegExp(`^${expanded}$`);
35
+ }
36
+ function matchesAny(url, patterns) {
37
+ return patterns.some((p) => compilePattern(p).test(url));
38
+ }
39
+
40
+ // ../coordinator/src/worker-registry.ts
41
+ var WorkerRegistry = class {
42
+ constructor(clock) {
43
+ this.clock = clock;
44
+ }
45
+ clock;
46
+ workers = /* @__PURE__ */ new Map();
47
+ register(w) {
48
+ if (this.workers.has(w.id)) {
49
+ throw new Error(`Worker ${w.id} already registered`);
50
+ }
51
+ this.workers.set(w.id, { ...w, connected_at: this.clock.now() });
52
+ }
53
+ unregister(id) {
54
+ this.workers.delete(id);
55
+ }
56
+ get(id) {
57
+ return this.workers.get(id);
58
+ }
59
+ list() {
60
+ return [...this.workers.values()];
61
+ }
62
+ heartbeat(id) {
63
+ const w = this.workers.get(id);
64
+ if (!w) return;
65
+ this.workers.set(id, { ...w, last_heartbeat_at: this.clock.now() });
66
+ }
67
+ /**
68
+ * Pick workers whose saved_tools have any url_pattern matching the given URL.
69
+ * When labels are provided, workers carrying any matching label sort first.
70
+ */
71
+ pickForUrl(url, preferLabels = []) {
72
+ const all = this.list();
73
+ const matching = all.filter(
74
+ (w) => w.saved_tools.some((t) => matchesAny(url, t.url_pattern))
75
+ );
76
+ if (preferLabels.length === 0) return matching;
77
+ return matching.sort((a, b) => labelScore(b, preferLabels) - labelScore(a, preferLabels));
78
+ }
79
+ };
80
+ function labelScore(w, prefer) {
81
+ let score = 0;
82
+ for (const l of prefer) if (w.labels.has(l)) score += 1;
83
+ return score;
84
+ }
85
+
86
+ // ../shared/src/protocol/version.ts
87
+ var PROTOCOL_VERSION = 1;
88
+ var SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
89
+ var ORPHAN_RECOVERY_MS = 5 * 60 * 1e3;
90
+ var NONCE_REPLAY_WINDOW_MS = 5 * 60 * 1e3;
91
+
92
+ // ../shared/src/protocol/envelope.ts
93
+ import { z } from "zod";
94
+ var EnvelopeFields = {
95
+ nonce: z.string().min(1),
96
+ ts: z.number().int().nonnegative(),
97
+ protocol_version: z.number().int().positive()
98
+ };
99
+ var EnvelopeSchema = z.object(EnvelopeFields);
100
+
101
+ // ../shared/src/protocol/errors.ts
102
+ import { z as z2 } from "zod";
103
+ var ErrorCodes = [
104
+ // ProtocolError (request itself is broken; not retryable)
105
+ "SessionNotFound",
106
+ "SessionExpired",
107
+ "InvalidArgs",
108
+ "PermissionDenied",
109
+ "ToolHashMismatch",
110
+ "ProtocolVersionMismatch",
111
+ "ReplayDetected",
112
+ // WorkerError (browser-side failure; often retryable)
113
+ "WorkerDisconnected",
114
+ "TabClosed",
115
+ "NavigationLost",
116
+ "PageScriptError",
117
+ // CoordinatorError (internal)
118
+ "WorkerBusy",
119
+ "QueueFull",
120
+ "InternalError",
121
+ // Quota
122
+ "SessionExhausted",
123
+ "DangerousQuotaExceeded"
124
+ ];
125
+ var ErrorBodySchema = z2.object({
126
+ code: z2.enum(ErrorCodes),
127
+ message: z2.string(),
128
+ retryable: z2.boolean(),
129
+ retry_after_ms: z2.number().int().nonnegative().optional(),
130
+ audit_id: z2.string().optional(),
131
+ /** machine-readable extra context, e.g. {denied_capability: "submit:form"} */
132
+ hints: z2.record(z2.unknown()).optional()
133
+ });
134
+
135
+ // ../shared/src/protocol/messages.ts
136
+ import { z as z4 } from "zod";
137
+
138
+ // ../shared/src/protocol/chat-event.ts
139
+ import { z as z3 } from "zod";
140
+ var ChatSessionStatusSchema = z3.enum([
141
+ "idle",
142
+ "streaming",
143
+ "awaiting",
144
+ "running",
145
+ "done",
146
+ "error",
147
+ "aborted"
148
+ ]);
149
+ var JsonValue = z3.lazy(
150
+ () => z3.union([
151
+ z3.string(),
152
+ z3.number(),
153
+ z3.boolean(),
154
+ z3.null(),
155
+ z3.array(JsonValue),
156
+ z3.record(JsonValue)
157
+ ])
158
+ );
159
+ var ToolUsePartSchema = z3.object({
160
+ type: z3.literal("tool_use"),
161
+ id: z3.string(),
162
+ name: z3.string(),
163
+ input: JsonValue
164
+ });
165
+ var SessionEndStatus = z3.enum(["done", "aborted", "max_rounds", "error"]);
166
+ var ChatSessionEventSchema = z3.discriminatedUnion("type", [
167
+ z3.object({ type: z3.literal("round_start"), round: z3.number().int() }),
168
+ z3.object({ type: z3.literal("text_delta"), text: z3.string() }),
169
+ z3.object({ type: z3.literal("tool_use_start"), id: z3.string(), name: z3.string() }),
170
+ z3.object({ type: z3.literal("tool_use_input_delta"), id: z3.string(), partial_json: z3.string() }),
171
+ z3.object({ type: z3.literal("tool_use_end"), id: z3.string(), input: JsonValue }),
172
+ z3.object({ type: z3.literal("assistant_turn_end"), toolUses: z3.array(ToolUsePartSchema) }),
173
+ z3.object({ type: z3.literal("tool_running"), id: z3.string() }),
174
+ z3.object({ type: z3.literal("tool_done"), id: z3.string(), output: JsonValue, ms: z3.number() }),
175
+ z3.object({ type: z3.literal("tool_error"), id: z3.string(), error: z3.string(), ms: z3.number() }),
176
+ z3.object({ type: z3.literal("tool_skipped"), id: z3.string() }),
177
+ z3.object({ type: z3.literal("usage"), input_tokens: z3.number().int(), output_tokens: z3.number().int() }),
178
+ z3.object({ type: z3.literal("continuation_nudge"), round: z3.number().int(), attempt: z3.number().int() }),
179
+ z3.object({ type: z3.literal("stream_error"), error: z3.string() }),
180
+ z3.object({ type: z3.literal("exception"), error: z3.string() }),
181
+ z3.object({
182
+ type: z3.literal("session_end"),
183
+ status: SessionEndStatus,
184
+ lastOutput: JsonValue.nullable(),
185
+ reason: z3.string().optional()
186
+ })
187
+ ]);
188
+
189
+ // ../shared/src/protocol/messages.ts
190
+ var StepSchema = z4.object({
191
+ tool: z4.string(),
192
+ args: z4.unknown()
193
+ });
194
+ var HelloSchema = z4.object({
195
+ ...EnvelopeFields,
196
+ type: z4.literal("HELLO"),
197
+ worker_id: z4.string().min(1),
198
+ fingerprint: z4.object({
199
+ ext_hash: z4.string(),
200
+ os: z4.string(),
201
+ chrome: z4.string()
202
+ }),
203
+ capabilities: z4.array(z4.string()),
204
+ attended: z4.boolean(),
205
+ available_tabs: z4.array(
206
+ z4.object({
207
+ tab_id: z4.string(),
208
+ url: z4.string(),
209
+ title: z4.string().optional()
210
+ })
211
+ ),
212
+ saved_tools: z4.array(
213
+ z4.object({
214
+ id: z4.string(),
215
+ version: z4.number().int().nonnegative(),
216
+ hash: z4.string(),
217
+ url_pattern: z4.array(z4.string()),
218
+ description: z4.string().optional()
219
+ })
220
+ ),
221
+ labels: z4.array(z4.string())
222
+ });
223
+ var PingSchema = z4.object({
224
+ ...EnvelopeFields,
225
+ type: z4.literal("PING")
226
+ });
227
+ var TabReadySchema = z4.object({
228
+ ...EnvelopeFields,
229
+ type: z4.literal("TAB_READY"),
230
+ session_id: z4.string(),
231
+ tab_id: z4.string(),
232
+ current_url: z4.string()
233
+ });
234
+ var ProgressSchema = z4.object({
235
+ ...EnvelopeFields,
236
+ type: z4.literal("PROGRESS"),
237
+ req_id: z4.string(),
238
+ partial: z4.unknown()
239
+ });
240
+ var ResultSchema = z4.object({
241
+ ...EnvelopeFields,
242
+ type: z4.literal("RESULT"),
243
+ req_id: z4.string(),
244
+ ok: z4.boolean(),
245
+ return: z4.unknown().optional(),
246
+ error: ErrorBodySchema.optional()
247
+ });
248
+ var SessionEventSchema = z4.object({
249
+ ...EnvelopeFields,
250
+ type: z4.literal("SESSION_EVENT"),
251
+ session_id: z4.string(),
252
+ kind: z4.enum(["navigated", "tab_closed", "audit"]),
253
+ payload: z4.unknown()
254
+ });
255
+ var StateSnapshotSchema = z4.object({
256
+ ...EnvelopeFields,
257
+ type: z4.literal("STATE_SNAPSHOT"),
258
+ last_session_states: z4.array(
259
+ z4.object({
260
+ session_id: z4.string(),
261
+ tab_id: z4.string(),
262
+ state: z4.string()
263
+ })
264
+ )
265
+ });
266
+ var WelcomeSchema = z4.object({
267
+ ...EnvelopeFields,
268
+ type: z4.literal("WELCOME"),
269
+ server_time: z4.number(),
270
+ heartbeat_interval_ms: z4.number().int().positive(),
271
+ server_pubkey_pin: z4.string().optional()
272
+ });
273
+ var PongSchema = z4.object({
274
+ ...EnvelopeFields,
275
+ type: z4.literal("PONG"),
276
+ echo_nonce: z4.string()
277
+ });
278
+ var OpenTabSchema = z4.object({
279
+ ...EnvelopeFields,
280
+ type: z4.literal("OPEN_TAB"),
281
+ session_id: z4.string(),
282
+ url: z4.string(),
283
+ reuse_if_match: z4.array(z4.string()).optional()
284
+ });
285
+ var ExecSchema = z4.object({
286
+ ...EnvelopeFields,
287
+ type: z4.literal("EXEC"),
288
+ req_id: z4.string(),
289
+ session_id: z4.string(),
290
+ tab_id: z4.string(),
291
+ step: StepSchema
292
+ });
293
+ var CloseSessionSchema = z4.object({
294
+ ...EnvelopeFields,
295
+ type: z4.literal("CLOSE_SESSION"),
296
+ session_id: z4.string()
297
+ });
298
+ var LlmStreamEventSchema = z4.discriminatedUnion("type", [
299
+ z4.object({ type: z4.literal("text_delta"), text: z4.string() }),
300
+ z4.object({ type: z4.literal("tool_use_start"), id: z4.string(), name: z4.string() }),
301
+ z4.object({ type: z4.literal("tool_use_input_delta"), id: z4.string(), partial_json: z4.string() }),
302
+ z4.object({ type: z4.literal("tool_use_end"), id: z4.string(), input: z4.unknown() }),
303
+ z4.object({
304
+ type: z4.literal("message_end"),
305
+ usage: z4.object({
306
+ input_tokens: z4.number().int(),
307
+ output_tokens: z4.number().int()
308
+ }).optional(),
309
+ stop_reason: z4.string().optional()
310
+ }),
311
+ z4.object({ type: z4.literal("error"), error: z4.string() })
312
+ ]);
313
+ var StartChatSessionSchema = z4.object({
314
+ ...EnvelopeFields,
315
+ type: z4.literal("START_CHAT_SESSION"),
316
+ session_id: z4.string().min(1),
317
+ user_prompt: z4.string(),
318
+ tab_id: z4.string().optional(),
319
+ mock_llm: z4.object({
320
+ rounds: z4.array(z4.array(LlmStreamEventSchema))
321
+ }).optional(),
322
+ settings_override: z4.object({
323
+ maxRounds: z4.number().int().positive().optional(),
324
+ maxContinuationNudges: z4.number().int().nonnegative().optional()
325
+ }).optional()
326
+ });
327
+ var AbortSessionSchema = z4.object({
328
+ ...EnvelopeFields,
329
+ type: z4.literal("ABORT_SESSION"),
330
+ session_id: z4.string().min(1)
331
+ });
332
+ var ReadSidepanelStateSchema = z4.object({
333
+ ...EnvelopeFields,
334
+ type: z4.literal("READ_SIDEPANEL_STATE"),
335
+ req_id: z4.string().min(1),
336
+ tab_id: z4.string().min(1)
337
+ });
338
+ var ChatEventSchema = z4.object({
339
+ ...EnvelopeFields,
340
+ type: z4.literal("CHAT_EVENT"),
341
+ session_id: z4.string().min(1),
342
+ event: ChatSessionEventSchema
343
+ });
344
+ var SidepanelStateReplySchema = z4.object({
345
+ ...EnvelopeFields,
346
+ type: z4.literal("SIDEPANEL_STATE_REPLY"),
347
+ req_id: z4.string().min(1),
348
+ found: z4.boolean(),
349
+ snapshot: z4.object({
350
+ status: ChatSessionStatusSchema,
351
+ messagesCount: z4.number().int().nonnegative(),
352
+ attachedTabs: z4.array(z4.object({
353
+ tabId: z4.number().int(),
354
+ source: z4.string(),
355
+ lastSeenUrl: z4.string()
356
+ })),
357
+ lastSystemNote: z4.string().optional()
358
+ }).optional()
359
+ });
360
+ var ClientToServerSchema = z4.discriminatedUnion("type", [
361
+ HelloSchema,
362
+ PingSchema,
363
+ TabReadySchema,
364
+ ProgressSchema,
365
+ ResultSchema,
366
+ SessionEventSchema,
367
+ StateSnapshotSchema,
368
+ ChatEventSchema,
369
+ SidepanelStateReplySchema
370
+ ]);
371
+ var ServerToClientSchema = z4.discriminatedUnion("type", [
372
+ WelcomeSchema,
373
+ PongSchema,
374
+ OpenTabSchema,
375
+ ExecSchema,
376
+ CloseSessionSchema,
377
+ StartChatSessionSchema,
378
+ AbortSessionSchema,
379
+ ReadSidepanelStateSchema
380
+ ]);
381
+ var ProtocolMessageSchema = z4.union([ClientToServerSchema, ServerToClientSchema]);
382
+
383
+ // ../coordinator/src/session-manager.ts
384
+ var SessionManager = class {
385
+ constructor(clock, idGen) {
386
+ this.clock = clock;
387
+ this.idGen = idGen;
388
+ }
389
+ clock;
390
+ idGen;
391
+ sessions = /* @__PURE__ */ new Map();
392
+ open(input) {
393
+ const id = this.idGen.next("session");
394
+ const now = this.clock.now();
395
+ const s = {
396
+ id,
397
+ ai_client_fingerprint: input.ai_client_fingerprint,
398
+ worker_id: input.worker_id,
399
+ tab_id: input.tab_id,
400
+ scope: input.scope,
401
+ state: "active",
402
+ created_at: now,
403
+ last_activity_at: now,
404
+ idle_timeout_ms: input.idle_timeout_ms ?? SESSION_IDLE_TIMEOUT_MS,
405
+ step_count: 0,
406
+ dangerous_count: 0
407
+ };
408
+ this.sessions.set(id, s);
409
+ return s;
410
+ }
411
+ get(id) {
412
+ return this.sessions.get(id);
413
+ }
414
+ list() {
415
+ return [...this.sessions.values()];
416
+ }
417
+ /** Record activity on a session, increment counters. Throws if not active. */
418
+ touch(id, opts) {
419
+ const s = this.sessions.get(id);
420
+ if (!s) throw new Error(`Session ${id} not found`);
421
+ if (s.state !== "active") throw new Error(`Session ${id} not active (state=${s.state})`);
422
+ const next = {
423
+ ...s,
424
+ last_activity_at: this.clock.now(),
425
+ step_count: s.step_count + 1,
426
+ dangerous_count: s.dangerous_count + (opts.dangerous ? 1 : 0)
427
+ };
428
+ this.sessions.set(id, next);
429
+ }
430
+ close(id) {
431
+ this.transition(id, "closed");
432
+ }
433
+ fail(id, code, message) {
434
+ const s = this.sessions.get(id);
435
+ if (!s) return;
436
+ this.sessions.set(id, { ...s, state: "error", error: { code, message } });
437
+ }
438
+ /** Mark all sessions belonging to a worker as paused (worker dropped). */
439
+ pauseByWorker(worker_id) {
440
+ const ids = [];
441
+ for (const s of this.sessions.values()) {
442
+ if (s.worker_id === worker_id && s.state === "active") {
443
+ this.sessions.set(s.id, { ...s, state: "paused" });
444
+ ids.push(s.id);
445
+ }
446
+ }
447
+ return ids;
448
+ }
449
+ /**
450
+ * When a worker reconnects, resume sessions present in last_session_states;
451
+ * any paused sessions NOT in the snapshot become error (worker lost them).
452
+ */
453
+ resumeByWorker(worker_id, restoredIds) {
454
+ for (const s of this.sessions.values()) {
455
+ if (s.worker_id !== worker_id || s.state !== "paused") continue;
456
+ if (restoredIds.has(s.id)) {
457
+ this.sessions.set(s.id, { ...s, state: "active", last_activity_at: this.clock.now() });
458
+ } else {
459
+ this.sessions.set(s.id, {
460
+ ...s,
461
+ state: "error",
462
+ error: { code: "WorkerDisconnected", message: "Lost during worker disconnect" }
463
+ });
464
+ }
465
+ }
466
+ }
467
+ /** Orphan all sessions whose AI client just disconnected. */
468
+ orphan(ai_client_fingerprint) {
469
+ const ids = [];
470
+ const now = this.clock.now();
471
+ for (const s of this.sessions.values()) {
472
+ if (s.ai_client_fingerprint === ai_client_fingerprint && s.state === "active") {
473
+ this.sessions.set(s.id, { ...s, state: "orphan", orphaned_at: now });
474
+ ids.push(s.id);
475
+ }
476
+ }
477
+ return ids;
478
+ }
479
+ /** Re-claim orphaned sessions when same AI client reconnects within window. */
480
+ recover(ai_client_fingerprint) {
481
+ const ids = [];
482
+ const now = this.clock.now();
483
+ for (const s of this.sessions.values()) {
484
+ if (s.state !== "orphan" || s.ai_client_fingerprint !== ai_client_fingerprint) continue;
485
+ if (s.orphaned_at !== void 0 && now - s.orphaned_at <= ORPHAN_RECOVERY_MS) {
486
+ this.sessions.set(s.id, {
487
+ ...s,
488
+ state: "active",
489
+ last_activity_at: now,
490
+ orphaned_at: void 0
491
+ });
492
+ ids.push(s.id);
493
+ }
494
+ }
495
+ return ids;
496
+ }
497
+ /**
498
+ * Periodic housekeeping. Returns ids whose state changed.
499
+ * - active too long idle → expired
500
+ * - orphan past ORPHAN_RECOVERY_MS → closed
501
+ */
502
+ tick() {
503
+ const now = this.clock.now();
504
+ const changed = [];
505
+ for (const s of this.sessions.values()) {
506
+ if (s.state === "active" && now - s.last_activity_at >= s.idle_timeout_ms) {
507
+ this.sessions.set(s.id, { ...s, state: "expired" });
508
+ changed.push(s.id);
509
+ } else if (s.state === "orphan" && s.orphaned_at !== void 0 && now - s.orphaned_at > ORPHAN_RECOVERY_MS) {
510
+ this.sessions.set(s.id, { ...s, state: "closed" });
511
+ changed.push(s.id);
512
+ }
513
+ }
514
+ return changed;
515
+ }
516
+ /** Quota snapshot for get_quota MCP tool. */
517
+ quota(id) {
518
+ const s = this.sessions.get(id);
519
+ if (!s) return void 0;
520
+ const now = this.clock.now();
521
+ const ms_until_expiry = Math.max(0, s.idle_timeout_ms - (now - s.last_activity_at));
522
+ return {
523
+ max_steps: QUOTA_DEFAULTS.max_steps_per_session,
524
+ steps_used: s.step_count,
525
+ max_dangerous: QUOTA_DEFAULTS.max_dangerous_per_session,
526
+ dangerous_used: s.dangerous_count,
527
+ ms_until_expiry
528
+ };
529
+ }
530
+ transition(id, target) {
531
+ const s = this.sessions.get(id);
532
+ if (!s) return;
533
+ this.sessions.set(id, { ...s, state: target });
534
+ }
535
+ };
536
+
537
+ // ../coordinator/src/catalog.ts
538
+ var Catalog = class {
539
+ constructor(registry) {
540
+ this.registry = registry;
541
+ }
542
+ registry;
543
+ /**
544
+ * Aggregate saved_tools across all workers and return those whose url_pattern
545
+ * matches the given URL. Entries with conflicting hashes are flagged so the
546
+ * UI / AI client can warn before invocation.
547
+ */
548
+ listFor(url) {
549
+ const byId = /* @__PURE__ */ new Map();
550
+ for (const w of this.registry.list()) {
551
+ for (const t of w.saved_tools) {
552
+ if (!matchesAny(url, t.url_pattern)) continue;
553
+ const existing = byId.get(t.id);
554
+ if (!existing) {
555
+ byId.set(t.id, {
556
+ id: t.id,
557
+ version: t.version,
558
+ hash: t.hash,
559
+ url_pattern: t.url_pattern,
560
+ description: t.description,
561
+ provided_by_workers: [w.id],
562
+ conflicting_hashes: false
563
+ });
564
+ } else {
565
+ existing.provided_by_workers = [.../* @__PURE__ */ new Set([...existing.provided_by_workers, w.id])];
566
+ if (existing.hash !== t.hash) existing.conflicting_hashes = true;
567
+ }
568
+ }
569
+ }
570
+ return [...byId.values()];
571
+ }
572
+ lookup(tool_id, url) {
573
+ return this.listFor(url).find((e) => e.id === tool_id);
574
+ }
575
+ };
576
+
577
+ // ../shared/src/capability/catalog.ts
578
+ var CAPABILITIES = [
579
+ "read:dom",
580
+ "read:image",
581
+ "read:storage",
582
+ "nav:tab",
583
+ "interact:form",
584
+ "submit:form",
585
+ "upload:file",
586
+ "httpRequest:no-cookie",
587
+ "httpRequest:cookied",
588
+ "runJS:scanned",
589
+ "runJS:unsafe",
590
+ "tab:open"
591
+ ];
592
+ var IMPLICIT_CAPABILITIES = /* @__PURE__ */ new Set([
593
+ "read:dom",
594
+ "read:image",
595
+ "nav:tab"
596
+ ]);
597
+ var DANGEROUS_CAPABILITIES = /* @__PURE__ */ new Set([
598
+ "read:storage",
599
+ "submit:form",
600
+ "upload:file",
601
+ "httpRequest:cookied",
602
+ "runJS:unsafe"
603
+ ]);
604
+ function isCapability(s) {
605
+ return CAPABILITIES.includes(s);
606
+ }
607
+
608
+ // ../shared/src/capability/tool-mapping.ts
609
+ function capabilityForTool(tool, opts) {
610
+ switch (tool) {
611
+ case "snapshotDOM":
612
+ case "querySelector":
613
+ case "querySelectorAll":
614
+ case "extractText":
615
+ case "extractFormState":
616
+ case "getValue":
617
+ return "read:dom";
618
+ case "extractImages":
619
+ return "read:image";
620
+ case "readStorage":
621
+ return "read:storage";
622
+ case "hover":
623
+ case "focus":
624
+ case "scroll":
625
+ case "waitFor":
626
+ return "nav:tab";
627
+ case "click":
628
+ case "fillInput":
629
+ case "setCheckbox":
630
+ case "selectOption":
631
+ return "interact:form";
632
+ case "submitForm":
633
+ return "submit:form";
634
+ case "uploadFile":
635
+ return "upload:file";
636
+ case "httpRequest":
637
+ return opts?.httpCookied ? "httpRequest:cookied" : "httpRequest:no-cookie";
638
+ default: {
639
+ const _exhaustive = tool;
640
+ throw new Error(`capabilityForTool: unknown tool ${_exhaustive}`);
641
+ }
642
+ }
643
+ }
644
+ function capabilityForRunJs(unsafe) {
645
+ return unsafe ? "runJS:unsafe" : "runJS:scanned";
646
+ }
647
+
648
+ // ../shared/src/capability/algebra.ts
649
+ function union(a, b) {
650
+ const out = new Set(a);
651
+ for (const c of b) out.add(c);
652
+ return out;
653
+ }
654
+ function effectiveScope(requested) {
655
+ return union(requested, IMPLICIT_CAPABILITIES);
656
+ }
657
+ function scopeCovers(requested, required) {
658
+ return effectiveScope(requested).has(required);
659
+ }
660
+
661
+ // ../coordinator/src/dispatcher.ts
662
+ var Dispatcher = class {
663
+ constructor(sessions) {
664
+ this.sessions = sessions;
665
+ }
666
+ sessions;
667
+ validate(input) {
668
+ const session = this.sessions.get(input.session_id);
669
+ if (!session) return fail("SessionNotFound", `Session ${input.session_id} not found`);
670
+ if (session.state === "expired")
671
+ return fail("SessionExpired", `Session ${input.session_id} is expired`);
672
+ if (session.state !== "active")
673
+ return fail("InternalError", `Session ${input.session_id} state=${session.state}`);
674
+ const required = input.kind === "extension_tool" ? capabilityForTool(input.tool, { httpCookied: input.httpCookied }) : capabilityForRunJs(input.unsafe);
675
+ if (!scopeCovers(session.scope, required)) {
676
+ return fail("PermissionDenied", `Capability ${required} not in session scope`, {
677
+ denied_capability: required
678
+ });
679
+ }
680
+ const dangerous = DANGEROUS_CAPABILITIES.has(required);
681
+ if (session.step_count >= QUOTA_DEFAULTS.max_steps_per_session) {
682
+ return fail(
683
+ "SessionExhausted",
684
+ `Session reached max_steps=${QUOTA_DEFAULTS.max_steps_per_session}`
685
+ );
686
+ }
687
+ if (dangerous && session.dangerous_count >= QUOTA_DEFAULTS.max_dangerous_per_session) {
688
+ return fail(
689
+ "DangerousQuotaExceeded",
690
+ `Session exceeded max_dangerous=${QUOTA_DEFAULTS.max_dangerous_per_session}`
691
+ );
692
+ }
693
+ return { ok: true, required_capability: required, dangerous };
694
+ }
695
+ };
696
+ function fail(code, message, hints) {
697
+ return {
698
+ ok: false,
699
+ error: {
700
+ code,
701
+ message,
702
+ retryable: code === "WorkerBusy" || code === "QueueFull" || code === "InternalError",
703
+ hints
704
+ }
705
+ };
706
+ }
707
+
708
+ // ../coordinator/src/coordinator.ts
709
+ var Coordinator = class {
710
+ sessions;
711
+ workers;
712
+ catalog;
713
+ dispatcher;
714
+ hub;
715
+ constructor(deps) {
716
+ this.hub = deps.hub;
717
+ this.sessions = new SessionManager(deps.clock, deps.idGen);
718
+ this.workers = new WorkerRegistry(deps.clock);
719
+ this.catalog = new Catalog(this.workers);
720
+ this.dispatcher = new Dispatcher(this.sessions);
721
+ }
722
+ // === Worker lifecycle ===
723
+ registerWorker(w) {
724
+ this.workers.register(w);
725
+ this.sessions.resumeByWorker(w.id, /* @__PURE__ */ new Set(
726
+ /* will be filled from STATE_SNAPSHOT later */
727
+ ));
728
+ }
729
+ unregisterWorker(id) {
730
+ this.workers.unregister(id);
731
+ this.sessions.pauseByWorker(id);
732
+ }
733
+ heartbeatWorker(id) {
734
+ this.workers.heartbeat(id);
735
+ }
736
+ // === Session lifecycle ===
737
+ openSession(input) {
738
+ return this.sessions.open(input);
739
+ }
740
+ closeSession(id) {
741
+ this.sessions.close(id);
742
+ }
743
+ // === Tool calls ===
744
+ validateCall(input) {
745
+ return this.dispatcher.validate(input);
746
+ }
747
+ /** Apply quota side-effects after a successful validation. Call before sending EXEC. */
748
+ recordCall(session_id, dangerous) {
749
+ this.sessions.touch(session_id, { dangerous });
750
+ }
751
+ // === Catalog & quota ===
752
+ listToolsForSession(session_id) {
753
+ const s = this.sessions.get(session_id);
754
+ if (!s) return void 0;
755
+ const worker = this.workers.get(s.worker_id);
756
+ if (!worker) return [];
757
+ const tabUrl = worker.available_tabs.find((t) => t.tab_id === s.tab_id)?.url ?? "";
758
+ return this.catalog.listFor(tabUrl);
759
+ }
760
+ quotaFor(session_id) {
761
+ return this.sessions.quota(session_id);
762
+ }
763
+ // === Periodic housekeeping ===
764
+ tick() {
765
+ const expired_sessions = this.sessions.tick();
766
+ return { expired_sessions };
767
+ }
768
+ };
769
+
770
+ // src/loopback-ws-hub.ts
771
+ import { WebSocketServer, WebSocket } from "ws";
772
+ var HEARTBEAT_INTERVAL_MS = 2e4;
773
+ var LoopbackWSHub = class {
774
+ constructor(opts) {
775
+ this.opts = opts;
776
+ this.execTimeoutMs = opts.execTimeoutMs ?? 3e4;
777
+ this.wss = new WebSocketServer({
778
+ port: opts.port,
779
+ path: "/worker",
780
+ handleProtocols: (protocols) => [...protocols][0] ?? false
781
+ });
782
+ this.wss.on("connection", (socket, req) => this.onConnection(socket, req));
783
+ }
784
+ opts;
785
+ wss;
786
+ byWorker = /* @__PURE__ */ new Map();
787
+ workerOf = /* @__PURE__ */ new Map();
788
+ pending = /* @__PURE__ */ new Map();
789
+ msgHandlers = [];
790
+ disconnectHandlers = [];
791
+ execTimeoutMs;
792
+ ready() {
793
+ return new Promise((resolve) => {
794
+ const addr = this.wss.address();
795
+ if (addr) return resolve(addr.port);
796
+ this.wss.on(
797
+ "listening",
798
+ () => resolve(this.wss.address().port)
799
+ );
800
+ });
801
+ }
802
+ close() {
803
+ for (const p of this.pending.values()) {
804
+ clearTimeout(p.timer);
805
+ p.reject(new Error("hub closing"));
806
+ }
807
+ this.pending.clear();
808
+ for (const socket of this.wss.clients) {
809
+ socket.terminate();
810
+ }
811
+ return new Promise((resolve) => this.wss.close(() => resolve()));
812
+ }
813
+ tokenOk(req) {
814
+ if (!this.opts.token) return true;
815
+ const offered = String(req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim());
816
+ return offered.includes(`bearer.${this.opts.token}`);
817
+ }
818
+ onConnection(socket, req) {
819
+ if (!this.tokenOk(req)) {
820
+ socket.close(4401, "bad token");
821
+ return;
822
+ }
823
+ socket.on("message", (raw) => this.onMessageRaw(socket, raw.toString()));
824
+ socket.on("close", () => this.onSocketClose(socket));
825
+ socket.on("error", () => {
826
+ });
827
+ }
828
+ onMessageRaw(socket, raw) {
829
+ let parsed;
830
+ try {
831
+ parsed = JSON.parse(raw);
832
+ } catch {
833
+ return;
834
+ }
835
+ const r = ClientToServerSchema.safeParse(parsed);
836
+ if (!r.success) return;
837
+ const msg = r.data;
838
+ if (msg.type === "HELLO") {
839
+ const existingSocket = this.byWorker.get(msg.worker_id);
840
+ if (existingSocket && existingSocket !== socket) {
841
+ this.workerOf.delete(existingSocket);
842
+ for (const [req_id, p] of [...this.pending]) {
843
+ if (p.worker_id === msg.worker_id) {
844
+ clearTimeout(p.timer);
845
+ this.pending.delete(req_id);
846
+ p.reject(new Error(`worker ${msg.worker_id} reconnected, exec cancelled`));
847
+ }
848
+ }
849
+ existingSocket.terminate();
850
+ }
851
+ this.byWorker.set(msg.worker_id, socket);
852
+ this.workerOf.set(socket, msg.worker_id);
853
+ this.rawSend(socket, {
854
+ type: "WELCOME",
855
+ nonce: this.opts.idGen.next("nonce"),
856
+ ts: this.opts.clock.now(),
857
+ protocol_version: PROTOCOL_VERSION,
858
+ server_time: this.opts.clock.now(),
859
+ heartbeat_interval_ms: HEARTBEAT_INTERVAL_MS
860
+ });
861
+ for (const h of this.msgHandlers) h(msg.worker_id, msg);
862
+ return;
863
+ }
864
+ if (msg.type === "RESULT") {
865
+ const p = this.pending.get(msg.req_id);
866
+ if (p) {
867
+ clearTimeout(p.timer);
868
+ this.pending.delete(msg.req_id);
869
+ p.resolve(msg);
870
+ }
871
+ return;
872
+ }
873
+ const wid = this.workerOf.get(socket);
874
+ if (wid) {
875
+ for (const h of this.msgHandlers) h(wid, msg);
876
+ }
877
+ }
878
+ onSocketClose(socket) {
879
+ const wid = this.workerOf.get(socket);
880
+ if (!wid) return;
881
+ this.workerOf.delete(socket);
882
+ this.byWorker.delete(wid);
883
+ for (const [req_id, p] of [...this.pending]) {
884
+ if (p.worker_id === wid) {
885
+ clearTimeout(p.timer);
886
+ this.pending.delete(req_id);
887
+ p.reject(new Error(`worker ${wid} disconnected`));
888
+ }
889
+ }
890
+ for (const h of this.disconnectHandlers) h(wid);
891
+ }
892
+ rawSend(socket, msg) {
893
+ const r = ServerToClientSchema.safeParse(msg);
894
+ if (!r.success) {
895
+ console.error("[hub] outgoing failed schema", r.error);
896
+ return;
897
+ }
898
+ if (socket.readyState === WebSocket.OPEN) {
899
+ socket.send(JSON.stringify(msg));
900
+ }
901
+ }
902
+ exec(worker_id, params) {
903
+ const socket = this.byWorker.get(worker_id);
904
+ if (!socket)
905
+ return Promise.reject(new Error(`worker ${worker_id} not connected`));
906
+ const req_id = this.opts.idGen.next("req");
907
+ return new Promise((resolve, reject) => {
908
+ const timer = setTimeout(() => {
909
+ this.pending.delete(req_id);
910
+ reject(
911
+ new Error(`EXEC ${req_id} timeout after ${this.execTimeoutMs}ms`)
912
+ );
913
+ }, this.execTimeoutMs);
914
+ this.pending.set(req_id, { resolve, reject, timer, worker_id });
915
+ this.rawSend(socket, {
916
+ type: "EXEC",
917
+ nonce: this.opts.idGen.next("nonce"),
918
+ ts: this.opts.clock.now(),
919
+ protocol_version: PROTOCOL_VERSION,
920
+ req_id,
921
+ session_id: params.session_id,
922
+ tab_id: params.tab_id,
923
+ step: { tool: params.step.tool, args: params.step.args }
924
+ });
925
+ if (this.byWorker.get(worker_id) !== socket) {
926
+ clearTimeout(timer);
927
+ this.pending.delete(req_id);
928
+ reject(new Error(`worker ${worker_id} disconnected`));
929
+ }
930
+ });
931
+ }
932
+ async send(worker_id, msg) {
933
+ const socket = this.byWorker.get(worker_id);
934
+ if (!socket) throw new Error(`worker ${worker_id} not connected`);
935
+ this.rawSend(socket, msg);
936
+ }
937
+ onMessage(handler) {
938
+ this.msgHandlers.push(handler);
939
+ }
940
+ onDisconnect(handler) {
941
+ this.disconnectHandlers.push(handler);
942
+ }
943
+ connectedWorkers() {
944
+ return [...this.byWorker.keys()];
945
+ }
946
+ // Intentionally fire-and-forget: actual cleanup (byWorker/workerOf removal and
947
+ // pending-exec rejection) happens asynchronously via the socket's close event.
948
+ // v1 callers do not await this method.
949
+ async disconnect(worker_id, _reason) {
950
+ const socket = this.byWorker.get(worker_id);
951
+ if (socket) socket.close();
952
+ }
953
+ };
954
+
955
+ // src/mcp-server.ts
956
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
957
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
958
+
959
+ // src/control-tools.ts
960
+ var CONTROL_TOOLS = [
961
+ {
962
+ name: "list_tabs",
963
+ description: "\u5217\u51FA\u5F53\u524D\u8FDE\u5165\u7684\u6D4F\u89C8\u5668\uFF08worker\uFF09\u53EF\u7528\u7684\u6807\u7B7E\u9875\uFF1A[{tab_id,url,title}]\u3002\u5148\u8C03\u5B83\u62FF tab_id\u3002",
964
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
965
+ },
966
+ {
967
+ name: "open_session",
968
+ description: "\u4E3A\u67D0\u4E2A tab \u5F00\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u8FD4\u56DE session_id\uFF1B\u540E\u7EED browser_* \u5DE5\u5177\u90FD\u5E26\u8FD9\u4E2A session_id\u3002capabilities \u7701\u7565=\u6388\u4E88\u5168\u90E8\u80FD\u529B\u3002",
969
+ inputSchema: {
970
+ type: "object",
971
+ required: ["tab_id"],
972
+ properties: {
973
+ tab_id: { type: "string", description: "list_tabs \u8FD4\u56DE\u7684 tab_id" },
974
+ capabilities: { type: "array", items: { type: "string" }, description: "\u80FD\u529B\u57DF\u767D\u540D\u5355\uFF1B\u7701\u7565=\u5168\u90E8" },
975
+ idle_timeout_min: { type: "number", description: "\u8986\u76D6\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\uFF08\u5206\u949F\uFF09" }
976
+ },
977
+ additionalProperties: false
978
+ }
979
+ },
980
+ {
981
+ name: "close_session",
982
+ description: "\u5173\u95ED\u4F1A\u8BDD\u3002",
983
+ inputSchema: { type: "object", required: ["session_id"], properties: { session_id: { type: "string" } }, additionalProperties: false }
984
+ },
985
+ {
986
+ name: "get_quota",
987
+ description: "\u67E5\u8BE2\u4F1A\u8BDD\u5269\u4F59\u9884\u7B97\uFF1Asteps/dangerous \u5DF2\u7528\u4E0E\u4E0A\u9650\u3001\u8DDD\u8FC7\u671F\u65F6\u95F4\u3002",
988
+ inputSchema: { type: "object", required: ["session_id"], properties: { session_id: { type: "string" } }, additionalProperties: false }
989
+ }
990
+ ];
991
+
992
+ // ../shared/src/llm/builtin-tool-defs.ts
993
+ var TOOL_DEFS = [
994
+ {
995
+ name: "snapshotDOM",
996
+ description: "\u9875\u9762 DOM \u6458\u8981\uFF1A\u8FD4\u56DE\u4ECE root \u5F00\u59CB\u7684\u7B80\u5316\u6811\uFF0C\u542B tag/id/classes/\u76F4\u63A5\u6587\u672C/children\u3002\u4F18\u5148\u5728\u6BCF\u6B21\u4EFB\u52A1\u5F00\u59CB\u7528\u4E00\u6B21\u4EE5\u4E86\u89E3\u7ED3\u6784\u3002",
997
+ input_schema: {
998
+ type: "object",
999
+ properties: {
1000
+ maxDepth: { type: "integer", default: 3 },
1001
+ root: { type: "string", description: "\u53EF\u9009\u7684 CSS \u9009\u62E9\u5668\uFF1B\u627E\u4E0D\u5230\u65F6\u9000\u56DE\u5230 <html>" },
1002
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1003
+ }
1004
+ }
1005
+ },
1006
+ {
1007
+ name: "querySelector",
1008
+ description: "\u8FD4\u56DE\u9996\u4E2A\u5339\u914D\u5143\u7D20\u7684\u6D45\u5C42\u6458\u8981 (tag/id/classes/text/attrs)\u3002",
1009
+ input_schema: {
1010
+ type: "object",
1011
+ properties: {
1012
+ selector: { type: "string" },
1013
+ root: { type: "string" },
1014
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1015
+ },
1016
+ required: ["selector"]
1017
+ }
1018
+ },
1019
+ {
1020
+ name: "querySelectorAll",
1021
+ description: "\u8FD4\u56DE\u6240\u6709\u5339\u914D\u5143\u7D20\u7684\u6D45\u5C42\u6458\u8981\u6570\u7EC4\u3002",
1022
+ input_schema: {
1023
+ type: "object",
1024
+ properties: {
1025
+ selector: { type: "string" },
1026
+ root: { type: "string" },
1027
+ limit: { type: "integer" },
1028
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1029
+ },
1030
+ required: ["selector"]
1031
+ }
1032
+ },
1033
+ {
1034
+ name: "extractText",
1035
+ description: "\u63D0\u53D6\u9009\u62E9\u5668\u547D\u4E2D\u7684\u5143\u7D20\u6587\u672C\u3002single=true \u8FD4\u56DE\u4E00\u4E2A\u5B57\u7B26\u4E32\uFF0C\u5426\u5219\u8FD4\u56DE\u6570\u7EC4\u3002",
1036
+ input_schema: {
1037
+ type: "object",
1038
+ properties: {
1039
+ selector: { type: "string" },
1040
+ root: { type: "string" },
1041
+ single: { type: "boolean" },
1042
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1043
+ },
1044
+ required: ["selector"]
1045
+ }
1046
+ },
1047
+ {
1048
+ name: "extractImages",
1049
+ description: "\u5728 root \u8303\u56F4\u5185\u63D0\u53D6\u6240\u6709 <img> \u7684 src/data-src/srcset\uFF1BincludeBg=true \u65F6\u4E5F\u63D0\u53D6\u80CC\u666F\u56FE\u3002\u8FD4\u56DE {url, via}[].",
1050
+ input_schema: {
1051
+ type: "object",
1052
+ properties: {
1053
+ root: { type: "string" },
1054
+ includeBg: { type: "boolean", default: false },
1055
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1056
+ }
1057
+ }
1058
+ },
1059
+ {
1060
+ name: "scroll",
1061
+ description: "\u6EDA\u52A8\u9875\u9762\u3002to \u53EF\u4E3A 'bottom'|'top'|number\uFF1Bmax \u662F\u6EDA\u52A8\u6B21\u6570\uFF1BuntilSelector \u51FA\u73B0\u65F6\u63D0\u524D\u505C\u3002",
1062
+ input_schema: {
1063
+ type: "object",
1064
+ properties: {
1065
+ to: { description: "'bottom' | 'top' | number" },
1066
+ max: { type: "integer", default: 1 },
1067
+ intervalMs: { type: "integer", default: 250 },
1068
+ untilSelector: { type: "string" },
1069
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1070
+ },
1071
+ required: ["to"]
1072
+ }
1073
+ },
1074
+ {
1075
+ name: "waitFor",
1076
+ description: "\u7B49\u5F85\u56FA\u5B9A ms\uFF0C\u6216\u7B49\u5F85\u9009\u62E9\u5668\u51FA\u73B0\uFF08\u5E26 timeoutMs \u515C\u5E95\uFF09\u3002",
1077
+ input_schema: {
1078
+ type: "object",
1079
+ properties: {
1080
+ ms: { type: "integer" },
1081
+ selector: { type: "string" },
1082
+ timeoutMs: { type: "integer", default: 5e3 },
1083
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1084
+ }
1085
+ }
1086
+ },
1087
+ {
1088
+ name: "click",
1089
+ description: "\u70B9\u51FB\u9009\u62E9\u5668\u547D\u4E2D\u7684\u5143\u7D20\u3002required=false \u65F6\u627E\u4E0D\u5230\u4E0D\u62A5\u9519\u3002\u9700\u8981\u4EBA\u5DE5\u5BA1\u9605\u3002",
1090
+ input_schema: {
1091
+ type: "object",
1092
+ properties: {
1093
+ selector: { type: "string" },
1094
+ required: { type: "boolean", default: true },
1095
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1096
+ },
1097
+ required: ["selector"]
1098
+ }
1099
+ },
1100
+ {
1101
+ name: "httpRequest",
1102
+ description: "\u901A\u8FC7\u540E\u53F0\u4EE3\u7406\u53D1\u8BF7\u6C42\u3002withCredentials=true \u65F6\u5E26 cookie\uFF0C\u9700\u8981\u4EBA\u5DE5\u5BA1\u9605\uFF1B\u9ED8\u8BA4 omit\u3002",
1103
+ input_schema: {
1104
+ type: "object",
1105
+ properties: {
1106
+ url: { type: "string" },
1107
+ method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE", "PATCH"], default: "GET" },
1108
+ headers: { type: "object" },
1109
+ body: { type: "string" },
1110
+ withCredentials: { type: "boolean", default: false },
1111
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1112
+ },
1113
+ required: ["url"]
1114
+ }
1115
+ },
1116
+ {
1117
+ name: "readStorage",
1118
+ description: "\u8BFB localStorage \u6216 sessionStorage \u7684\u6307\u5B9A key\u3002\u9700\u8981\u4EBA\u5DE5\u5BA1\u9605\u3002",
1119
+ input_schema: {
1120
+ type: "object",
1121
+ properties: {
1122
+ store: { type: "string", enum: ["local", "session"] },
1123
+ key: { type: "string" },
1124
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1125
+ },
1126
+ required: ["store", "key"]
1127
+ }
1128
+ },
1129
+ {
1130
+ name: "fillInput",
1131
+ description: "\u5F80 input/textarea/contenteditable \u586B\u503C\uFF1B\u89E6\u53D1 input/change \u4E8B\u4EF6\u4EE5\u517C\u5BB9 React/Vue\u3002clear=true\uFF08\u9ED8\u8BA4\uFF09\u4F1A\u5148\u6E05\u7A7A\u518D\u586B\u3002",
1132
+ input_schema: {
1133
+ type: "object",
1134
+ properties: {
1135
+ selector: { type: "string" },
1136
+ value: { type: "string" },
1137
+ clear: { type: "boolean", default: true },
1138
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1139
+ },
1140
+ required: ["selector", "value"]
1141
+ }
1142
+ },
1143
+ {
1144
+ name: "setCheckbox",
1145
+ description: "\u8BBE\u7F6E checkbox \u52FE\u9009\u72B6\u6001\uFF1B\u6D3E\u53D1 change \u4E8B\u4EF6\u3002",
1146
+ input_schema: {
1147
+ type: "object",
1148
+ properties: {
1149
+ selector: { type: "string" },
1150
+ checked: { type: "boolean" },
1151
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1152
+ },
1153
+ required: ["selector", "checked"]
1154
+ }
1155
+ },
1156
+ {
1157
+ name: "selectOption",
1158
+ description: "<select> \u5143\u7D20\u6309 value \u6216 label \u9009\u9879\u3002\u540C\u65F6\u7ED9\u4E24\u8005\u65F6\u4F18\u5148 value\u3002",
1159
+ input_schema: {
1160
+ type: "object",
1161
+ properties: {
1162
+ selector: { type: "string" },
1163
+ value: { type: "string" },
1164
+ label: { type: "string" },
1165
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1166
+ },
1167
+ required: ["selector"]
1168
+ }
1169
+ },
1170
+ {
1171
+ name: "submitForm",
1172
+ description: "\u63D0\u4EA4 <form>\u3002\u4F1A\u89E6\u53D1\u670D\u52A1\u7AEF\u52A8\u4F5C\uFF08\u4E0B\u5355\u3001\u7559\u8A00\u7B49\uFF09\uFF0C\u9700\u8981\u5BA1\u9605\u3002",
1173
+ input_schema: {
1174
+ type: "object",
1175
+ properties: {
1176
+ selector: { type: "string", default: "form" },
1177
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1178
+ }
1179
+ }
1180
+ },
1181
+ {
1182
+ name: "hover",
1183
+ description: "\u628A\u9F20\u6807\u60AC\u505C\u5728\u5143\u7D20\u4E0A\uFF08\u89E6\u53D1 mouseenter / mouseover / mousemove\uFF09\u3002",
1184
+ input_schema: {
1185
+ type: "object",
1186
+ properties: {
1187
+ selector: { type: "string" },
1188
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1189
+ },
1190
+ required: ["selector"]
1191
+ }
1192
+ },
1193
+ {
1194
+ name: "focus",
1195
+ description: "\u628A\u7126\u70B9\u7ED9\u67D0\u5143\u7D20\uFF08\u89E6\u53D1 focus / focusin\uFF09\u3002",
1196
+ input_schema: {
1197
+ type: "object",
1198
+ properties: {
1199
+ selector: { type: "string" },
1200
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1201
+ },
1202
+ required: ["selector"]
1203
+ }
1204
+ },
1205
+ {
1206
+ name: "uploadFile",
1207
+ description: "\u628A\u540E\u7AEF\u4EE3\u7406\u62C9\u5230\u7684\u6587\u4EF6\u586B\u5230 <input type=file>\u3002\u67D0\u4E9B\u7AD9\u70B9\u4F1A\u62D2\u7EDD\u5408\u6210 File\uFF08isTrusted \u6821\u9A8C\uFF09\u3002",
1208
+ input_schema: {
1209
+ type: "object",
1210
+ properties: {
1211
+ selector: { type: "string" },
1212
+ url: { type: "string" },
1213
+ filename: { type: "string" },
1214
+ mime: { type: "string" },
1215
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1216
+ },
1217
+ required: ["selector", "url"]
1218
+ }
1219
+ },
1220
+ {
1221
+ name: "getValue",
1222
+ description: "\u8BFB input/select/textarea/contenteditable \u7684\u5F53\u524D\u503C\u3002",
1223
+ input_schema: {
1224
+ type: "object",
1225
+ properties: {
1226
+ selector: { type: "string" },
1227
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1228
+ },
1229
+ required: ["selector"]
1230
+ }
1231
+ },
1232
+ {
1233
+ name: "extractFormState",
1234
+ description: "\u628A <form> \u5185\u6240\u6709\u53EF\u586B\u5B57\u6BB5\u8BFB\u6210 {name: value} \u5BF9\u8C61\uFF08radio \u53D6\u9009\u4E2D\u503C\uFF1Bcheckbox \u591A\u9009\u53D6\u6570\u7EC4\uFF09\u3002",
1235
+ input_schema: {
1236
+ type: "object",
1237
+ properties: {
1238
+ selector: { type: "string", default: "form" },
1239
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1240
+ }
1241
+ }
1242
+ },
1243
+ {
1244
+ name: "runJS",
1245
+ description: "\u5728 MAIN world \u6CE8\u5165\u5E76\u6267\u884C\u4E00\u6BB5 async \u51FD\u6570\u4F53\uFF08receives `ctx` = bindings\uFF09\u3002\u52A1\u5FC5\u4F7F\u7528 return \u8FD4\u56DE\u503C\u3002\u4EC5\u5728\u7ED3\u6784\u5316\u5DE5\u5177\u4E0D\u591F\u7528\u65F6\u4F7F\u7528\uFF0C\u4F1A\u7ECF\u8FC7\u9759\u6001\u626B\u63CF\u4E0E\u4EBA\u5DE5\u5BA1\u9605\u3002",
1246
+ input_schema: {
1247
+ type: "object",
1248
+ properties: {
1249
+ source: { type: "string", description: "async function body" },
1250
+ tabId: { type: "integer", description: "\u76EE\u6807 tab\u3002\u8981\u64CD\u4F5C\u975E\u672C\u4F1A\u8BDD tab \u624D\u586B\uFF0C\u4E14\u5FC5\u987B\u5DF2\u5728 attachedTabs\uFF08\u53EF\u8C03\u7528 attachTab \u7533\u8BF7\uFF09\u3002\u64CD\u4F5C\u672C\u4F1A\u8BDD tab \u8BF7\u6574\u4E2A\u4E0D\u5E26\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u8981\u4F20 0 \u6216 null" }
1251
+ },
1252
+ required: ["source"]
1253
+ }
1254
+ },
1255
+ {
1256
+ name: "listTabs",
1257
+ description: "\u5217\u51FA\u6240\u6709\u7A97\u53E3\u7684\u53EF\u8BBF\u95EE tab\uFF1B\u8FD4\u56DE [{tabId, windowId, url, title, attached, isCurrent}]\u3002\u4EC5\u5728\u4F60\u9700\u8981\u8BC6\u522B\u65B0 tab \u65F6\u8C03\u7528\u3002",
1258
+ input_schema: {
1259
+ type: "object",
1260
+ properties: {
1261
+ windowId: { type: "integer", description: "\u4EC5\u8FD4\u56DE\u6B64\u7A97\u53E3\u7684 tab\uFF1B\u7701\u7565=\u5168\u90E8\u7A97\u53E3" }
1262
+ }
1263
+ }
1264
+ },
1265
+ {
1266
+ name: "openTab",
1267
+ description: "\u6253\u5F00\u65B0 tab\uFF0C\u6210\u529F\u540E\u81EA\u52A8\u52A0\u5165\u4F1A\u8BDD attachedTabs\uFF08source=ai-open\uFF09\u3002\u8FD4\u56DE {tabId, url, title}\u3002",
1268
+ input_schema: {
1269
+ type: "object",
1270
+ properties: {
1271
+ url: { type: "string" },
1272
+ active: { type: "boolean", default: false, description: "true=\u5207\u5230\u8BE5 tab" }
1273
+ },
1274
+ required: ["url"]
1275
+ }
1276
+ },
1277
+ {
1278
+ name: "attachTab",
1279
+ description: "\u8BF7\u6C42\u628A\u67D0\u4E2A\u5DF2\u6253\u5F00\u7684 tab \u7EB3\u5165\u4F1A\u8BDD attachedTabs\uFF1B\u672A\u9884\u6279\u51C6\u65F6\u4F1A\u5411\u7528\u6237\u7D22\u53D6\u5BA1\u6279\u3002",
1280
+ input_schema: {
1281
+ type: "object",
1282
+ properties: {
1283
+ tabId: { type: "integer" },
1284
+ reason: { type: "string", description: "\u5411\u7528\u6237\u89E3\u91CA\u4E3A\u4F55\u9700\u8981\u8BBF\u95EE\u8BE5 tab" }
1285
+ },
1286
+ required: ["tabId"]
1287
+ }
1288
+ },
1289
+ {
1290
+ name: "detachTab",
1291
+ description: "\u4ECE\u4F1A\u8BDD attachedTabs \u79FB\u9664\u4E00\u4E2A tab\uFF1B\u4E0D\u5173\u95ED\u8BE5 tab\u3002",
1292
+ input_schema: {
1293
+ type: "object",
1294
+ properties: { tabId: { type: "integer" } },
1295
+ required: ["tabId"]
1296
+ }
1297
+ }
1298
+ ];
1299
+
1300
+ // src/tool-gen.ts
1301
+ var EXEC_TOOL_NAMES = [
1302
+ "snapshotDOM",
1303
+ "querySelector",
1304
+ "querySelectorAll",
1305
+ "extractText",
1306
+ "extractImages",
1307
+ "getValue",
1308
+ "extractFormState",
1309
+ "hover",
1310
+ "focus",
1311
+ "scroll",
1312
+ "waitFor",
1313
+ "click",
1314
+ "fillInput",
1315
+ "setCheckbox",
1316
+ "selectOption",
1317
+ "httpRequest",
1318
+ "submitForm",
1319
+ "uploadFile",
1320
+ "readStorage"
1321
+ ];
1322
+ function rebuildSchema(src) {
1323
+ const s = src;
1324
+ const properties = { ...s.properties ?? {} };
1325
+ delete properties.tabId;
1326
+ properties.session_id = { type: "string", description: "open_session \u8FD4\u56DE\u7684\u4F1A\u8BDD id\uFF08\u51B3\u5B9A\u76EE\u6807 worker \u4E0E tab\uFF09" };
1327
+ const required = [.../* @__PURE__ */ new Set([...(s.required ?? []).filter((r) => r !== "tabId"), "session_id"])];
1328
+ return { type: "object", properties, required };
1329
+ }
1330
+ function generateBrowserTools() {
1331
+ const allow = new Set(EXEC_TOOL_NAMES);
1332
+ return TOOL_DEFS.filter((t) => allow.has(t.name)).map((t) => ({
1333
+ name: `browser_${t.name}`,
1334
+ builtinTool: t.name,
1335
+ description: t.description,
1336
+ inputSchema: rebuildSchema(t.input_schema)
1337
+ }));
1338
+ }
1339
+
1340
+ // src/handlers.ts
1341
+ function singleWorkerId(c) {
1342
+ const workers = c.workers.list();
1343
+ if (workers.length === 0) throw new Error("\u6CA1\u6709\u6D4F\u89C8\u5668\u8FDE\u5165\uFF0C\u8BF7\u5728\u6269\u5C55\u8BBE\u7F6E\u9875\u586B ws://127.0.0.1:<port>/worker \u8FDE\u63A5");
1344
+ if (workers.length > 1) throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
1345
+ return workers[0].id;
1346
+ }
1347
+ function handleListTabs(deps) {
1348
+ const w = deps.coordinator.workers.get(singleWorkerId(deps.coordinator));
1349
+ return { tabs: w.available_tabs };
1350
+ }
1351
+ function handleOpenSession(deps, args) {
1352
+ const worker_id = singleWorkerId(deps.coordinator);
1353
+ const tab_id = String(args.tab_id);
1354
+ const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
1355
+ const scope = new Set(requested.length ? requested : CAPABILITIES);
1356
+ const idle_timeout_ms = typeof args.idle_timeout_min === "number" ? args.idle_timeout_min * 6e4 : void 0;
1357
+ const s = deps.coordinator.openSession({ ai_client_fingerprint: "mcp-local", worker_id, tab_id, scope, idle_timeout_ms });
1358
+ return { session_id: s.id };
1359
+ }
1360
+ function handleCloseSession(deps, args) {
1361
+ deps.coordinator.closeSession(String(args.session_id));
1362
+ return { ok: true };
1363
+ }
1364
+ function handleGetQuota(deps, args) {
1365
+ const q = deps.coordinator.quotaFor(String(args.session_id));
1366
+ if (!q) throw new Error(`session ${String(args.session_id)} not found`);
1367
+ return q;
1368
+ }
1369
+ async function handleBrowserTool(deps, gen, args) {
1370
+ const session_id = String(args.session_id);
1371
+ const session = deps.coordinator.sessions.get(session_id);
1372
+ if (!session) throw new Error(`session ${session_id} not found`);
1373
+ const { session_id: _omit, ...toolArgs } = args;
1374
+ const tool = gen.builtinTool;
1375
+ const httpCookied = tool === "httpRequest" ? Boolean(toolArgs.withCredentials) : void 0;
1376
+ const v = deps.coordinator.validateCall({ session_id, kind: "extension_tool", tool, httpCookied });
1377
+ if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1378
+ deps.coordinator.recordCall(session_id, v.dangerous);
1379
+ const result = await deps.hub.exec(session.worker_id, { session_id, tab_id: session.tab_id, step: { tool, args: toolArgs } });
1380
+ if (!result.ok) throw new Error(result.error ? `${result.error.code}: ${result.error.message}` : "EXEC failed");
1381
+ return result.return ?? null;
1382
+ }
1383
+
1384
+ // src/mcp-server.ts
1385
+ var BROWSER_TOOLS = generateBrowserTools();
1386
+ var BROWSER_BY_NAME = new Map(BROWSER_TOOLS.map((t) => [t.name, t]));
1387
+ function buildToolList() {
1388
+ return [
1389
+ ...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
1390
+ ...BROWSER_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
1391
+ ];
1392
+ }
1393
+ var ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data ?? null) }] });
1394
+ var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
1395
+ async function dispatchCall(deps, name, args) {
1396
+ try {
1397
+ if (name === "list_tabs") return ok(handleListTabs(deps));
1398
+ if (name === "open_session") return ok(handleOpenSession(deps, args));
1399
+ if (name === "close_session") return ok(handleCloseSession(deps, args));
1400
+ if (name === "get_quota") return ok(handleGetQuota(deps, args));
1401
+ const gen = BROWSER_BY_NAME.get(name);
1402
+ if (gen) return ok(await handleBrowserTool(deps, gen, args));
1403
+ return fail2(`unknown tool: ${name}`);
1404
+ } catch (e) {
1405
+ return fail2(e instanceof Error ? e.message : String(e));
1406
+ }
1407
+ }
1408
+ function createMcpServer(deps) {
1409
+ const server = new Server({ name: "atwebpilot-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
1410
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList() }));
1411
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1412
+ const args = req.params.arguments ?? {};
1413
+ return dispatchCall(deps, req.params.name, args);
1414
+ });
1415
+ return server;
1416
+ }
1417
+
1418
+ // src/wire.ts
1419
+ function helloToWorker(h, now) {
1420
+ return {
1421
+ id: h.worker_id,
1422
+ fingerprint: h.fingerprint,
1423
+ capabilities: new Set(h.capabilities.filter(isCapability)),
1424
+ attended: h.attended,
1425
+ labels: new Set(h.labels),
1426
+ available_tabs: h.available_tabs,
1427
+ saved_tools: h.saved_tools,
1428
+ protocol_version: h.protocol_version,
1429
+ connected_at: now,
1430
+ last_heartbeat_at: now
1431
+ };
1432
+ }
1433
+ function installWire(hub, coordinator, clock) {
1434
+ hub.onMessage((worker_id, msg) => {
1435
+ switch (msg.type) {
1436
+ case "HELLO":
1437
+ coordinator.unregisterWorker(msg.worker_id);
1438
+ coordinator.registerWorker(helloToWorker(msg, clock.now()));
1439
+ break;
1440
+ case "PING":
1441
+ coordinator.heartbeatWorker(worker_id);
1442
+ break;
1443
+ default:
1444
+ break;
1445
+ }
1446
+ });
1447
+ hub.onDisconnect((worker_id) => coordinator.unregisterWorker(worker_id));
1448
+ }
1449
+
1450
+ // src/index.ts
1451
+ async function main() {
1452
+ const port = Number(process.env.ATWEBPILOT_WS_PORT ?? 8787);
1453
+ const token = process.env.ATWEBPILOT_WS_TOKEN || void 0;
1454
+ const clock = new DefaultClock();
1455
+ const idGen = new DefaultIdGen();
1456
+ const hub = new LoopbackWSHub({ port, token, clock, idGen });
1457
+ await hub.ready();
1458
+ const coordinator = new Coordinator({ hub, clock, idGen });
1459
+ installWire(hub, coordinator, clock);
1460
+ const server = createMcpServer({ coordinator, hub });
1461
+ await server.connect(new StdioServerTransport());
1462
+ console.error(`[atwebpilot-mcp] ws://127.0.0.1:${port}/worker ready; stdio MCP connected`);
1463
+ }
1464
+ main().catch((e) => {
1465
+ console.error("[atwebpilot-mcp] fatal", e);
1466
+ process.exit(1);
1467
+ });