@attson/atwebpilot-mcp 0.0.55 → 0.0.57

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 (3) hide show
  1. package/README.md +72 -7
  2. package/dist/index.js +883 -292
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -217,6 +217,14 @@ var StepSchema = z4.object({
217
217
  tool: z4.string(),
218
218
  args: z4.unknown()
219
219
  });
220
+ var DerivedTabFields = {
221
+ /** true when the receiving connection owns this tab */
222
+ mine: z4.boolean().optional(),
223
+ /** true when some *other* connection owns it */
224
+ busy: z4.boolean().optional(),
225
+ /** the owning session's label, present only when busy */
226
+ busy_label: z4.string().optional()
227
+ };
220
228
  var HelloSchema = z4.object({
221
229
  ...EnvelopeFields,
222
230
  type: z4.literal("HELLO"),
@@ -227,12 +235,20 @@ var HelloSchema = z4.object({
227
235
  chrome: z4.string()
228
236
  }),
229
237
  capabilities: z4.array(z4.string()),
238
+ /**
239
+ * Tool names this worker can actually execute. Optional for backward
240
+ * compatibility: an extension predating Plan 32 does not send it, and the
241
+ * server then falls back to the legacy 19-tool surface rather than
242
+ * advertising tools that would fail at call time with "unknown tool".
243
+ */
244
+ supported_tools: z4.array(z4.string()).optional(),
230
245
  attended: z4.boolean(),
231
246
  available_tabs: z4.array(
232
247
  z4.object({
233
248
  tab_id: z4.string(),
234
249
  url: z4.string(),
235
- title: z4.string().optional()
250
+ title: z4.string().optional(),
251
+ ...DerivedTabFields
236
252
  })
237
253
  ),
238
254
  saved_tools: z4.array(
@@ -383,6 +399,24 @@ var SidepanelStateReplySchema = z4.object({
383
399
  lastSystemNote: z4.string().optional()
384
400
  }).optional()
385
401
  });
402
+ var SessionOpenedSchema = z4.object({
403
+ ...EnvelopeFields,
404
+ type: z4.literal("SESSION_OPENED"),
405
+ session_id: z4.string(),
406
+ tab_id: z4.string()
407
+ });
408
+ var TabsUpdateSchema = z4.object({
409
+ ...EnvelopeFields,
410
+ type: z4.literal("TABS_UPDATE"),
411
+ tabs: z4.array(
412
+ z4.object({
413
+ tab_id: z4.string(),
414
+ url: z4.string(),
415
+ title: z4.string().optional(),
416
+ ...DerivedTabFields
417
+ })
418
+ )
419
+ });
386
420
  var ClientToServerSchema = z4.discriminatedUnion("type", [
387
421
  HelloSchema,
388
422
  PingSchema,
@@ -392,7 +426,8 @@ var ClientToServerSchema = z4.discriminatedUnion("type", [
392
426
  SessionEventSchema,
393
427
  StateSnapshotSchema,
394
428
  ChatEventSchema,
395
- SidepanelStateReplySchema
429
+ SidepanelStateReplySchema,
430
+ TabsUpdateSchema
396
431
  ]);
397
432
  var ServerToClientSchema = z4.discriminatedUnion("type", [
398
433
  WelcomeSchema,
@@ -402,7 +437,8 @@ var ServerToClientSchema = z4.discriminatedUnion("type", [
402
437
  CloseSessionSchema,
403
438
  StartChatSessionSchema,
404
439
  AbortSessionSchema,
405
- ReadSidepanelStateSchema
440
+ ReadSidepanelStateSchema,
441
+ SessionOpenedSchema
406
442
  ]);
407
443
  var ProtocolMessageSchema = z4.union([ClientToServerSchema, ServerToClientSchema]);
408
444
 
@@ -613,19 +649,26 @@ var CAPABILITIES = [
613
649
  "httpRequest:cookied",
614
650
  "runJS:scanned",
615
651
  "runJS:unsafe",
616
- "tab:open"
652
+ "tab:open",
653
+ // Plan 32 — page-event recorder
654
+ "read:console",
655
+ "read:network",
656
+ "read:network-body"
617
657
  ];
618
658
  var IMPLICIT_CAPABILITIES = /* @__PURE__ */ new Set([
619
659
  "read:dom",
620
660
  "read:image",
621
- "nav:tab"
661
+ "nav:tab",
662
+ "read:console"
622
663
  ]);
623
664
  var DANGEROUS_CAPABILITIES = /* @__PURE__ */ new Set([
624
665
  "read:storage",
625
666
  "submit:form",
626
667
  "upload:file",
627
668
  "httpRequest:cookied",
628
- "runJS:unsafe"
669
+ "runJS:unsafe",
670
+ // response headers routinely carry Authorization / Set-Cookie / bearer tokens
671
+ "read:network-body"
629
672
  ]);
630
673
  function isCapability(s) {
631
674
  return CAPABILITIES.includes(s);
@@ -703,6 +746,26 @@ function capabilityForTool(tool, opts) {
703
746
  // can be added later if a finer distinction is needed.
704
747
  case "writeStorage":
705
748
  return "read:storage";
749
+ // Plan 32 — playwright parity
750
+ case "consoleMessages":
751
+ return "read:console";
752
+ case "networkRequests":
753
+ return "read:network";
754
+ case "networkRequestDetail":
755
+ return "read:network-body";
756
+ case "recorderConfig":
757
+ return opts?.recorderArmsBodies ? "read:network-body" : "read:network";
758
+ case "handleDialog":
759
+ case "drag":
760
+ return "interact:form";
761
+ case "drop":
762
+ return opts?.dropHasFiles ? "upload:file" : "interact:form";
763
+ case "navigateBack":
764
+ case "navigateForward":
765
+ case "resize":
766
+ return "nav:tab";
767
+ case "findElements":
768
+ return "read:dom";
706
769
  default: {
707
770
  const _exhaustive = tool;
708
771
  throw new Error(`capabilityForTool: unknown tool ${_exhaustive}`);
@@ -739,7 +802,11 @@ var Dispatcher = class {
739
802
  return fail("SessionExpired", `Session ${input.session_id} is expired`);
740
803
  if (session.state !== "active")
741
804
  return fail("InternalError", `Session ${input.session_id} state=${session.state}`);
742
- const required = input.kind === "extension_tool" ? capabilityForTool(input.tool, { httpCookied: input.httpCookied }) : capabilityForRunJs(input.unsafe);
805
+ const required = input.kind === "extension_tool" ? capabilityForTool(input.tool, {
806
+ httpCookied: input.httpCookied,
807
+ dropHasFiles: input.dropHasFiles,
808
+ recorderArmsBodies: input.recorderArmsBodies
809
+ }) : input.kind === "capability" ? input.capability : capabilityForRunJs(input.unsafe);
743
810
  if (!scopeCovers(session.scope, required)) {
744
811
  return fail("PermissionDenied", `Capability ${required} not in session scope`, {
745
812
  denied_capability: required
@@ -780,8 +847,12 @@ var Coordinator = class {
780
847
  catalog;
781
848
  dispatcher;
782
849
  hub;
850
+ clock;
851
+ idGen;
783
852
  constructor(deps) {
784
853
  this.hub = deps.hub;
854
+ this.clock = deps.clock;
855
+ this.idGen = deps.idGen;
785
856
  this.sessions = new SessionManager(deps.clock, deps.idGen);
786
857
  this.workers = new WorkerRegistry(deps.clock);
787
858
  this.catalog = new Catalog(this.workers);
@@ -803,7 +874,17 @@ var Coordinator = class {
803
874
  }
804
875
  // === Session lifecycle ===
805
876
  openSession(input) {
806
- return this.sessions.open(input);
877
+ const session = this.sessions.open(input);
878
+ void this.hub.send(session.worker_id, {
879
+ type: "SESSION_OPENED",
880
+ nonce: this.idGen.next("nonce"),
881
+ ts: this.clock.now(),
882
+ protocol_version: PROTOCOL_VERSION,
883
+ session_id: session.id,
884
+ tab_id: session.tab_id
885
+ }).catch(() => {
886
+ });
887
+ return session;
807
888
  }
808
889
  closeSession(id) {
809
890
  this.sessions.close(id);
@@ -835,232 +916,6 @@ var Coordinator = class {
835
916
  }
836
917
  };
837
918
 
838
- // src/loopback-ws-hub.ts
839
- import { WebSocketServer, WebSocket } from "ws";
840
- var HEARTBEAT_INTERVAL_MS = 2e4;
841
- var DEFAULT_KEEPALIVE_INTERVAL_MS = 15e3;
842
- var LoopbackWSHub = class {
843
- constructor(opts) {
844
- this.opts = opts;
845
- this.execTimeoutMs = opts.execTimeoutMs ?? 3e4;
846
- this.keepaliveIntervalMs = opts.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
847
- this.wss = new WebSocketServer({
848
- port: opts.port,
849
- path: "/worker",
850
- handleProtocols: (protocols) => [...protocols][0] ?? false
851
- });
852
- this.wss.on("connection", (socket, req) => this.onConnection(socket, req));
853
- }
854
- opts;
855
- wss;
856
- byWorker = /* @__PURE__ */ new Map();
857
- workerOf = /* @__PURE__ */ new Map();
858
- pending = /* @__PURE__ */ new Map();
859
- msgHandlers = [];
860
- disconnectHandlers = [];
861
- execTimeoutMs;
862
- keepaliveIntervalMs;
863
- keepaliveTimers = /* @__PURE__ */ new Map();
864
- ready() {
865
- return new Promise((resolve) => {
866
- const addr = this.wss.address();
867
- if (addr) return resolve(addr.port);
868
- this.wss.on(
869
- "listening",
870
- () => resolve(this.wss.address().port)
871
- );
872
- });
873
- }
874
- close() {
875
- for (const p of this.pending.values()) {
876
- clearTimeout(p.timer);
877
- p.reject(new Error("hub closing"));
878
- }
879
- this.pending.clear();
880
- for (const timer of this.keepaliveTimers.values()) clearInterval(timer);
881
- this.keepaliveTimers.clear();
882
- for (const socket of this.wss.clients) {
883
- socket.terminate();
884
- }
885
- return new Promise((resolve) => this.wss.close(() => resolve()));
886
- }
887
- tokenOk(req) {
888
- if (!this.opts.token) return true;
889
- const offered = String(req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim());
890
- return offered.includes(`bearer.${this.opts.token}`);
891
- }
892
- onConnection(socket, req) {
893
- if (!this.tokenOk(req)) {
894
- socket.close(4401, "bad token");
895
- return;
896
- }
897
- socket.on("message", (raw) => this.onMessageRaw(socket, raw.toString()));
898
- socket.on("close", () => this.onSocketClose(socket));
899
- socket.on("error", () => {
900
- });
901
- }
902
- onMessageRaw(socket, raw) {
903
- let parsed;
904
- try {
905
- parsed = JSON.parse(raw);
906
- } catch {
907
- return;
908
- }
909
- const r = ClientToServerSchema.safeParse(parsed);
910
- if (!r.success) return;
911
- const msg = r.data;
912
- if (msg.type === "HELLO") {
913
- const existingSocket = this.byWorker.get(msg.worker_id);
914
- if (existingSocket && existingSocket !== socket) {
915
- this.workerOf.delete(existingSocket);
916
- for (const [req_id, p] of [...this.pending]) {
917
- if (p.worker_id === msg.worker_id) {
918
- clearTimeout(p.timer);
919
- this.pending.delete(req_id);
920
- p.reject(new Error(`worker ${msg.worker_id} reconnected, exec cancelled`));
921
- }
922
- }
923
- existingSocket.terminate();
924
- }
925
- this.byWorker.set(msg.worker_id, socket);
926
- this.workerOf.set(socket, msg.worker_id);
927
- this.rawSend(socket, {
928
- type: "WELCOME",
929
- nonce: this.opts.idGen.next("nonce"),
930
- ts: this.opts.clock.now(),
931
- protocol_version: PROTOCOL_VERSION,
932
- server_time: this.opts.clock.now(),
933
- heartbeat_interval_ms: HEARTBEAT_INTERVAL_MS
934
- });
935
- this.startKeepalive(socket);
936
- for (const h of this.msgHandlers) h(msg.worker_id, msg);
937
- return;
938
- }
939
- if (msg.type === "RESULT") {
940
- const p = this.pending.get(msg.req_id);
941
- if (p) {
942
- clearTimeout(p.timer);
943
- this.pending.delete(msg.req_id);
944
- p.resolve(msg);
945
- }
946
- return;
947
- }
948
- if (msg.type === "PING") {
949
- this.rawSend(socket, {
950
- type: "PONG",
951
- nonce: this.opts.idGen.next("nonce"),
952
- ts: this.opts.clock.now(),
953
- protocol_version: PROTOCOL_VERSION,
954
- echo_nonce: msg.nonce
955
- });
956
- }
957
- const wid = this.workerOf.get(socket);
958
- if (wid) {
959
- for (const h of this.msgHandlers) h(wid, msg);
960
- }
961
- }
962
- onSocketClose(socket) {
963
- this.stopKeepalive(socket);
964
- const wid = this.workerOf.get(socket);
965
- if (!wid) return;
966
- this.workerOf.delete(socket);
967
- this.byWorker.delete(wid);
968
- for (const [req_id, p] of [...this.pending]) {
969
- if (p.worker_id === wid) {
970
- clearTimeout(p.timer);
971
- this.pending.delete(req_id);
972
- p.reject(new Error(`worker ${wid} disconnected`));
973
- }
974
- }
975
- for (const h of this.disconnectHandlers) h(wid);
976
- }
977
- startKeepalive(socket) {
978
- if (this.keepaliveIntervalMs <= 0) return;
979
- if (this.keepaliveTimers.has(socket)) return;
980
- const timer = setInterval(() => {
981
- if (socket.readyState !== WebSocket.OPEN) return;
982
- this.rawSend(socket, {
983
- type: "PONG",
984
- nonce: this.opts.idGen.next("nonce"),
985
- ts: this.opts.clock.now(),
986
- protocol_version: PROTOCOL_VERSION,
987
- echo_nonce: "server-keepalive"
988
- });
989
- }, this.keepaliveIntervalMs);
990
- if (typeof timer.unref === "function") {
991
- timer.unref();
992
- }
993
- this.keepaliveTimers.set(socket, timer);
994
- }
995
- stopKeepalive(socket) {
996
- const timer = this.keepaliveTimers.get(socket);
997
- if (!timer) return;
998
- clearInterval(timer);
999
- this.keepaliveTimers.delete(socket);
1000
- }
1001
- rawSend(socket, msg) {
1002
- const r = ServerToClientSchema.safeParse(msg);
1003
- if (!r.success) {
1004
- console.error("[hub] outgoing failed schema", r.error);
1005
- return;
1006
- }
1007
- if (socket.readyState === WebSocket.OPEN) {
1008
- socket.send(JSON.stringify(msg));
1009
- }
1010
- }
1011
- exec(worker_id, params) {
1012
- const socket = this.byWorker.get(worker_id);
1013
- if (!socket)
1014
- return Promise.reject(new Error(`worker ${worker_id} not connected`));
1015
- const req_id = this.opts.idGen.next("req");
1016
- return new Promise((resolve, reject) => {
1017
- const timer = setTimeout(() => {
1018
- this.pending.delete(req_id);
1019
- reject(
1020
- new Error(`EXEC ${req_id} timeout after ${this.execTimeoutMs}ms`)
1021
- );
1022
- }, this.execTimeoutMs);
1023
- this.pending.set(req_id, { resolve, reject, timer, worker_id });
1024
- this.rawSend(socket, {
1025
- type: "EXEC",
1026
- nonce: this.opts.idGen.next("nonce"),
1027
- ts: this.opts.clock.now(),
1028
- protocol_version: PROTOCOL_VERSION,
1029
- req_id,
1030
- session_id: params.session_id,
1031
- tab_id: params.tab_id,
1032
- step: { kind: "tool", tool: params.step.tool, args: params.step.args }
1033
- });
1034
- if (this.byWorker.get(worker_id) !== socket) {
1035
- clearTimeout(timer);
1036
- this.pending.delete(req_id);
1037
- reject(new Error(`worker ${worker_id} disconnected`));
1038
- }
1039
- });
1040
- }
1041
- async send(worker_id, msg) {
1042
- const socket = this.byWorker.get(worker_id);
1043
- if (!socket) throw new Error(`worker ${worker_id} not connected`);
1044
- this.rawSend(socket, msg);
1045
- }
1046
- onMessage(handler) {
1047
- this.msgHandlers.push(handler);
1048
- }
1049
- onDisconnect(handler) {
1050
- this.disconnectHandlers.push(handler);
1051
- }
1052
- connectedWorkers() {
1053
- return [...this.byWorker.keys()];
1054
- }
1055
- // Intentionally fire-and-forget: actual cleanup (byWorker/workerOf removal and
1056
- // pending-exec rejection) happens asynchronously via the socket's close event.
1057
- // v1 callers do not await this method.
1058
- async disconnect(worker_id, _reason) {
1059
- const socket = this.byWorker.get(worker_id);
1060
- if (socket) socket.close();
1061
- }
1062
- };
1063
-
1064
919
  // src/mcp-server.ts
1065
920
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1066
921
  import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -1253,6 +1108,8 @@ var TOOL_DEFS = [
1253
1108
  ms: { type: "integer" },
1254
1109
  selector: { type: "string" },
1255
1110
  timeoutMs: { type: "integer", default: 5e3 },
1111
+ text: { type: "string", description: "\u7B49\u5F85\u9875\u9762\u51FA\u73B0\u8BE5\u6587\u672C" },
1112
+ textGone: { type: "string", description: "\u7B49\u5F85\u8BE5\u6587\u672C\u6D88\u5931" },
1256
1113
  tabId: TAB_ID_FIELD
1257
1114
  }
1258
1115
  }
@@ -1265,6 +1122,12 @@ var TOOL_DEFS = [
1265
1122
  properties: {
1266
1123
  selector: { type: "string" },
1267
1124
  required: { type: "boolean", default: true },
1125
+ doubleClick: { type: "boolean", default: false },
1126
+ button: { type: "string", enum: ["left", "right", "middle"], default: "left" },
1127
+ modifiers: {
1128
+ type: "array",
1129
+ items: { type: "string", enum: ["Alt", "Control", "Meta", "Shift"] }
1130
+ },
1268
1131
  tabId: TAB_ID_FIELD
1269
1132
  },
1270
1133
  required: ["selector"]
@@ -1308,6 +1171,12 @@ var TOOL_DEFS = [
1308
1171
  selector: { type: "string" },
1309
1172
  value: { type: "string" },
1310
1173
  clear: { type: "boolean", default: true },
1174
+ slowly: {
1175
+ type: "boolean",
1176
+ default: false,
1177
+ description: "\u9010\u5B57\u7B26\u89E6\u53D1 keydown/keypress/input/keyup\uFF0C\u5BF9\u4ED8\u53D7\u63A7\u7EC4\u4EF6"
1178
+ },
1179
+ submit: { type: "boolean", default: false, description: "\u586B\u5B8C\u6309\u4E00\u6B21 Enter" },
1311
1180
  tabId: TAB_ID_FIELD
1312
1181
  },
1313
1182
  required: ["selector", "value"]
@@ -1499,6 +1368,9 @@ var TOOL_DEFS = [
1499
1368
  blockId: { type: "string", description: "\u53EF\u9009\uFF1Apage-index \u8FD4\u56DE\u7684 blockId\uFF0C\u7528\u4E8E\u5C40\u90E8\u89C6\u89C9\u8BC1\u636E" },
1500
1369
  indexId: { type: "string", description: "\u53EF\u9009\uFF1A\u4EA7\u751F blockId \u7684 indexId\uFF0C\u907F\u514D refresh \u540E\u8BEF\u8BFB" },
1501
1370
  highlightMs: { type: "integer", default: 1500, description: "\u622A\u56FE\u524D\u76EE\u6807\u9AD8\u4EAE\u6301\u7EED\u65F6\u95F4\uFF0C250-5000ms" },
1371
+ fullPage: { type: "boolean", default: false, description: "\u6EDA\u52A8\u5206\u6BB5\u622A\u56FE\u540E\u62FC\u63A5\u6574\u9875" },
1372
+ format: { type: "string", enum: ["png", "jpeg"], default: "png" },
1373
+ scale: { type: "number", default: 1, description: "0.1\u20131\uFF0C\u7F29\u5C0F\u53EF\u7701 token" },
1502
1374
  tabId: TAB_ID_FIELD
1503
1375
  }
1504
1376
  }
@@ -1757,89 +1629,318 @@ var TOOL_DEFS = [
1757
1629
  },
1758
1630
  required: ["store", "key", "value"]
1759
1631
  }
1760
- }
1761
- ];
1762
-
1763
- // src/tool-gen.ts
1764
- var EXEC_TOOL_NAMES = [
1765
- "snapshotDOM",
1766
- "querySelector",
1767
- "querySelectorAll",
1768
- "extractText",
1769
- "extractImages",
1770
- "getValue",
1771
- "extractFormState",
1772
- "hover",
1773
- "focus",
1774
- "scroll",
1775
- "waitFor",
1632
+ },
1633
+ // ── Plan 32 — playwright parity ──────────────────────────────────────────
1634
+ {
1635
+ name: "consoleMessages",
1636
+ description: "[OBSERVE] \u8BFB\u53D6\u672C\u9875 console \u65E5\u5FD7\u4E0E\u672A\u6355\u83B7\u9519\u8BEF\u3002\u8FD4\u56DE\u91CC\u5E26 backend \u5B57\u6BB5\uFF1Amain-world \u6863\u770B\u4E0D\u5230\u811A\u672C\u6CE8\u5165\u4E4B\u524D\u7684\u6D88\u606F\u548C\u6D4F\u89C8\u5668\u7EA7 CORS/CSP \u62A5\u9519\uFF0Ccdp \u6863\u80FD\u770B\u5230\u3002\n\u793A\u4F8B\uFF1A\u53EA\u770B\u62A5\u9519 { level: 'error', limit: 50 }\uFF1B\u589E\u91CF\u8F6E\u8BE2 { sinceId: \u4E0A\u6B21\u8FD4\u56DE\u7684\u6700\u5927 id }",
1637
+ input_schema: {
1638
+ type: "object",
1639
+ properties: {
1640
+ level: { type: "string", enum: ["log", "info", "warn", "error", "debug", "trace"] },
1641
+ limit: { type: "integer", default: 100 },
1642
+ sinceId: { type: "integer", description: "\u53EA\u8FD4\u56DE id \u5927\u4E8E\u6B64\u503C\u7684\u6D88\u606F\uFF0C\u7528\u4E8E\u589E\u91CF\u8BFB\u53D6" },
1643
+ tabId: TAB_ID_FIELD
1644
+ }
1645
+ }
1646
+ },
1647
+ {
1648
+ name: "networkRequests",
1649
+ description: "[OBSERVE] \u5217\u51FA\u672C\u9875\u53D1\u51FA\u7684\u7F51\u7EDC\u8BF7\u6C42\u6458\u8981\uFF08method / url / status / \u8017\u65F6\uFF09\u3002\u9ED8\u8BA4\u9690\u85CF PerformanceObserver \u89C2\u6D4B\u5230\u7684\u9759\u6001\u8D44\u6E90\uFF0CincludeStatic:true \u624D\u5E26\u4E0A\u3002\n\u8981\u770B headers \u6216 body \u7528 networkRequestDetail\u3002",
1650
+ input_schema: {
1651
+ type: "object",
1652
+ properties: {
1653
+ urlPattern: { type: "string", description: "\u5B50\u4E32\u5339\u914D\uFF1B\u7528 /re/ \u6216 /re/i \u5305\u8D77\u6765\u5219\u6309\u6B63\u5219" },
1654
+ method: { type: "string" },
1655
+ status: { type: "integer" },
1656
+ includeStatic: { type: "boolean", default: false },
1657
+ limit: { type: "integer", default: 50 },
1658
+ sinceId: { type: "integer" },
1659
+ tabId: TAB_ID_FIELD
1660
+ }
1661
+ }
1662
+ },
1663
+ {
1664
+ name: "networkRequestDetail",
1665
+ description: "[OBSERVE] \u8BFB\u53D6\u5355\u6761\u8BF7\u6C42\u7684 headers \u4E0E body\u3002**dangerous**\uFF1A\u54CD\u5E94\u5934\u91CC\u53EF\u80FD\u6709 Authorization / Set-Cookie / token\u3002\nmain-world \u6863\u9700\u5148 recorderConfig({bodies:true}) \u624D\u4F1A\u8BB0\u5F55 body\uFF0C\u4E14\u53EA\u8BB0 256KB \u4EE5\u5185\u7684\u6587\u672C\u7C7B\u54CD\u5E94\uFF1Bcdp \u6863\u76F4\u63A5\u53EF\u53D6\u3002",
1666
+ input_schema: {
1667
+ type: "object",
1668
+ properties: {
1669
+ id: { type: "integer", description: "networkRequests \u8FD4\u56DE\u7684\u6761\u76EE id" },
1670
+ part: {
1671
+ type: "string",
1672
+ enum: ["request-headers", "request-body", "response-headers", "response-body"],
1673
+ description: "\u53EA\u53D6\u5176\u4E2D\u4E00\u90E8\u5206\uFF1B\u7701\u7565\u5219\u5168\u7ED9"
1674
+ },
1675
+ tabId: TAB_ID_FIELD
1676
+ },
1677
+ required: ["id"]
1678
+ }
1679
+ },
1680
+ {
1681
+ name: "handleDialog",
1682
+ description: "[OBSERVE] \u8BBE\u5B9A alert / confirm / prompt \u7684\u5E94\u7B54\u7B56\u7565\uFF0C\u5E76\u8FD4\u56DE\u5DF2\u8BB0\u5F55\u7684\u5F39\u7A97\u3002\nmain-world \u6863\u4E0B\u5F39\u7A97\u662F\u540C\u6B65\u7684\uFF0C\u65E0\u6CD5\u6302\u8D77\u7B49\u4F60\u51B3\u5B9A\uFF0C\u6240\u4EE5\u8FD9\u91CC\u8BBE\u7684\u662F**\u9884\u5148\u7B56\u7565**\uFF1A\u8C03\u7528\u4E4B\u540E\u53D1\u751F\u7684\u5F39\u7A97\u6309\u6B64\u5904\u7406\u3002cdp \u6863\u4E0B\u5F39\u7A97\u771F\u6302\u8D77\uFF0C\u672C\u8C03\u7528\u4F1A\u7ACB\u5373\u5E94\u7B54\u5F53\u524D\u6302\u8D77\u7684\u5F39\u7A97\u3002\n\u672A\u8C03\u7528\u8FC7\u672C\u5DE5\u5177\u65F6\u5F39\u7A97\u4FDD\u6301\u539F\u751F\u884C\u4E3A\uFF08passthrough\uFF09\uFF0C\u9875\u9762\u8868\u73B0\u4E0E\u6CA1\u88C5\u6269\u5C55\u4E00\u81F4\u3002",
1683
+ input_schema: {
1684
+ type: "object",
1685
+ properties: {
1686
+ accept: { type: "boolean", description: "true=\u786E\u5B9A\uFF0Cfalse=\u53D6\u6D88" },
1687
+ promptText: { type: "string", description: "prompt \u5F39\u7A97\u586B\u5165\u7684\u6587\u672C" },
1688
+ scope: { type: "string", enum: ["next", "all"], default: "next" },
1689
+ tabId: TAB_ID_FIELD
1690
+ },
1691
+ required: ["accept"]
1692
+ }
1693
+ },
1694
+ {
1695
+ name: "recorderConfig",
1696
+ description: "[OBSERVE] \u5F00\u5173\u9875\u9762\u4E8B\u4EF6\u5F55\u5236\u3002bodies:true \u6253\u5F00\u8BF7\u6C42/\u54CD\u5E94 body \u6355\u83B7\uFF08\u9ED8\u8BA4\u5173\uFF0C\u6709\u5185\u5B58\u4EE3\u4EF7\uFF09\uFF1Bdialog:true \u8BA9\u5F39\u7A97\u8D70\u7B56\u7565\u800C\u4E0D\u662F\u539F\u751F\u884C\u4E3A\uFF1Bclear \u6E05\u7A7A\u7F13\u51B2\u3002\u7701\u7565\u7684\u5B57\u6BB5\u4FDD\u6301\u539F\u6837\u3002",
1697
+ input_schema: {
1698
+ type: "object",
1699
+ properties: {
1700
+ console: { type: "boolean" },
1701
+ network: { type: "boolean" },
1702
+ bodies: { type: "boolean" },
1703
+ dialog: { type: "boolean" },
1704
+ clear: {
1705
+ type: "array",
1706
+ items: { type: "string", enum: ["console", "network", "dialog"] }
1707
+ },
1708
+ tabId: TAB_ID_FIELD
1709
+ }
1710
+ }
1711
+ },
1712
+ {
1713
+ name: "navigateBack",
1714
+ description: "[FLOW] \u540E\u9000\u4E00\u9875\u3002\u5DF2\u5728\u5386\u53F2\u8D77\u70B9\u65F6\u8FD4\u56DE { ok:false, reason }\uFF0C\u4E0D\u629B\u9519\u3002",
1715
+ input_schema: { type: "object", properties: { tabId: TAB_ID_FIELD } }
1716
+ },
1717
+ {
1718
+ name: "navigateForward",
1719
+ description: "[FLOW] \u524D\u8FDB\u4E00\u9875\u3002\u5DF2\u5728\u5386\u53F2\u672B\u5C3E\u65F6\u8FD4\u56DE { ok:false, reason }\uFF0C\u4E0D\u629B\u9519\u3002",
1720
+ input_schema: { type: "object", properties: { tabId: TAB_ID_FIELD } }
1721
+ },
1722
+ {
1723
+ name: "resize",
1724
+ description: "[FLOW] \u628A\u89C6\u53E3\u8C03\u6574\u5230\u6307\u5B9A\u5C3A\u5BF8\u3002main-world \u6863\u91CF\u53D6 outerWidth-innerWidth \u53CD\u63A8\u6D4F\u89C8\u5668\u8FB9\u6846\u540E\u6539\u7A97\u53E3\u5916\u6846\uFF0C\u89C6\u53E3\u7CBE\u786E\u4F46**\u7528\u6237\u7684\u7A97\u53E3\u4F1A\u771F\u7684\u53D8\u5927\u5C0F**\uFF1Bcdp \u6863\u7528 Emulation \u8986\u76D6\u8BBE\u5907\u5C3A\u5BF8\uFF0C\u4E0D\u52A8\u771F\u5B9E\u7A97\u53E3\u3002",
1725
+ input_schema: {
1726
+ type: "object",
1727
+ properties: {
1728
+ width: { type: "integer" },
1729
+ height: { type: "integer" },
1730
+ tabId: TAB_ID_FIELD
1731
+ },
1732
+ required: ["width", "height"]
1733
+ }
1734
+ },
1735
+ {
1736
+ name: "drag",
1737
+ description: "[ACT] \u628A\u4E00\u4E2A\u5143\u7D20\u62D6\u5230\u53E6\u4E00\u4E2A\u5143\u7D20\u4E0A\u3002\u540C\u65F6\u53D1 pointer \u5E8F\u5217\u548C HTML5 DragEvent\uFF08\u5171\u7528\u4E00\u4E2A DataTransfer\uFF09\uFF0C\u517C\u5BB9\u539F\u751F\u62D6\u653E\u548C\u81EA\u5B9A\u4E49 pointer \u62D6\u653E\u3002\n\u8FD4\u56DE { consumed: { pointer, html5 } } \u8BF4\u660E\u76EE\u6807\u5B9E\u9645\u6D88\u8D39\u4E86\u54EA\u4E00\u7C7B\u4E8B\u4EF6\u2014\u2014\u90FD\u4E3A false \u8BF4\u660E\u8FD9\u4E2A\u62D6\u653E\u5B9E\u73B0\u4E24\u6761\u8DEF\u90FD\u4E0D\u5403\u3002",
1738
+ input_schema: {
1739
+ type: "object",
1740
+ properties: {
1741
+ fromSelector: { type: "string" },
1742
+ fromUid: { type: "string", description: "takeSnapshot / findElements \u8FD4\u56DE\u7684 uid" },
1743
+ toSelector: { type: "string" },
1744
+ toUid: { type: "string" },
1745
+ tabId: TAB_ID_FIELD
1746
+ }
1747
+ }
1748
+ },
1749
+ {
1750
+ name: "drop",
1751
+ description: "[ACT] \u6A21\u62DF\u4ECE\u6D4F\u89C8\u5668\u5916\u90E8\u628A\u6587\u4EF6\u6216\u6570\u636E\u62D6\u653E\u5230\u9875\u9762\u5143\u7D20\u4E0A\u3002\u5E26 files \u65F6\u7B49\u540C\u4E0A\u4F20\uFF08dangerous\uFF09\u3002\n\u793A\u4F8B\uFF1A{ selector:'#dropzone', files:[{ name:'a.csv', mimeType:'text/csv', base64:'...' }] }",
1752
+ input_schema: {
1753
+ type: "object",
1754
+ properties: {
1755
+ selector: { type: "string" },
1756
+ uid: { type: "string" },
1757
+ files: {
1758
+ type: "array",
1759
+ items: {
1760
+ type: "object",
1761
+ properties: {
1762
+ name: { type: "string" },
1763
+ mimeType: { type: "string" },
1764
+ base64: { type: "string" }
1765
+ },
1766
+ required: ["name", "base64"]
1767
+ }
1768
+ },
1769
+ data: { type: "object", description: "MIME \u2192 \u5B57\u7B26\u4E32\uFF0C\u4F8B\u5982 { 'text/plain':'hi' }" },
1770
+ tabId: TAB_ID_FIELD
1771
+ }
1772
+ }
1773
+ },
1774
+ {
1775
+ name: "findElements",
1776
+ description: "[READ] \u6309\u6587\u672C\u6216\u6B63\u5219\u5728\u53EF\u4EA4\u4E92\u5143\u7D20\u91CC\u627E\u76EE\u6807\uFF0C\u8FD4\u56DE uid / role / name / bounds\u3002\u4E0D\u9700\u8981\u5148 createPageIndex\u3002\n\u62FF\u5230 uid \u540E\u7528 clickByUid / fillByUid \u64CD\u4F5C\u6700\u7A33\u3002",
1777
+ input_schema: {
1778
+ type: "object",
1779
+ properties: {
1780
+ text: { type: "string", description: "\u5927\u5C0F\u5199\u4E0D\u654F\u611F\u5B50\u4E32" },
1781
+ regex: { type: "string", description: "\u4E0E text \u4E8C\u9009\u4E00" },
1782
+ limit: { type: "integer", default: 20 },
1783
+ tabId: TAB_ID_FIELD
1784
+ }
1785
+ }
1786
+ }
1787
+ ];
1788
+
1789
+ // src/tool-gen.ts
1790
+ var BLOCKED_TOOLS = /* @__PURE__ */ new Set(["askUser", "attachTab", "detachTab"]);
1791
+ var PARITY_TOOLS = [
1792
+ // page state and search
1793
+ "takeSnapshot",
1794
+ "findElements",
1795
+ "getPageInfo",
1796
+ // interaction
1797
+ "clickByUid",
1776
1798
  "click",
1777
1799
  "fillInput",
1778
- "setCheckbox",
1800
+ "fillForm",
1779
1801
  "selectOption",
1780
- "httpRequest",
1781
- "submitForm",
1802
+ "setCheckbox",
1803
+ "hover",
1804
+ "pressKey",
1805
+ "drag",
1806
+ "drop",
1782
1807
  "uploadFile",
1783
- "readStorage"
1808
+ // navigation and tabs
1809
+ "navigate",
1810
+ "navigateBack",
1811
+ "listTabs",
1812
+ "openTab",
1813
+ "resize",
1814
+ // observation
1815
+ "screenshot",
1816
+ "waitFor",
1817
+ "runJS",
1818
+ "consoleMessages",
1819
+ "networkRequests"
1784
1820
  ];
1821
+ var IMAGE_RESULT_TOOLS = /* @__PURE__ */ new Set(["screenshot"]);
1822
+ var JS_STEP_TOOLS = /* @__PURE__ */ new Set(["runJS"]);
1785
1823
  function rebuildSchema(src) {
1786
1824
  const s = src;
1787
1825
  const properties = { ...s.properties ?? {} };
1788
1826
  delete properties.tabId;
1789
- properties.session_id = { type: "string", description: "open_session \u8FD4\u56DE\u7684\u4F1A\u8BDD id\uFF08\u51B3\u5B9A\u76EE\u6807 worker \u4E0E tab\uFF09" };
1827
+ properties.session_id = {
1828
+ type: "string",
1829
+ description: "open_session \u8FD4\u56DE\u7684\u4F1A\u8BDD id\uFF08\u51B3\u5B9A\u76EE\u6807 worker \u4E0E tab\uFF09"
1830
+ };
1790
1831
  const required = [.../* @__PURE__ */ new Set([...(s.required ?? []).filter((r) => r !== "tabId"), "session_id"])];
1791
1832
  return { type: "object", properties, required };
1792
1833
  }
1793
- function generateBrowserTools() {
1794
- const allow = new Set(EXEC_TOOL_NAMES);
1795
- return TOOL_DEFS.filter((t) => allow.has(t.name)).map((t) => ({
1834
+ function generateBrowserTools(mode = "full") {
1835
+ const parity = new Set(PARITY_TOOLS);
1836
+ return TOOL_DEFS.filter((t) => {
1837
+ if (BLOCKED_TOOLS.has(t.name)) return false;
1838
+ return mode === "full" || parity.has(t.name);
1839
+ }).map((t) => ({
1796
1840
  name: `browser_${t.name}`,
1797
1841
  builtinTool: t.name,
1798
1842
  description: t.description,
1843
+ resultKind: IMAGE_RESULT_TOOLS.has(t.name) ? "image" : "json",
1844
+ stepKind: JS_STEP_TOOLS.has(t.name) ? "js" : "tool",
1799
1845
  inputSchema: rebuildSchema(t.input_schema)
1800
1846
  }));
1801
1847
  }
1848
+ function readToolMode(env) {
1849
+ const raw = env.ATWEBPILOT_MCP_TOOLS;
1850
+ if (raw == null || raw === "") return "full";
1851
+ if (raw === "full" || raw === "parity") return raw;
1852
+ process.stderr.write(
1853
+ `[atwebpilot-mcp] ATWEBPILOT_MCP_TOOLS="${raw}" is not recognised; using "full"
1854
+ `
1855
+ );
1856
+ return "full";
1857
+ }
1802
1858
 
1803
1859
  // src/handlers.ts
1804
- function singleWorkerId(c) {
1860
+ function singleWorkerId(c, pairUrl) {
1805
1861
  const workers = c.workers.list();
1806
- 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");
1862
+ if (workers.length === 0) {
1863
+ throw new Error(
1864
+ "\u6CA1\u6709\u6D4F\u89C8\u5668\u8FDE\u5165\u3002\u5DF2\u4E3A\u4F60\u6253\u5F00\u914D\u5BF9\u9875" + (pairUrl ? ` ${pairUrl}` : "") + "\uFF0C\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u91CD\u8BD5\u672C\u6B21\u8C03\u7528\u3002\uFF08\u5982\u679C\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E0D\u662F\u88C5\u4E86\u6269\u5C55\u7684\u90A3\u4E2A\uFF0C\u8BF7\u628A\u8BE5\u5730\u5740\u7C98\u8D34\u8FC7\u53BB\u6253\u5F00\uFF09"
1865
+ );
1866
+ }
1807
1867
  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");
1808
1868
  return workers[0].id;
1809
1869
  }
1810
- function handleListTabs(deps) {
1811
- const w = deps.coordinator.workers.get(singleWorkerId(deps.coordinator));
1870
+ async function handleListTabs(deps) {
1871
+ const { coordinator } = await deps.ensure();
1872
+ const w = coordinator.workers.get(singleWorkerId(coordinator, deps.pairUrl()));
1812
1873
  return { tabs: w.available_tabs };
1813
1874
  }
1814
- function handleOpenSession(deps, args) {
1815
- const worker_id = singleWorkerId(deps.coordinator);
1875
+ async function handleOpenSession(deps, args) {
1876
+ const { coordinator } = await deps.ensure();
1877
+ const worker_id = singleWorkerId(coordinator, deps.pairUrl());
1816
1878
  const tab_id = String(args.tab_id);
1817
1879
  const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
1818
1880
  const scope = new Set(requested.length ? requested : CAPABILITIES);
1819
1881
  const idle_timeout_ms = typeof args.idle_timeout_min === "number" ? args.idle_timeout_min * 6e4 : void 0;
1820
- const s = deps.coordinator.openSession({ ai_client_fingerprint: "mcp-local", worker_id, tab_id, scope, idle_timeout_ms });
1882
+ const s = coordinator.openSession({ ai_client_fingerprint: "mcp-local", worker_id, tab_id, scope, idle_timeout_ms });
1821
1883
  return { session_id: s.id };
1822
1884
  }
1823
- function handleCloseSession(deps, args) {
1824
- deps.coordinator.closeSession(String(args.session_id));
1885
+ async function handleCloseSession(deps, args) {
1886
+ const { coordinator } = await deps.ensure();
1887
+ coordinator.closeSession(String(args.session_id));
1825
1888
  return { ok: true };
1826
1889
  }
1827
- function handleGetQuota(deps, args) {
1828
- const q = deps.coordinator.quotaFor(String(args.session_id));
1890
+ async function handleGetQuota(deps, args) {
1891
+ const { coordinator } = await deps.ensure();
1892
+ const q = coordinator.quotaFor(String(args.session_id));
1829
1893
  if (!q) throw new Error(`session ${String(args.session_id)} not found`);
1830
1894
  return q;
1831
1895
  }
1896
+ var NON_BUILTIN_CAPABILITY = {
1897
+ listTabs: "tab:open",
1898
+ openTab: "tab:open"
1899
+ };
1832
1900
  async function handleBrowserTool(deps, gen, args) {
1901
+ const { coordinator, hub } = await deps.ensure();
1833
1902
  const session_id = String(args.session_id);
1834
- const session = deps.coordinator.sessions.get(session_id);
1903
+ const session = coordinator.sessions.get(session_id);
1835
1904
  if (!session) throw new Error(`session ${session_id} not found`);
1836
1905
  const { session_id: _omit, ...toolArgs } = args;
1837
1906
  const tool = gen.builtinTool;
1838
- const httpCookied = tool === "httpRequest" ? Boolean(toolArgs.withCredentials) : void 0;
1839
- const v = deps.coordinator.validateCall({ session_id, kind: "extension_tool", tool, httpCookied });
1840
- if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1841
- deps.coordinator.recordCall(session_id, v.dangerous);
1842
- const result = await deps.hub.exec(session.worker_id, { session_id, tab_id: session.tab_id, step: { kind: "tool", tool, args: toolArgs } });
1907
+ if (gen.stepKind === "js") {
1908
+ const source = String(toolArgs.source ?? "");
1909
+ if (!source) throw new Error("browser_runJS: source required");
1910
+ const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe: true });
1911
+ if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1912
+ coordinator.recordCall(session_id, v.dangerous);
1913
+ const jsResult = await hub.exec(session.worker_id, {
1914
+ session_id,
1915
+ tab_id: session.tab_id,
1916
+ step: { kind: "js", source }
1917
+ });
1918
+ if (!jsResult.ok) {
1919
+ throw new Error(
1920
+ jsResult.error ? `${jsResult.error.code}: ${jsResult.error.message}` : "EXEC failed"
1921
+ );
1922
+ }
1923
+ return jsResult.return ?? null;
1924
+ }
1925
+ const override = NON_BUILTIN_CAPABILITY[gen.builtinTool];
1926
+ if (override) {
1927
+ const v = coordinator.validateCall({ session_id, kind: "capability", capability: override });
1928
+ if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1929
+ coordinator.recordCall(session_id, v.dangerous);
1930
+ } else {
1931
+ const raw = toolArgs;
1932
+ const v = coordinator.validateCall({
1933
+ session_id,
1934
+ kind: "extension_tool",
1935
+ tool,
1936
+ httpCookied: tool === "httpRequest" ? Boolean(raw.withCredentials) : void 0,
1937
+ dropHasFiles: tool === "drop" ? Array.isArray(raw.files) && raw.files.length > 0 : void 0,
1938
+ recorderArmsBodies: tool === "recorderConfig" ? raw.bodies === true : void 0
1939
+ });
1940
+ if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
1941
+ coordinator.recordCall(session_id, v.dangerous);
1942
+ }
1943
+ const result = await hub.exec(session.worker_id, { session_id, tab_id: session.tab_id, step: { kind: "tool", tool, args: toolArgs } });
1843
1944
  if (!result.ok) throw new Error(result.error ? `${result.error.code}: ${result.error.message}` : "EXEC failed");
1844
1945
  return result.return ?? null;
1845
1946
  }
@@ -1886,16 +1987,57 @@ var SKILL_TOOL = {
1886
1987
  };
1887
1988
 
1888
1989
  // src/mcp-server.ts
1889
- var BROWSER_TOOLS = generateBrowserTools();
1990
+ var TOOL_MODE = readToolMode(process.env);
1991
+ var BROWSER_TOOLS = generateBrowserTools(TOOL_MODE);
1890
1992
  var BROWSER_BY_NAME = new Map(BROWSER_TOOLS.map((t) => [t.name, t]));
1891
- function buildToolList() {
1993
+ var LEGACY_TOOLS = [
1994
+ "snapshotDOM",
1995
+ "querySelector",
1996
+ "querySelectorAll",
1997
+ "extractText",
1998
+ "extractImages",
1999
+ "getValue",
2000
+ "extractFormState",
2001
+ "hover",
2002
+ "focus",
2003
+ "scroll",
2004
+ "waitFor",
2005
+ "click",
2006
+ "fillInput",
2007
+ "setCheckbox",
2008
+ "selectOption",
2009
+ "httpRequest",
2010
+ "submitForm",
2011
+ "uploadFile",
2012
+ "readStorage"
2013
+ ];
2014
+ function workerToolSupport(deps) {
2015
+ const bundle = deps?.peek();
2016
+ if (!bundle) return void 0;
2017
+ const workers = bundle.coordinator.workers.list();
2018
+ if (workers.length === 0) return void 0;
2019
+ const supported = workers[0].supported_tools;
2020
+ return supported ?? new Set(LEGACY_TOOLS);
2021
+ }
2022
+ function buildToolList(deps) {
2023
+ const supported = workerToolSupport(deps);
2024
+ const browser = supported ? BROWSER_TOOLS.filter((t) => supported.has(t.builtinTool)) : BROWSER_TOOLS;
1892
2025
  return [
1893
2026
  { name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
1894
2027
  ...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
1895
- ...BROWSER_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
2028
+ ...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
1896
2029
  ];
1897
2030
  }
1898
- var ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data ?? null) }] });
2031
+ var ok = (data) => ({
2032
+ content: [{ type: "text", text: JSON.stringify(data ?? null) }]
2033
+ });
2034
+ function toolResult(gen, data) {
2035
+ if (gen.resultKind !== "image") return ok(data);
2036
+ const d = data ?? {};
2037
+ if (typeof d.data !== "string") return ok(data);
2038
+ const mimeType = typeof d.media_type === "string" ? d.media_type : "image/png";
2039
+ return { content: [{ type: "image", data: d.data, mimeType }] };
2040
+ }
1899
2041
  var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
1900
2042
  async function dispatchCall(deps, name, args) {
1901
2043
  try {
@@ -1903,12 +2045,12 @@ async function dispatchCall(deps, name, args) {
1903
2045
  const bundle = readSkillBundle();
1904
2046
  return { content: [{ type: "text", text: bundle.content }] };
1905
2047
  }
1906
- if (name === "list_tabs") return ok(handleListTabs(deps));
1907
- if (name === "open_session") return ok(handleOpenSession(deps, args));
1908
- if (name === "close_session") return ok(handleCloseSession(deps, args));
1909
- if (name === "get_quota") return ok(handleGetQuota(deps, args));
2048
+ if (name === "list_tabs") return ok(await handleListTabs(deps));
2049
+ if (name === "open_session") return ok(await handleOpenSession(deps, args));
2050
+ if (name === "close_session") return ok(await handleCloseSession(deps, args));
2051
+ if (name === "get_quota") return ok(await handleGetQuota(deps, args));
1910
2052
  const gen = BROWSER_BY_NAME.get(name);
1911
- if (gen) return ok(await handleBrowserTool(deps, gen, args));
2053
+ if (gen) return toolResult(gen, await handleBrowserTool(deps, gen, args));
1912
2054
  return fail2(`unknown tool: ${name}`);
1913
2055
  } catch (e) {
1914
2056
  return fail2(e instanceof Error ? e.message : String(e));
@@ -1916,7 +2058,7 @@ async function dispatchCall(deps, name, args) {
1916
2058
  }
1917
2059
  function createMcpServer(deps) {
1918
2060
  const server = new Server({ name: "atwebpilot-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
1919
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList() }));
2061
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps) }));
1920
2062
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
1921
2063
  const args = req.params.arguments ?? {};
1922
2064
  return dispatchCall(deps, req.params.name, args);
@@ -1924,12 +2066,325 @@ function createMcpServer(deps) {
1924
2066
  return server;
1925
2067
  }
1926
2068
 
2069
+ // src/loopback-ws-hub.ts
2070
+ import { WebSocketServer, WebSocket } from "ws";
2071
+ import { createServer } from "http";
2072
+
2073
+ // ../shared/src/pairing/types.ts
2074
+ var GRACEFUL_CLOSE_CODE = 4e3;
2075
+ var GRACEFUL_CLOSE_REASON = "server-shutting-down";
2076
+
2077
+ // src/pair-page.ts
2078
+ function renderPairPage(payload) {
2079
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c");
2080
+ return `<!doctype html>
2081
+ <meta charset="utf-8">
2082
+ <title>AtWebPilot \u914D\u5BF9</title>
2083
+ <body style="font:14px/1.6 system-ui,sans-serif;padding:2rem;max-width:32rem;margin:auto;color:#222">
2084
+ <h1 style="font-size:1.1rem;margin:0 0 1rem">AtWebPilot \u914D\u5BF9</h1>
2085
+ <p id="atwebpilot-status">\u6B63\u5728\u8054\u7CFB\u6269\u5C55\u2026</p>
2086
+ <p style="color:#666;font-size:13px">\u4F1A\u8BDD\uFF1A${escapeHtml(payload.label)} \xB7 pid ${payload.pid} \xB7 \u7AEF\u53E3 ${payload.port}</p>
2087
+ <p style="color:#666;font-size:13px">\u6CA1\u6709\u53CD\u5E94\uFF1F\u8BF7\u786E\u8BA4\u8FD9\u4E2A\u6D4F\u89C8\u5668\u5DF2\u5B89\u88C5\u5E76\u542F\u7528 AtWebPilot \u6269\u5C55\uFF0C\u7136\u540E\u5237\u65B0\u672C\u9875\u3002</p>
2088
+ <script>
2089
+ (function () {
2090
+ var payload = ${json};
2091
+ window.postMessage({ source: "atwebpilot-pair", payload: payload }, "*");
2092
+ window.addEventListener("message", function (e) {
2093
+ if (!e.data || e.data.source !== "atwebpilot-pair-result") return;
2094
+ var el = document.getElementById("atwebpilot-status");
2095
+ if (e.data.ok) {
2096
+ el.textContent = e.data.trusted ? "\u5DF2\u4FE1\u4EFB\uFF0C\u8FDE\u63A5\u4E2D\u2026" : "\u5DF2\u8FDE\u63A5";
2097
+ setTimeout(function () { window.close(); }, 1200);
2098
+ } else {
2099
+ el.textContent = "\u5DF2\u62D2\u7EDD\u3002\u53EF\u4EE5\u5173\u95ED\u672C\u9875\u3002";
2100
+ }
2101
+ });
2102
+ })();
2103
+ </script>
2104
+ `;
2105
+ }
2106
+ function escapeHtml(s) {
2107
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2108
+ }
2109
+
2110
+ // src/loopback-ws-hub.ts
2111
+ var HEARTBEAT_INTERVAL_MS = 2e4;
2112
+ var DEFAULT_KEEPALIVE_INTERVAL_MS = 15e3;
2113
+ var LoopbackWSHub = class {
2114
+ constructor(opts) {
2115
+ this.opts = opts;
2116
+ this.execTimeoutMs = opts.execTimeoutMs ?? 3e4;
2117
+ this.keepaliveIntervalMs = opts.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
2118
+ this.http = createServer((req, res) => this.onHttpRequest(req, res));
2119
+ this.readyPromise = new Promise((resolve, reject) => {
2120
+ this.http.once("listening", () => resolve(this.http.address().port));
2121
+ this.http.once("error", (err) => {
2122
+ reject(
2123
+ err.code === "EADDRINUSE" ? new Error(
2124
+ `port ${opts.port} is already in use \u2014 another atwebpilot-mcp may be running; set ATWEBPILOT_WS_PORT to choose another port`
2125
+ ) : err
2126
+ );
2127
+ });
2128
+ });
2129
+ this.wss = new WebSocketServer({
2130
+ server: this.http,
2131
+ path: "/worker",
2132
+ handleProtocols: (protocols) => [...protocols][0] ?? false
2133
+ });
2134
+ this.wss.on("connection", (socket, req) => this.onConnection(socket, req));
2135
+ this.http.on("error", () => void 0);
2136
+ this.wss.on("error", () => void 0);
2137
+ this.http.listen(opts.port);
2138
+ }
2139
+ opts;
2140
+ http;
2141
+ wss;
2142
+ /** Settled by the constructor so ready() cannot race the listen outcome. */
2143
+ readyPromise;
2144
+ byWorker = /* @__PURE__ */ new Map();
2145
+ workerOf = /* @__PURE__ */ new Map();
2146
+ pending = /* @__PURE__ */ new Map();
2147
+ msgHandlers = [];
2148
+ disconnectHandlers = [];
2149
+ execTimeoutMs;
2150
+ keepaliveIntervalMs;
2151
+ keepaliveTimers = /* @__PURE__ */ new Map();
2152
+ ready() {
2153
+ return this.readyPromise;
2154
+ }
2155
+ /** Serves the pairing page; everything else is a 404. */
2156
+ onHttpRequest(req, res) {
2157
+ const path = (req.url ?? "").split("?")[0];
2158
+ if (path !== "/pair") {
2159
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2160
+ res.end("not found");
2161
+ return;
2162
+ }
2163
+ const payload = this.opts.pairPayload?.();
2164
+ if (!payload) {
2165
+ res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
2166
+ res.end("pairing unavailable");
2167
+ return;
2168
+ }
2169
+ res.writeHead(200, {
2170
+ "content-type": "text/html; charset=utf-8",
2171
+ "cache-control": "no-store"
2172
+ });
2173
+ res.end(renderPairPage(payload));
2174
+ }
2175
+ /**
2176
+ * Tells connected extensions this shutdown is deliberate, so they drop the
2177
+ * endpoint instead of retrying it for the rest of the browser session.
2178
+ */
2179
+ async shutdown() {
2180
+ for (const socket of this.wss.clients) {
2181
+ try {
2182
+ socket.close(GRACEFUL_CLOSE_CODE, GRACEFUL_CLOSE_REASON);
2183
+ } catch {
2184
+ }
2185
+ }
2186
+ await new Promise((r) => setTimeout(r, 50));
2187
+ await this.close();
2188
+ }
2189
+ close() {
2190
+ for (const p of this.pending.values()) {
2191
+ clearTimeout(p.timer);
2192
+ p.reject(new Error("hub closing"));
2193
+ }
2194
+ this.pending.clear();
2195
+ for (const timer of this.keepaliveTimers.values()) clearInterval(timer);
2196
+ this.keepaliveTimers.clear();
2197
+ for (const socket of this.wss.clients) {
2198
+ socket.terminate();
2199
+ }
2200
+ return new Promise(
2201
+ (resolve) => this.wss.close(() => this.http.close(() => resolve()))
2202
+ );
2203
+ }
2204
+ tokenOk(req) {
2205
+ if (!this.opts.token) return true;
2206
+ const offered = String(req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim());
2207
+ return offered.includes(`bearer.${this.opts.token}`);
2208
+ }
2209
+ onConnection(socket, req) {
2210
+ if (!this.tokenOk(req)) {
2211
+ socket.close(4401, "bad token");
2212
+ return;
2213
+ }
2214
+ socket.on("message", (raw) => this.onMessageRaw(socket, raw.toString()));
2215
+ socket.on("close", () => this.onSocketClose(socket));
2216
+ socket.on("error", () => {
2217
+ });
2218
+ }
2219
+ onMessageRaw(socket, raw) {
2220
+ let parsed;
2221
+ try {
2222
+ parsed = JSON.parse(raw);
2223
+ } catch {
2224
+ return;
2225
+ }
2226
+ const r = ClientToServerSchema.safeParse(parsed);
2227
+ if (!r.success) return;
2228
+ const msg = r.data;
2229
+ if (msg.type === "HELLO") {
2230
+ const existingSocket = this.byWorker.get(msg.worker_id);
2231
+ if (existingSocket && existingSocket !== socket) {
2232
+ this.workerOf.delete(existingSocket);
2233
+ for (const [req_id, p] of [...this.pending]) {
2234
+ if (p.worker_id === msg.worker_id) {
2235
+ clearTimeout(p.timer);
2236
+ this.pending.delete(req_id);
2237
+ p.reject(new Error(`worker ${msg.worker_id} reconnected, exec cancelled`));
2238
+ }
2239
+ }
2240
+ existingSocket.terminate();
2241
+ }
2242
+ this.byWorker.set(msg.worker_id, socket);
2243
+ this.workerOf.set(socket, msg.worker_id);
2244
+ this.rawSend(socket, {
2245
+ type: "WELCOME",
2246
+ nonce: this.opts.idGen.next("nonce"),
2247
+ ts: this.opts.clock.now(),
2248
+ protocol_version: PROTOCOL_VERSION,
2249
+ server_time: this.opts.clock.now(),
2250
+ heartbeat_interval_ms: HEARTBEAT_INTERVAL_MS
2251
+ });
2252
+ this.startKeepalive(socket);
2253
+ for (const h of this.msgHandlers) h(msg.worker_id, msg);
2254
+ return;
2255
+ }
2256
+ if (msg.type === "RESULT") {
2257
+ const p = this.pending.get(msg.req_id);
2258
+ if (p) {
2259
+ clearTimeout(p.timer);
2260
+ this.pending.delete(msg.req_id);
2261
+ p.resolve(msg);
2262
+ }
2263
+ return;
2264
+ }
2265
+ if (msg.type === "PING") {
2266
+ this.rawSend(socket, {
2267
+ type: "PONG",
2268
+ nonce: this.opts.idGen.next("nonce"),
2269
+ ts: this.opts.clock.now(),
2270
+ protocol_version: PROTOCOL_VERSION,
2271
+ echo_nonce: msg.nonce
2272
+ });
2273
+ }
2274
+ const wid = this.workerOf.get(socket);
2275
+ if (wid) {
2276
+ for (const h of this.msgHandlers) h(wid, msg);
2277
+ }
2278
+ }
2279
+ onSocketClose(socket) {
2280
+ this.stopKeepalive(socket);
2281
+ const wid = this.workerOf.get(socket);
2282
+ if (!wid) return;
2283
+ this.workerOf.delete(socket);
2284
+ this.byWorker.delete(wid);
2285
+ for (const [req_id, p] of [...this.pending]) {
2286
+ if (p.worker_id === wid) {
2287
+ clearTimeout(p.timer);
2288
+ this.pending.delete(req_id);
2289
+ p.reject(new Error(`worker ${wid} disconnected`));
2290
+ }
2291
+ }
2292
+ for (const h of this.disconnectHandlers) h(wid);
2293
+ }
2294
+ startKeepalive(socket) {
2295
+ if (this.keepaliveIntervalMs <= 0) return;
2296
+ if (this.keepaliveTimers.has(socket)) return;
2297
+ const timer = setInterval(() => {
2298
+ if (socket.readyState !== WebSocket.OPEN) return;
2299
+ this.rawSend(socket, {
2300
+ type: "PONG",
2301
+ nonce: this.opts.idGen.next("nonce"),
2302
+ ts: this.opts.clock.now(),
2303
+ protocol_version: PROTOCOL_VERSION,
2304
+ echo_nonce: "server-keepalive"
2305
+ });
2306
+ }, this.keepaliveIntervalMs);
2307
+ if (typeof timer.unref === "function") {
2308
+ timer.unref();
2309
+ }
2310
+ this.keepaliveTimers.set(socket, timer);
2311
+ }
2312
+ stopKeepalive(socket) {
2313
+ const timer = this.keepaliveTimers.get(socket);
2314
+ if (!timer) return;
2315
+ clearInterval(timer);
2316
+ this.keepaliveTimers.delete(socket);
2317
+ }
2318
+ rawSend(socket, msg) {
2319
+ const r = ServerToClientSchema.safeParse(msg);
2320
+ if (!r.success) {
2321
+ console.error("[hub] outgoing failed schema", r.error);
2322
+ return;
2323
+ }
2324
+ if (socket.readyState === WebSocket.OPEN) {
2325
+ socket.send(JSON.stringify(msg));
2326
+ }
2327
+ }
2328
+ exec(worker_id, params) {
2329
+ const socket = this.byWorker.get(worker_id);
2330
+ if (!socket)
2331
+ return Promise.reject(new Error(`worker ${worker_id} not connected`));
2332
+ const req_id = this.opts.idGen.next("req");
2333
+ return new Promise((resolve, reject) => {
2334
+ const timer = setTimeout(() => {
2335
+ this.pending.delete(req_id);
2336
+ reject(
2337
+ new Error(`EXEC ${req_id} timeout after ${this.execTimeoutMs}ms`)
2338
+ );
2339
+ }, this.execTimeoutMs);
2340
+ this.pending.set(req_id, { resolve, reject, timer, worker_id });
2341
+ this.rawSend(socket, {
2342
+ type: "EXEC",
2343
+ nonce: this.opts.idGen.next("nonce"),
2344
+ ts: this.opts.clock.now(),
2345
+ protocol_version: PROTOCOL_VERSION,
2346
+ req_id,
2347
+ session_id: params.session_id,
2348
+ tab_id: params.tab_id,
2349
+ step: { kind: "tool", tool: params.step.tool, args: params.step.args }
2350
+ });
2351
+ if (this.byWorker.get(worker_id) !== socket) {
2352
+ clearTimeout(timer);
2353
+ this.pending.delete(req_id);
2354
+ reject(new Error(`worker ${worker_id} disconnected`));
2355
+ }
2356
+ });
2357
+ }
2358
+ async send(worker_id, msg) {
2359
+ const socket = this.byWorker.get(worker_id);
2360
+ if (!socket) throw new Error(`worker ${worker_id} not connected`);
2361
+ this.rawSend(socket, msg);
2362
+ }
2363
+ onMessage(handler) {
2364
+ this.msgHandlers.push(handler);
2365
+ }
2366
+ onDisconnect(handler) {
2367
+ this.disconnectHandlers.push(handler);
2368
+ }
2369
+ connectedWorkers() {
2370
+ return [...this.byWorker.keys()];
2371
+ }
2372
+ // Intentionally fire-and-forget: actual cleanup (byWorker/workerOf removal and
2373
+ // pending-exec rejection) happens asynchronously via the socket's close event.
2374
+ // v1 callers do not await this method.
2375
+ async disconnect(worker_id, _reason) {
2376
+ const socket = this.byWorker.get(worker_id);
2377
+ if (socket) socket.close();
2378
+ }
2379
+ };
2380
+
1927
2381
  // src/wire.ts
1928
2382
  function helloToWorker(h, now) {
1929
2383
  return {
1930
2384
  id: h.worker_id,
1931
2385
  fingerprint: h.fingerprint,
1932
2386
  capabilities: new Set(h.capabilities.filter(isCapability)),
2387
+ supported_tools: h.supported_tools ? new Set(h.supported_tools) : void 0,
1933
2388
  attended: h.attended,
1934
2389
  labels: new Set(h.labels),
1935
2390
  available_tabs: h.available_tabs,
@@ -1949,6 +2404,11 @@ function installWire(hub, coordinator, clock) {
1949
2404
  case "PING":
1950
2405
  coordinator.heartbeatWorker(worker_id);
1951
2406
  break;
2407
+ case "TABS_UPDATE": {
2408
+ const w = coordinator.workers.get(worker_id);
2409
+ if (w) w.available_tabs = msg.tabs.map((t) => ({ ...t }));
2410
+ break;
2411
+ }
1952
2412
  default:
1953
2413
  break;
1954
2414
  }
@@ -1956,19 +2416,150 @@ function installWire(hub, coordinator, clock) {
1956
2416
  hub.onDisconnect((worker_id) => coordinator.unregisterWorker(worker_id));
1957
2417
  }
1958
2418
 
2419
+ // src/identity.ts
2420
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
2421
+ import { homedir } from "os";
2422
+ import { join as join2 } from "path";
2423
+ import { randomBytes, randomUUID } from "crypto";
2424
+ var FILE = "identity.json";
2425
+ function defaultIdentityDir() {
2426
+ return join2(homedir(), ".atwebpilot");
2427
+ }
2428
+ function read(dir) {
2429
+ try {
2430
+ const raw = readFileSync2(join2(dir, FILE), "utf-8");
2431
+ const parsed = JSON.parse(raw);
2432
+ if (typeof parsed.installId !== "string" || typeof parsed.secret !== "string") return null;
2433
+ return parsed;
2434
+ } catch {
2435
+ return null;
2436
+ }
2437
+ }
2438
+ function write(dir, data) {
2439
+ mkdirSync(dir, { recursive: true, mode: 448 });
2440
+ writeFileSync(join2(dir, FILE), `${JSON.stringify(data, null, 2)}
2441
+ `, { mode: 384 });
2442
+ }
2443
+ function loadOrCreateIdentity(dir = defaultIdentityDir()) {
2444
+ const existing = read(dir);
2445
+ if (existing) return { installId: existing.installId, secret: existing.secret };
2446
+ const created = {
2447
+ installId: `inst_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
2448
+ secret: randomBytes(24).toString("base64url")
2449
+ };
2450
+ write(dir, created);
2451
+ return { installId: created.installId, secret: created.secret };
2452
+ }
2453
+ function loadLastPort(dir = defaultIdentityDir()) {
2454
+ const existing = read(dir);
2455
+ return typeof existing?.lastPort === "number" ? existing.lastPort : void 0;
2456
+ }
2457
+ function saveLastPort(port, dir = defaultIdentityDir()) {
2458
+ const existing = read(dir);
2459
+ const base = existing ?? { ...loadOrCreateIdentity(dir) };
2460
+ write(dir, { ...base, lastPort: port });
2461
+ }
2462
+ function processInfo(cwd = process.cwd()) {
2463
+ const home = homedir();
2464
+ const label = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
2465
+ return {
2466
+ sessionId: `sess_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
2467
+ label,
2468
+ pid: process.pid
2469
+ };
2470
+ }
2471
+
2472
+ // src/ensure-hub.ts
2473
+ function createHubEnsurer(d) {
2474
+ let bundle = null;
2475
+ let inflight = null;
2476
+ let pairUrlValue = null;
2477
+ let opened = false;
2478
+ const info = d.processInfo ?? processInfo();
2479
+ async function bind() {
2480
+ const identity = loadOrCreateIdentity(d.identityDir);
2481
+ const preferred = d.explicitPort ?? loadLastPort(d.identityDir) ?? 0;
2482
+ let hub;
2483
+ let port;
2484
+ const pairPayload = () => ({
2485
+ v: 1,
2486
+ installId: identity.installId,
2487
+ secret: identity.secret,
2488
+ sessionId: info.sessionId,
2489
+ label: info.label,
2490
+ pid: info.pid,
2491
+ port
2492
+ });
2493
+ try {
2494
+ hub = new LoopbackWSHub({ ...common(), port: preferred, pairPayload });
2495
+ port = await hub.ready();
2496
+ } catch (e) {
2497
+ if (d.explicitPort != null) throw e;
2498
+ hub = new LoopbackWSHub({ ...common(), port: 0, pairPayload });
2499
+ port = await hub.ready();
2500
+ }
2501
+ saveLastPort(port, d.identityDir);
2502
+ const coordinator = new Coordinator({ hub, clock: d.clock, idGen: d.idGen });
2503
+ installWire(hub, coordinator, d.clock);
2504
+ pairUrlValue = `http://127.0.0.1:${port}/pair`;
2505
+ return { coordinator, hub, port };
2506
+ function common() {
2507
+ return { token: d.token, clock: d.clock, idGen: d.idGen };
2508
+ }
2509
+ }
2510
+ async function ensure() {
2511
+ if (bundle) return bundle;
2512
+ inflight ??= bind().then((b2) => {
2513
+ bundle = b2;
2514
+ return b2;
2515
+ });
2516
+ const b = await inflight;
2517
+ if (!opened) {
2518
+ opened = true;
2519
+ if (pairUrlValue) d.openUrl?.(pairUrlValue);
2520
+ }
2521
+ return b;
2522
+ }
2523
+ return {
2524
+ ensure,
2525
+ peek: () => bundle,
2526
+ pairUrl: () => pairUrlValue,
2527
+ bound: () => bundle != null
2528
+ };
2529
+ }
2530
+ function defaultOpenUrl(url) {
2531
+ void import("child_process").then(({ spawn }) => {
2532
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2533
+ try {
2534
+ spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
2535
+ } catch {
2536
+ }
2537
+ });
2538
+ }
2539
+
1959
2540
  // src/index.ts
1960
2541
  async function main() {
1961
- const port = Number(process.env.ATWEBPILOT_WS_PORT ?? 8787);
2542
+ const explicitPort = process.env.ATWEBPILOT_WS_PORT ? Number(process.env.ATWEBPILOT_WS_PORT) : void 0;
1962
2543
  const token = process.env.ATWEBPILOT_WS_TOKEN || void 0;
1963
2544
  const clock = new DefaultClock();
1964
2545
  const idGen = new DefaultIdGen();
1965
- const hub = new LoopbackWSHub({ port, token, clock, idGen });
1966
- await hub.ready();
1967
- const coordinator = new Coordinator({ hub, clock, idGen });
1968
- installWire(hub, coordinator, clock);
1969
- const server = createMcpServer({ coordinator, hub });
2546
+ const deps = createHubEnsurer({
2547
+ clock,
2548
+ idGen,
2549
+ explicitPort,
2550
+ token,
2551
+ openUrl: defaultOpenUrl
2552
+ });
2553
+ const server = createMcpServer(deps);
1970
2554
  await server.connect(new StdioServerTransport());
1971
- console.error(`[atwebpilot-mcp] ws://127.0.0.1:${port}/worker ready; stdio MCP connected`);
2555
+ console.error("[atwebpilot-mcp] stdio MCP connected; ws port binds on first browser use");
2556
+ const shutdown = () => {
2557
+ const bundle = deps.peek();
2558
+ void (bundle?.hub.shutdown?.() ?? Promise.resolve()).finally(() => process.exit(0));
2559
+ };
2560
+ process.on("SIGTERM", shutdown);
2561
+ process.on("SIGINT", shutdown);
2562
+ process.stdin.on("close", shutdown);
1972
2563
  }
1973
2564
  main().catch((e) => {
1974
2565
  console.error("[atwebpilot-mcp] fatal", e);