@rainhole/rain-code 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -0
- package/dist/chunk-06b2745w.js +3583 -0
- package/dist/chunk-06saje2v.js +39 -0
- package/dist/chunk-07069jq1.js +30 -0
- package/dist/chunk-0731m51q.js +8 -0
- package/dist/chunk-0bs2528v.js +192 -0
- package/dist/chunk-0fj1d707.js +313 -0
- package/dist/chunk-0gdkvtx4.js +117 -0
- package/dist/chunk-0jd8rpt4.js +34257 -0
- package/dist/chunk-0paqc2yw.js +15 -0
- package/dist/chunk-0pk0w4j1.js +332 -0
- package/dist/chunk-0rpb2bdp.js +8 -0
- package/dist/chunk-0rvwew67.js +9300 -0
- package/dist/chunk-0sa1g8jf.js +318 -0
- package/dist/chunk-0vkfrmqm.js +690 -0
- package/dist/chunk-0xgxxqhr.js +708 -0
- package/dist/chunk-0xjaqda8.js +1124 -0
- package/dist/chunk-0zep39v0.js +81 -0
- package/dist/chunk-1141xmr4.js +8 -0
- package/dist/chunk-12hn2mhy.js +17 -0
- package/dist/chunk-12rjry89.js +81 -0
- package/dist/chunk-168xb8v1.js +436 -0
- package/dist/chunk-1cwdhk7a.js +969 -0
- package/dist/chunk-1cx0fd76.js +132 -0
- package/dist/chunk-1erfksyp.js +107 -0
- package/dist/chunk-1h1tc4mw.js +126 -0
- package/dist/chunk-1h2famwb.js +49 -0
- package/dist/chunk-1jnbys9f.js +1811 -0
- package/dist/chunk-1k4fp6ft.js +389 -0
- package/dist/chunk-1mjn8xxc.js +22 -0
- package/dist/chunk-1mn6r4y2.js +747 -0
- package/dist/chunk-1p8myfza.js +1568 -0
- package/dist/chunk-1vthytca.js +79 -0
- package/dist/chunk-1y32rx73.js +145 -0
- package/dist/chunk-200781fd.js +64 -0
- package/dist/chunk-205mmm67.js +100 -0
- package/dist/chunk-209eh6ak.js +9381 -0
- package/dist/chunk-21mfpnva.js +85 -0
- package/dist/chunk-238g70xa.js +36 -0
- package/dist/chunk-24k6t9fa.js +151 -0
- package/dist/chunk-29gg6hx0.js +2992 -0
- package/dist/chunk-2a42s11t.js +412 -0
- package/dist/chunk-2ekjxh7d.js +289 -0
- package/dist/chunk-2eykm0j0.js +20 -0
- package/dist/chunk-2fwze72g.js +24 -0
- package/dist/chunk-2gzv8nrw.js +205 -0
- package/dist/chunk-2k995y2x.js +880 -0
- package/dist/chunk-2nayx6q1.js +63 -0
- package/dist/chunk-2pg9pmvc.js +182 -0
- package/dist/chunk-2qp2hc02.js +36 -0
- package/dist/chunk-2rj0dnps.js +436 -0
- package/dist/chunk-2t0xa4dt.js +145 -0
- package/dist/chunk-2ym6tnww.js +216 -0
- package/dist/chunk-30bpz5m2.js +34 -0
- package/dist/chunk-30rst83v.js +168 -0
- package/dist/chunk-36b2q5fg.js +1299 -0
- package/dist/chunk-3c25bcsw.js +17 -0
- package/dist/chunk-3ewzhjyb.js +207 -0
- package/dist/chunk-3h8a89gy.js +46 -0
- package/dist/chunk-3nk79af8.js +8 -0
- package/dist/chunk-3sfr7j85.js +884 -0
- package/dist/chunk-3w6s9m5w.js +16 -0
- package/dist/chunk-3x52v1wb.js +660 -0
- package/dist/chunk-404qm8xt.js +1715 -0
- package/dist/chunk-42fjay4q.js +141 -0
- package/dist/chunk-48649h96.js +8590 -0
- package/dist/chunk-495d85x1.js +8066 -0
- package/dist/chunk-4anj5saq.js +111 -0
- package/dist/chunk-4asynmj4.js +259 -0
- package/dist/chunk-4ck9wve9.js +225 -0
- package/dist/chunk-4cp6193g.js +9076 -0
- package/dist/chunk-4g3v8y12.js +23 -0
- package/dist/chunk-4geyher8.js +244 -0
- package/dist/chunk-4hv90qcz.js +8 -0
- package/dist/chunk-4jhr03e7.js +61 -0
- package/dist/chunk-4jm600zv.js +13 -0
- package/dist/chunk-4nspekjp.js +69 -0
- package/dist/chunk-4ptqcw4p.js +16 -0
- package/dist/chunk-4qj8cqbh.js +173 -0
- package/dist/chunk-4wxsg6a0.js +122 -0
- package/dist/chunk-4zfkzkt6.js +37 -0
- package/dist/chunk-54dckh8a.js +1169 -0
- package/dist/chunk-54y13759.js +650 -0
- package/dist/chunk-54yf6e8d.js +91 -0
- package/dist/chunk-57qz90z8.js +154 -0
- package/dist/chunk-59k0mjnc.js +120 -0
- package/dist/chunk-59p9jye4.js +104 -0
- package/dist/chunk-5c753bd4.js +64 -0
- package/dist/chunk-5cw6zwep.js +106 -0
- package/dist/chunk-5eaf3j24.js +133 -0
- package/dist/chunk-5me7398b.js +81 -0
- package/dist/chunk-5pevjsyw.js +118 -0
- package/dist/chunk-5q3c2rf4.js +107 -0
- package/dist/chunk-5xwtnq4g.js +146 -0
- package/dist/chunk-5z28bqne.js +6748 -0
- package/dist/chunk-613f6fbv.js +63 -0
- package/dist/chunk-641hawvm.js +95 -0
- package/dist/chunk-64c1avct.js +13 -0
- package/dist/chunk-64hks9ax.js +456 -0
- package/dist/chunk-677as3nh.js +15 -0
- package/dist/chunk-6bd8brc4.js +48 -0
- package/dist/chunk-6cs6mmez.js +112 -0
- package/dist/chunk-6dj5t602.js +341 -0
- package/dist/chunk-6e8vht4y.js +780 -0
- package/dist/chunk-6gr3c3w9.js +378 -0
- package/dist/chunk-6hkd32eh.js +83 -0
- package/dist/chunk-6jqxvef0.js +198 -0
- package/dist/chunk-6mh3vfa4.js +115 -0
- package/dist/chunk-6mpw9h55.js +1071 -0
- package/dist/chunk-6p4cyhb3.js +16 -0
- package/dist/chunk-6qvgnepd.js +26 -0
- package/dist/chunk-6r2bhsvt.js +6956 -0
- package/dist/chunk-6tq2v3rk.js +186 -0
- package/dist/chunk-7316m01k.js +125 -0
- package/dist/chunk-73rpbt04.js +1947 -0
- package/dist/chunk-75zk7zr1.js +309 -0
- package/dist/chunk-760252na.js +101 -0
- package/dist/chunk-764ec4ja.js +173 -0
- package/dist/chunk-76vgnv24.js +108 -0
- package/dist/chunk-7739pg2c.js +4261 -0
- package/dist/chunk-7a7hxy3d.js +167 -0
- package/dist/chunk-7aw745vx.js +40175 -0
- package/dist/chunk-7b6bg1r4.js +16128 -0
- package/dist/chunk-7ca4qskp.js +99 -0
- package/dist/chunk-7deph26x.js +311 -0
- package/dist/chunk-7ds8zqar.js +108 -0
- package/dist/chunk-7fqxw3d2.js +479 -0
- package/dist/chunk-7nysf9yd.js +266 -0
- package/dist/chunk-7tascxe2.js +90 -0
- package/dist/chunk-7wm5s02e.js +216 -0
- package/dist/chunk-7xmw2tcz.js +277 -0
- package/dist/chunk-7xyswgjh.js +8 -0
- package/dist/chunk-7zkhadkb.js +220 -0
- package/dist/chunk-8225arm6.js +6374 -0
- package/dist/chunk-83hfzbx3.js +10 -0
- package/dist/chunk-85tag8nv.js +1391 -0
- package/dist/chunk-8bd8q5xw.js +359 -0
- package/dist/chunk-8bwqtasa.js +213 -0
- package/dist/chunk-8g5pe1gr.js +37 -0
- package/dist/chunk-8g747a8x.js +26 -0
- package/dist/chunk-8h6sdj66.js +447 -0
- package/dist/chunk-8mm8b6dd.js +19 -0
- package/dist/chunk-8ndwn6st.js +120 -0
- package/dist/chunk-8pn8tvgg.js +637 -0
- package/dist/chunk-8tnn9kcb.js +131 -0
- package/dist/chunk-8tnsngw2.js +31 -0
- package/dist/chunk-8x5w267y.js +112 -0
- package/dist/chunk-8y12jxg8.js +10 -0
- package/dist/chunk-8ymf4e6z.js +48 -0
- package/dist/chunk-90mt0m1w.js +102 -0
- package/dist/chunk-90wp6wez.js +10510 -0
- package/dist/chunk-91ew1d6f.js +2689 -0
- package/dist/chunk-97jsc1kn.js +231 -0
- package/dist/chunk-993pnm45.js +4521 -0
- package/dist/chunk-9a0rsdre.js +46 -0
- package/dist/chunk-9d7fmqkw.js +41 -0
- package/dist/chunk-9dgaq0g4.js +667 -0
- package/dist/chunk-9f52phb8.js +138 -0
- package/dist/chunk-9fbpqghc.js +105 -0
- package/dist/chunk-9hzxd5q3.js +122 -0
- package/dist/chunk-9kyrssch.js +28 -0
- package/dist/chunk-9q93n4w6.js +308 -0
- package/dist/chunk-9s0xs06y.js +581 -0
- package/dist/chunk-9x5zgy22.js +12 -0
- package/dist/chunk-9zdcb9jy.js +105 -0
- package/dist/chunk-a136cvvz.js +27 -0
- package/dist/chunk-a4psxnr1.js +224 -0
- package/dist/chunk-a4twdmhf.js +45 -0
- package/dist/chunk-a7rhvq9b.js +106 -0
- package/dist/chunk-a8ejc632.js +3094 -0
- package/dist/chunk-a9vdeb6y.js +258 -0
- package/dist/chunk-a9zh40sj.js +48 -0
- package/dist/chunk-aawg48hh.js +4102 -0
- package/dist/chunk-ad2f3890.js +65 -0
- package/dist/chunk-ae7ar1jk.js +105 -0
- package/dist/chunk-ak3hdbe4.js +234 -0
- package/dist/chunk-as4e9g8f.js +298 -0
- package/dist/chunk-ase86y73.js +15 -0
- package/dist/chunk-awb4vc41.js +69 -0
- package/dist/chunk-b4bep0aq.js +5041 -0
- package/dist/chunk-b4wg70y1.js +54 -0
- package/dist/chunk-b81hd3m6.js +32 -0
- package/dist/chunk-b9knmzt0.js +106 -0
- package/dist/chunk-besmz4ym.js +32 -0
- package/dist/chunk-bgpkemkg.js +65 -0
- package/dist/chunk-bj8x29tz.js +54 -0
- package/dist/chunk-bkqsnwrq.js +239 -0
- package/dist/chunk-bnf662he.js +359 -0
- package/dist/chunk-bq00fcsd.js +165 -0
- package/dist/chunk-brz2c4cq.js +275 -0
- package/dist/chunk-bsa118r4.js +502 -0
- package/dist/chunk-bsengrbt.js +2737 -0
- package/dist/chunk-bt64sq5y.js +19 -0
- package/dist/chunk-bxcfz5gy.js +56 -0
- package/dist/chunk-bxs2a71m.js +213 -0
- package/dist/chunk-byv2p9hn.js +397 -0
- package/dist/chunk-c03am3es.js +29 -0
- package/dist/chunk-c2ahb7yg.js +105 -0
- package/dist/chunk-c2qm0t6p.js +111 -0
- package/dist/chunk-c42h9gzx.js +71 -0
- package/dist/chunk-c47xbxqw.js +38 -0
- package/dist/chunk-c5vkve4j.js +68 -0
- package/dist/chunk-c6sjhj89.js +1284 -0
- package/dist/chunk-ca147npg.js +27 -0
- package/dist/chunk-caxx597g.js +71 -0
- package/dist/chunk-cbrt5vsb.js +31 -0
- package/dist/chunk-ce0z23ct.js +443 -0
- package/dist/chunk-cefsp8h1.js +155 -0
- package/dist/chunk-cfv996bs.js +22 -0
- package/dist/chunk-cgfdkzhb.js +12 -0
- package/dist/chunk-chsyvavm.js +3435 -0
- package/dist/chunk-cmsknj6n.js +28228 -0
- package/dist/chunk-cn8bkmvj.js +264 -0
- package/dist/chunk-crmjpsqe.js +68 -0
- package/dist/chunk-cthpw4pj.js +849 -0
- package/dist/chunk-cwy9nj4y.js +134 -0
- package/dist/chunk-cyagce56.js +155 -0
- package/dist/chunk-cynewytp.js +227 -0
- package/dist/chunk-cznf6k4g.js +46 -0
- package/dist/chunk-d0954t9j.js +267 -0
- package/dist/chunk-d1qhftze.js +118 -0
- package/dist/chunk-d4mdda98.js +94 -0
- package/dist/chunk-d4rnqevg.js +41 -0
- package/dist/chunk-d7886r6a.js +13870 -0
- package/dist/chunk-dd1qvamk.js +678 -0
- package/dist/chunk-dg4je8qj.js +183 -0
- package/dist/chunk-dggvswz1.js +104 -0
- package/dist/chunk-dgqrcy74.js +48 -0
- package/dist/chunk-dm3n2qgd.js +24 -0
- package/dist/chunk-dnh7jtpb.js +37 -0
- package/dist/chunk-dsenyfax.js +105 -0
- package/dist/chunk-dtf0a7rr.js +655 -0
- package/dist/chunk-dxdb7bs1.js +60 -0
- package/dist/chunk-e0pth8g7.js +79 -0
- package/dist/chunk-e36y0q82.js +109 -0
- package/dist/chunk-ef55cwrv.js +267 -0
- package/dist/chunk-efh644hv.js +915 -0
- package/dist/chunk-ehtwnxpg.js +1591 -0
- package/dist/chunk-ejahyhd8.js +120 -0
- package/dist/chunk-ejk1bpzz.js +472 -0
- package/dist/chunk-emef71ea.js +202 -0
- package/dist/chunk-en7v028t.js +333 -0
- package/dist/chunk-eqp1rfft.js +17 -0
- package/dist/chunk-ewadzrm8.js +103 -0
- package/dist/chunk-exh7z6tv.js +132 -0
- package/dist/chunk-f0npbgyf.js +340 -0
- package/dist/chunk-f2qkc8ce.js +229 -0
- package/dist/chunk-f49sb3q7.js +40 -0
- package/dist/chunk-f5ma3nh5.js +3436 -0
- package/dist/chunk-fbv4apne.js +51 -0
- package/dist/chunk-fday7sfg.js +269 -0
- package/dist/chunk-feyzykye.js +10 -0
- package/dist/chunk-fgqwr70t.js +241 -0
- package/dist/chunk-fhy7zs1w.js +943 -0
- package/dist/chunk-fmw3f68k.js +432 -0
- package/dist/chunk-fmwqvpqm.js +506 -0
- package/dist/chunk-fn1apgre.js +66 -0
- package/dist/chunk-fpy4nam9.js +21933 -0
- package/dist/chunk-fqy0erkw.js +908 -0
- package/dist/chunk-ftqwza3a.js +43 -0
- package/dist/chunk-fyc5fepv.js +10 -0
- package/dist/chunk-fz0ch109.js +329 -0
- package/dist/chunk-fzhn8z8j.js +253 -0
- package/dist/chunk-g12brv6y.js +321 -0
- package/dist/chunk-g338npwr.js +1061 -0
- package/dist/chunk-g5tpks28.js +795 -0
- package/dist/chunk-g63tgj4f.js +58 -0
- package/dist/chunk-g827r0mk.js +266 -0
- package/dist/chunk-g8wzsxgc.js +2534 -0
- package/dist/chunk-gey7mwcw.js +36 -0
- package/dist/chunk-gghb9pks.js +120 -0
- package/dist/chunk-grdpaf1p.js +606 -0
- package/dist/chunk-gx8016vp.js +125 -0
- package/dist/chunk-h0qngp9w.js +157 -0
- package/dist/chunk-h0rbjg6x.js +55 -0
- package/dist/chunk-h1mr3371.js +121 -0
- package/dist/chunk-h3xwdgpd.js +68 -0
- package/dist/chunk-h4b85amj.js +2331 -0
- package/dist/chunk-h6d137aa.js +20 -0
- package/dist/chunk-h9n7z00d.js +95 -0
- package/dist/chunk-hakdhagh.js +16606 -0
- package/dist/chunk-hd8531ec.js +685 -0
- package/dist/chunk-heq3rn8t.js +33068 -0
- package/dist/chunk-hfvxc9fd.js +542 -0
- package/dist/chunk-hgpd0qmm.js +371 -0
- package/dist/chunk-hh7cmy4k.js +20 -0
- package/dist/chunk-hhjs87wh.js +1407 -0
- package/dist/chunk-hjtvdvgj.js +208 -0
- package/dist/chunk-hk9xz7gk.js +118 -0
- package/dist/chunk-hknbjky3.js +160798 -0
- package/dist/chunk-htsbqyf9.js +338 -0
- package/dist/chunk-htx7jmxk.js +78 -0
- package/dist/chunk-hzhe8ygc.js +547 -0
- package/dist/chunk-j2k4p94p.js +55 -0
- package/dist/chunk-j3a4p81y.js +184 -0
- package/dist/chunk-j46rb9m9.js +544 -0
- package/dist/chunk-j5bth84e.js +97 -0
- package/dist/chunk-j64ga6ta.js +8035 -0
- package/dist/chunk-j6jh72hw.js +140 -0
- package/dist/chunk-j9gxwbe3.js +349 -0
- package/dist/chunk-jaaxk89e.js +113 -0
- package/dist/chunk-jd32zbps.js +15 -0
- package/dist/chunk-jdgeec04.js +4249 -0
- package/dist/chunk-jk826edv.js +174 -0
- package/dist/chunk-jnhkref0.js +30 -0
- package/dist/chunk-jsx72hep.js +208 -0
- package/dist/chunk-jx817w05.js +11 -0
- package/dist/chunk-jzmz18nn.js +65 -0
- package/dist/chunk-k0p9w03v.js +4957 -0
- package/dist/chunk-k3s4yk22.js +477 -0
- package/dist/chunk-kfd89dsd.js +21 -0
- package/dist/chunk-kgfz4522.js +126 -0
- package/dist/chunk-kn6zg6bd.js +524 -0
- package/dist/chunk-kp9vkzj6.js +120 -0
- package/dist/chunk-kraf793v.js +118 -0
- package/dist/chunk-kw0r30h3.js +208 -0
- package/dist/chunk-kwekc97v.js +18 -0
- package/dist/chunk-m2bcpk8j.js +30 -0
- package/dist/chunk-m2kp5f7p.js +4389 -0
- package/dist/chunk-m74w3187.js +164 -0
- package/dist/chunk-m88q4jmb.js +61 -0
- package/dist/chunk-man6wb4n.js +1456 -0
- package/dist/chunk-mdk3xgya.js +42 -0
- package/dist/chunk-mf4g439x.js +82 -0
- package/dist/chunk-mg8g0xqs.js +71 -0
- package/dist/chunk-mkwzrqvf.js +38 -0
- package/dist/chunk-mn61mk4v.js +351 -0
- package/dist/chunk-ms09sxcj.js +908 -0
- package/dist/chunk-mtn1anwe.js +2351 -0
- package/dist/chunk-mv0zm0qj.js +65 -0
- package/dist/chunk-mx28h61f.js +1147 -0
- package/dist/chunk-mxbf8ajn.js +1067 -0
- package/dist/chunk-n0qaeaa5.js +256 -0
- package/dist/chunk-n34z5cw8.js +74 -0
- package/dist/chunk-n55ehz77.js +120 -0
- package/dist/chunk-n7ttdtk0.js +641 -0
- package/dist/chunk-n9ktjngj.js +336 -0
- package/dist/chunk-nb2ntesh.js +149 -0
- package/dist/chunk-netzwgv1.js +154 -0
- package/dist/chunk-nh3cd07f.js +14358 -0
- package/dist/chunk-nka1g8f4.js +773 -0
- package/dist/chunk-nm97cw83.js +458 -0
- package/dist/chunk-nq0fxyxh.js +752 -0
- package/dist/chunk-nt837qt9.js +21 -0
- package/dist/chunk-nx87c0vw.js +250 -0
- package/dist/chunk-nxw6y6xm.js +862 -0
- package/dist/chunk-nzt717xg.js +32 -0
- package/dist/chunk-p2816w9z.js +1486 -0
- package/dist/chunk-p2d5nh3g.js +342 -0
- package/dist/chunk-p9cdykwf.js +72 -0
- package/dist/chunk-paxw1ryd.js +227 -0
- package/dist/chunk-ph5g4d1j.js +109 -0
- package/dist/chunk-pp42p8y3.js +574 -0
- package/dist/chunk-ppdn71n4.js +400 -0
- package/dist/chunk-ppwjyveh.js +8 -0
- package/dist/chunk-ps49ymvj.js +43 -0
- package/dist/chunk-pshjyzq4.js +17656 -0
- package/dist/chunk-pw8nf9rt.js +126 -0
- package/dist/chunk-pwwa7s62.js +11 -0
- package/dist/chunk-py3zxq5j.js +129 -0
- package/dist/chunk-pyz1qswz.js +300 -0
- package/dist/chunk-q50q8mc5.js +157 -0
- package/dist/chunk-q7tpyeb3.js +39 -0
- package/dist/chunk-q82r31er.js +151 -0
- package/dist/chunk-qajrkk97.js +298 -0
- package/dist/chunk-qcpxnyeh.js +110 -0
- package/dist/chunk-qeabchwr.js +132 -0
- package/dist/chunk-qm7nbd10.js +755 -0
- package/dist/chunk-qm8j7kxr.js +280 -0
- package/dist/chunk-qmwgg5zy.js +136 -0
- package/dist/chunk-qnfx3qtx.js +617 -0
- package/dist/chunk-qp2qdcda.js +100 -0
- package/dist/chunk-qphaajd7.js +305 -0
- package/dist/chunk-qt21xxgm.js +419 -0
- package/dist/chunk-qy2z2p0b.js +122 -0
- package/dist/chunk-qyz8q0dj.js +689 -0
- package/dist/chunk-qz2x630m.js +49145 -0
- package/dist/chunk-qzq5n1yn.js +76 -0
- package/dist/chunk-r0ya57xw.js +170 -0
- package/dist/chunk-r7j395t6.js +122 -0
- package/dist/chunk-r7trcrs7.js +62 -0
- package/dist/chunk-re4yh70t.js +1095 -0
- package/dist/chunk-rhte1r8g.js +272 -0
- package/dist/chunk-rhw4ayb1.js +6851 -0
- package/dist/chunk-rn0v1hk8.js +34 -0
- package/dist/chunk-rphakhme.js +281 -0
- package/dist/chunk-rrtzz2pv.js +32 -0
- package/dist/chunk-rtnjk8ge.js +726 -0
- package/dist/chunk-rwhswkma.js +87 -0
- package/dist/chunk-rxd4kn2g.js +308 -0
- package/dist/chunk-s083x8ry.js +2840 -0
- package/dist/chunk-s16sn02n.js +365 -0
- package/dist/chunk-s1s8qfdh.js +182 -0
- package/dist/chunk-s2bwz69v.js +473 -0
- package/dist/chunk-s3pzvdss.js +50 -0
- package/dist/chunk-s9y6t0mt.js +291 -0
- package/dist/chunk-sd36yzx8.js +954 -0
- package/dist/chunk-se8xzw8h.js +87 -0
- package/dist/chunk-sg66v252.js +1648 -0
- package/dist/chunk-sg7gf3hj.js +1585 -0
- package/dist/chunk-spx24x6j.js +98 -0
- package/dist/chunk-sybxpy18.js +73 -0
- package/dist/chunk-szj5wvdy.js +28 -0
- package/dist/chunk-t0gb304x.js +182 -0
- package/dist/chunk-t3fr4skc.js +165 -0
- package/dist/chunk-t58wfamm.js +5368 -0
- package/dist/chunk-t6s2kkm3.js +104 -0
- package/dist/chunk-t8hv0x81.js +116 -0
- package/dist/chunk-tezak8rx.js +6157 -0
- package/dist/chunk-th5y3hj5.js +189 -0
- package/dist/chunk-tj0d3870.js +180 -0
- package/dist/chunk-tjgsawpx.js +40 -0
- package/dist/chunk-ts2p6bv1.js +224 -0
- package/dist/chunk-ttk5dzz8.js +25 -0
- package/dist/chunk-tv9pcdnz.js +51 -0
- package/dist/chunk-ty99rgvw.js +122 -0
- package/dist/chunk-tzgp8av2.js +61 -0
- package/dist/chunk-v1kzp02e.js +785 -0
- package/dist/chunk-v2c9dq7t.js +275 -0
- package/dist/chunk-v3aq3heg.js +620 -0
- package/dist/chunk-v78fj8by.js +145 -0
- package/dist/chunk-v9smspw2.js +4301 -0
- package/dist/chunk-var1et7e.js +66 -0
- package/dist/chunk-vchrkvet.js +120 -0
- package/dist/chunk-vest0y6x.js +3830 -0
- package/dist/chunk-vf5sd1nq.js +12 -0
- package/dist/chunk-vf612n57.js +472 -0
- package/dist/chunk-vgpy9md6.js +339 -0
- package/dist/chunk-vkzt82ry.js +644 -0
- package/dist/chunk-vsh80y0c.js +100 -0
- package/dist/chunk-vxkdxhyk.js +103 -0
- package/dist/chunk-vxt6ywm9.js +485 -0
- package/dist/chunk-vyjeh50y.js +2149 -0
- package/dist/chunk-vytdzvqs.js +1181 -0
- package/dist/chunk-w0qks2ja.js +1030 -0
- package/dist/chunk-wfz0qffj.js +3939 -0
- package/dist/chunk-wp1568nt.js +828 -0
- package/dist/chunk-wpckhx7b.js +66 -0
- package/dist/chunk-wpvkvwvc.js +41 -0
- package/dist/chunk-ws5d6qs4.js +655 -0
- package/dist/chunk-wsnxrrb5.js +986 -0
- package/dist/chunk-wyavftcj.js +103 -0
- package/dist/chunk-wybh0bvv.js +121 -0
- package/dist/chunk-wzpdet3m.js +843 -0
- package/dist/chunk-x2dp18yj.js +74 -0
- package/dist/chunk-x6r4v44b.js +8 -0
- package/dist/chunk-x7r5k0hf.js +423 -0
- package/dist/chunk-xahk20z8.js +877 -0
- package/dist/chunk-xajwdaxm.js +295 -0
- package/dist/chunk-xkt36p6r.js +61 -0
- package/dist/chunk-xnav6j8h.js +490 -0
- package/dist/chunk-xrj294jg.js +440 -0
- package/dist/chunk-xsq9ae7x.js +90 -0
- package/dist/chunk-xt9z9t7b.js +40 -0
- package/dist/chunk-xv3era3s.js +8 -0
- package/dist/chunk-y1784krc.js +19 -0
- package/dist/chunk-y289fz8y.js +116 -0
- package/dist/chunk-y3r7v9pq.js +336 -0
- package/dist/chunk-y98z50mm.js +158 -0
- package/dist/chunk-yhce3x0q.js +64 -0
- package/dist/chunk-yhhf6n9p.js +338 -0
- package/dist/chunk-ykr34msh.js +411 -0
- package/dist/chunk-yks9ggzn.js +103 -0
- package/dist/chunk-ym5r3jnk.js +2007 -0
- package/dist/chunk-ypa349qq.js +148 -0
- package/dist/chunk-yzx3mhfd.js +110 -0
- package/dist/chunk-z1bs6d7k.js +24 -0
- package/dist/chunk-z2dp53wn.js +17 -0
- package/dist/chunk-z3k0k30d.js +882 -0
- package/dist/chunk-z42cqdtf.js +107 -0
- package/dist/chunk-z7asrz6j.js +259 -0
- package/dist/chunk-za09fhhh.js +716 -0
- package/dist/chunk-zae0t3p4.js +252 -0
- package/dist/chunk-zbsw794g.js +259 -0
- package/dist/chunk-ze6zvkg6.js +22820 -0
- package/dist/chunk-zejm280k.js +39 -0
- package/dist/chunk-zk2wsm7d.js +15 -0
- package/dist/chunk-znf2b75k.js +113 -0
- package/dist/chunk-zqpnvsdz.js +24 -0
- package/dist/chunk-zs2gdkj5.js +433 -0
- package/dist/chunk-zseb1639.js +37 -0
- package/dist/chunk-zsgha506.js +63 -0
- package/dist/chunk-zvsfgmq8.js +146 -0
- package/dist/chunk-zw5xd7m9.js +160 -0
- package/dist/chunk-zzm33q8x.js +8 -0
- package/dist/cli.js +97 -0
- package/dist/download-ripgrep.js +24821 -0
- package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
- package/package.json +178 -0
- package/scripts/download-ripgrep.ts +335 -0
- package/scripts/postinstall.cjs +319 -0
|
@@ -0,0 +1,2007 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
Server,
|
|
6
|
+
init_server,
|
|
7
|
+
init_types
|
|
8
|
+
} from "./chunk-4cp6193g.js";
|
|
9
|
+
import {
|
|
10
|
+
__esm
|
|
11
|
+
} from "./chunk-qp2qdcda.js";
|
|
12
|
+
|
|
13
|
+
// packages/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts
|
|
14
|
+
import { promises as fsPromises } from "fs";
|
|
15
|
+
import { createConnection } from "net";
|
|
16
|
+
import { platform } from "os";
|
|
17
|
+
import { dirname } from "path";
|
|
18
|
+
function isToolResponse(message) {
|
|
19
|
+
return "result" in message || "error" in message;
|
|
20
|
+
}
|
|
21
|
+
function isNotification(message) {
|
|
22
|
+
return "method" in message && typeof message.method === "string";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class McpSocketClient {
|
|
26
|
+
socket = null;
|
|
27
|
+
connected = false;
|
|
28
|
+
connecting = false;
|
|
29
|
+
responseCallback = null;
|
|
30
|
+
notificationHandler = null;
|
|
31
|
+
responseBuffer = Buffer.alloc(0);
|
|
32
|
+
reconnectAttempts = 0;
|
|
33
|
+
maxReconnectAttempts = 10;
|
|
34
|
+
reconnectDelay = 1000;
|
|
35
|
+
reconnectTimer = null;
|
|
36
|
+
context;
|
|
37
|
+
disableAutoReconnect = false;
|
|
38
|
+
constructor(context) {
|
|
39
|
+
this.context = context;
|
|
40
|
+
}
|
|
41
|
+
async connect() {
|
|
42
|
+
const { serverName, logger } = this.context;
|
|
43
|
+
if (this.connecting) {
|
|
44
|
+
logger.info(`[${serverName}] Already connecting, skipping duplicate attempt`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.closeSocket();
|
|
48
|
+
this.connecting = true;
|
|
49
|
+
const socketPath = this.context.getSocketPath?.() ?? this.context.socketPath;
|
|
50
|
+
logger.info(`[${serverName}] Attempting to connect to: ${socketPath}`);
|
|
51
|
+
try {
|
|
52
|
+
await this.validateSocketSecurity(socketPath);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.connecting = false;
|
|
55
|
+
logger.info(`[${serverName}] Security validation failed:`, error);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.socket = createConnection(socketPath);
|
|
59
|
+
const connectTimeout = setTimeout(() => {
|
|
60
|
+
if (!this.connected) {
|
|
61
|
+
logger.info(`[${serverName}] Connection attempt timed out after 5000ms`);
|
|
62
|
+
this.closeSocket();
|
|
63
|
+
this.scheduleReconnect();
|
|
64
|
+
}
|
|
65
|
+
}, 5000);
|
|
66
|
+
this.socket.on("connect", () => {
|
|
67
|
+
clearTimeout(connectTimeout);
|
|
68
|
+
this.connected = true;
|
|
69
|
+
this.connecting = false;
|
|
70
|
+
this.reconnectAttempts = 0;
|
|
71
|
+
logger.info(`[${serverName}] Successfully connected to bridge server`);
|
|
72
|
+
});
|
|
73
|
+
this.socket.on("data", (data) => {
|
|
74
|
+
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
|
75
|
+
while (this.responseBuffer.length >= 4) {
|
|
76
|
+
const length = this.responseBuffer.readUInt32LE(0);
|
|
77
|
+
if (this.responseBuffer.length < 4 + length) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const messageBytes = this.responseBuffer.slice(4, 4 + length);
|
|
81
|
+
this.responseBuffer = this.responseBuffer.slice(4 + length);
|
|
82
|
+
try {
|
|
83
|
+
const message = JSON.parse(messageBytes.toString("utf-8"));
|
|
84
|
+
if (isNotification(message)) {
|
|
85
|
+
logger.info(`[${serverName}] Received notification: ${message.method}`);
|
|
86
|
+
if (this.notificationHandler) {
|
|
87
|
+
this.notificationHandler(message);
|
|
88
|
+
}
|
|
89
|
+
} else if (isToolResponse(message)) {
|
|
90
|
+
logger.info(`[${serverName}] Received tool response: ${message}`);
|
|
91
|
+
this.handleResponse(message);
|
|
92
|
+
} else {
|
|
93
|
+
logger.info(`[${serverName}] Received unknown message: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.info(`[${serverName}] Failed to parse message:`, error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.socket.on("error", (error) => {
|
|
101
|
+
clearTimeout(connectTimeout);
|
|
102
|
+
logger.info(`[${serverName}] Socket error (code: ${error.code}):`, error);
|
|
103
|
+
this.connected = false;
|
|
104
|
+
this.connecting = false;
|
|
105
|
+
if (error.code && [
|
|
106
|
+
"ECONNREFUSED",
|
|
107
|
+
"ECONNRESET",
|
|
108
|
+
"EPIPE",
|
|
109
|
+
"ENOENT",
|
|
110
|
+
"EOPNOTSUPP",
|
|
111
|
+
"ECONNABORTED"
|
|
112
|
+
].includes(error.code)) {
|
|
113
|
+
this.scheduleReconnect();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
this.socket.on("close", () => {
|
|
117
|
+
clearTimeout(connectTimeout);
|
|
118
|
+
this.connected = false;
|
|
119
|
+
this.connecting = false;
|
|
120
|
+
this.scheduleReconnect();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
scheduleReconnect() {
|
|
124
|
+
const { serverName, logger } = this.context;
|
|
125
|
+
if (this.disableAutoReconnect) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (this.reconnectTimer) {
|
|
129
|
+
logger.info(`[${serverName}] Reconnect already scheduled, skipping`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.reconnectAttempts++;
|
|
133
|
+
const maxTotalAttempts = 100;
|
|
134
|
+
if (this.reconnectAttempts > maxTotalAttempts) {
|
|
135
|
+
logger.info(`[${serverName}] Giving up after ${maxTotalAttempts} attempts. Will retry on next tool call.`);
|
|
136
|
+
this.reconnectAttempts = 0;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
|
|
140
|
+
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
|
141
|
+
logger.info(`[${serverName}] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
|
|
142
|
+
} else if (this.reconnectAttempts % 10 === 0) {
|
|
143
|
+
logger.info(`[${serverName}] Still polling for native host (attempt ${this.reconnectAttempts})`);
|
|
144
|
+
}
|
|
145
|
+
this.reconnectTimer = setTimeout(() => {
|
|
146
|
+
this.reconnectTimer = null;
|
|
147
|
+
this.connect();
|
|
148
|
+
}, delay);
|
|
149
|
+
}
|
|
150
|
+
handleResponse(response) {
|
|
151
|
+
if (this.responseCallback) {
|
|
152
|
+
const callback = this.responseCallback;
|
|
153
|
+
this.responseCallback = null;
|
|
154
|
+
callback(response);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
setNotificationHandler(handler) {
|
|
158
|
+
this.notificationHandler = handler;
|
|
159
|
+
}
|
|
160
|
+
async ensureConnected() {
|
|
161
|
+
const { serverName } = this.context;
|
|
162
|
+
if (this.connected && this.socket) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (!this.socket && !this.connecting) {
|
|
166
|
+
await this.connect();
|
|
167
|
+
}
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
let checkTimeoutId = null;
|
|
170
|
+
const timeout = setTimeout(() => {
|
|
171
|
+
if (checkTimeoutId) {
|
|
172
|
+
clearTimeout(checkTimeoutId);
|
|
173
|
+
}
|
|
174
|
+
reject(new SocketConnectionError(`[${serverName}] Connection attempt timed out after 5000ms`));
|
|
175
|
+
}, 5000);
|
|
176
|
+
const checkConnection = () => {
|
|
177
|
+
if (this.connected) {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
resolve(true);
|
|
180
|
+
} else {
|
|
181
|
+
checkTimeoutId = setTimeout(checkConnection, 500);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
checkConnection();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async sendRequest(request, timeoutMs = 30000) {
|
|
188
|
+
const { serverName } = this.context;
|
|
189
|
+
if (!this.socket) {
|
|
190
|
+
throw new SocketConnectionError(`[${serverName}] Cannot send request: not connected`);
|
|
191
|
+
}
|
|
192
|
+
const socket = this.socket;
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const timeout = setTimeout(() => {
|
|
195
|
+
this.responseCallback = null;
|
|
196
|
+
reject(new SocketConnectionError(`[${serverName}] Tool request timed out after ${timeoutMs}ms`));
|
|
197
|
+
}, timeoutMs);
|
|
198
|
+
this.responseCallback = (response) => {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
resolve(response);
|
|
201
|
+
};
|
|
202
|
+
const requestJson = JSON.stringify(request);
|
|
203
|
+
const requestBytes = Buffer.from(requestJson, "utf-8");
|
|
204
|
+
const lengthPrefix = Buffer.allocUnsafe(4);
|
|
205
|
+
lengthPrefix.writeUInt32LE(requestBytes.length, 0);
|
|
206
|
+
const message = Buffer.concat([lengthPrefix, requestBytes]);
|
|
207
|
+
socket.write(message);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async callTool(name, args, _permissionOverrides) {
|
|
211
|
+
const request = {
|
|
212
|
+
method: "execute_tool",
|
|
213
|
+
params: {
|
|
214
|
+
client_id: this.context.clientTypeId,
|
|
215
|
+
tool: name,
|
|
216
|
+
args
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return this.sendRequestWithRetry(request);
|
|
220
|
+
}
|
|
221
|
+
async sendRequestWithRetry(request) {
|
|
222
|
+
const { serverName, logger } = this.context;
|
|
223
|
+
try {
|
|
224
|
+
return await this.sendRequest(request);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!(error instanceof SocketConnectionError)) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
logger.info(`[${serverName}] Connection error, forcing reconnect and retrying: ${error.message}`);
|
|
230
|
+
this.closeSocket();
|
|
231
|
+
await this.ensureConnected();
|
|
232
|
+
return await this.sendRequest(request);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async setPermissionMode(_mode, _allowedDomains) {}
|
|
236
|
+
isConnected() {
|
|
237
|
+
return this.connected;
|
|
238
|
+
}
|
|
239
|
+
closeSocket() {
|
|
240
|
+
if (this.socket) {
|
|
241
|
+
this.socket.removeAllListeners();
|
|
242
|
+
this.socket.end();
|
|
243
|
+
this.socket.destroy();
|
|
244
|
+
this.socket = null;
|
|
245
|
+
}
|
|
246
|
+
this.connected = false;
|
|
247
|
+
this.connecting = false;
|
|
248
|
+
}
|
|
249
|
+
cleanup() {
|
|
250
|
+
if (this.reconnectTimer) {
|
|
251
|
+
clearTimeout(this.reconnectTimer);
|
|
252
|
+
this.reconnectTimer = null;
|
|
253
|
+
}
|
|
254
|
+
this.closeSocket();
|
|
255
|
+
this.reconnectAttempts = 0;
|
|
256
|
+
this.responseBuffer = Buffer.alloc(0);
|
|
257
|
+
this.responseCallback = null;
|
|
258
|
+
}
|
|
259
|
+
disconnect() {
|
|
260
|
+
this.cleanup();
|
|
261
|
+
}
|
|
262
|
+
async validateSocketSecurity(socketPath) {
|
|
263
|
+
const { serverName, logger } = this.context;
|
|
264
|
+
if (platform() === "win32") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const dirPath = dirname(socketPath);
|
|
269
|
+
const dirBasename = dirPath.split("/").pop() || "";
|
|
270
|
+
const isSocketDir = dirBasename.startsWith("claude-mcp-browser-bridge-");
|
|
271
|
+
if (isSocketDir) {
|
|
272
|
+
try {
|
|
273
|
+
const dirStats = await fsPromises.stat(dirPath);
|
|
274
|
+
if (dirStats.isDirectory()) {
|
|
275
|
+
const dirMode = dirStats.mode & 511;
|
|
276
|
+
if (dirMode !== 448) {
|
|
277
|
+
throw new Error(`[${serverName}] Insecure socket directory permissions: ${dirMode.toString(8)} (expected 0700). Directory may have been tampered with.`);
|
|
278
|
+
}
|
|
279
|
+
const currentUid2 = process.getuid?.();
|
|
280
|
+
if (currentUid2 !== undefined && dirStats.uid !== currentUid2) {
|
|
281
|
+
throw new Error(`Socket directory not owned by current user (uid: ${currentUid2}, dir uid: ${dirStats.uid}). ` + `Potential security risk.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (dirError) {
|
|
285
|
+
if (dirError.code !== "ENOENT") {
|
|
286
|
+
throw dirError;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const stats = await fsPromises.stat(socketPath);
|
|
291
|
+
if (!stats.isSocket()) {
|
|
292
|
+
throw new Error(`[${serverName}] Path exists but it's not a socket: ${socketPath}`);
|
|
293
|
+
}
|
|
294
|
+
const mode = stats.mode & 511;
|
|
295
|
+
if (mode !== 384) {
|
|
296
|
+
throw new Error(`[${serverName}] Insecure socket permissions: ${mode.toString(8)} (expected 0600). Socket may have been tampered with.`);
|
|
297
|
+
}
|
|
298
|
+
const currentUid = process.getuid?.();
|
|
299
|
+
if (currentUid !== undefined && stats.uid !== currentUid) {
|
|
300
|
+
throw new Error(`Socket not owned by current user (uid: ${currentUid}, socket uid: ${stats.uid}). ` + `Potential security risk.`);
|
|
301
|
+
}
|
|
302
|
+
logger.info(`[${serverName}] Socket security validation passed`);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.code === "ENOENT") {
|
|
305
|
+
logger.info(`[${serverName}] Socket not found, will be created by server`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function createMcpSocketClient(context) {
|
|
313
|
+
return new McpSocketClient(context);
|
|
314
|
+
}
|
|
315
|
+
var SocketConnectionError;
|
|
316
|
+
var init_mcpSocketClient = __esm(() => {
|
|
317
|
+
SocketConnectionError = class SocketConnectionError extends Error {
|
|
318
|
+
constructor(message) {
|
|
319
|
+
super(message);
|
|
320
|
+
this.name = "SocketConnectionError";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// packages/@ant/claude-for-chrome-mcp/src/types.ts
|
|
326
|
+
function localPlatformLabel() {
|
|
327
|
+
return process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
|
|
328
|
+
}
|
|
329
|
+
var init_types2 = () => {};
|
|
330
|
+
|
|
331
|
+
// packages/@ant/claude-for-chrome-mcp/src/bridgeClient.ts
|
|
332
|
+
import WebSocket from "ws";
|
|
333
|
+
|
|
334
|
+
class BridgeClient {
|
|
335
|
+
ws = null;
|
|
336
|
+
connected = false;
|
|
337
|
+
authenticated = false;
|
|
338
|
+
connecting = false;
|
|
339
|
+
reconnectTimer = null;
|
|
340
|
+
reconnectAttempts = 0;
|
|
341
|
+
pendingCalls = new Map;
|
|
342
|
+
notificationHandler = null;
|
|
343
|
+
context;
|
|
344
|
+
permissionMode = "ask";
|
|
345
|
+
allowedDomains;
|
|
346
|
+
tabsContextCollectionTimeoutMs = 2000;
|
|
347
|
+
toolCallTimeoutMs = 120000;
|
|
348
|
+
connectionStartTime = null;
|
|
349
|
+
connectionEstablishedTime = null;
|
|
350
|
+
selectedDeviceId;
|
|
351
|
+
discoveryComplete = false;
|
|
352
|
+
discoveryPromise = null;
|
|
353
|
+
pendingDiscovery = null;
|
|
354
|
+
previousSelectedDeviceId;
|
|
355
|
+
peerConnectedWaiters = [];
|
|
356
|
+
pendingPairingRequestId;
|
|
357
|
+
pairingInProgress = false;
|
|
358
|
+
persistedDeviceId;
|
|
359
|
+
pendingSwitchResolve = null;
|
|
360
|
+
constructor(context) {
|
|
361
|
+
this.context = context;
|
|
362
|
+
if (context.initialPermissionMode) {
|
|
363
|
+
this.permissionMode = context.initialPermissionMode;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async ensureConnected() {
|
|
367
|
+
const { logger, serverName } = this.context;
|
|
368
|
+
logger.info(`[${serverName}] ensureConnected called, connected=${this.connected}, authenticated=${this.authenticated}, wsState=${this.ws?.readyState}`);
|
|
369
|
+
if (this.connected && this.authenticated && this.ws?.readyState === WebSocket.OPEN) {
|
|
370
|
+
logger.info(`[${serverName}] Already connected and authenticated`);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
if (!this.connecting) {
|
|
374
|
+
logger.info(`[${serverName}] Not connecting, starting connection...`);
|
|
375
|
+
await this.connect();
|
|
376
|
+
} else {
|
|
377
|
+
logger.info(`[${serverName}] Already connecting, waiting...`);
|
|
378
|
+
}
|
|
379
|
+
return new Promise((resolve) => {
|
|
380
|
+
const timeout = setTimeout(() => {
|
|
381
|
+
logger.info(`[${serverName}] Connection timeout, connected=${this.connected}, authenticated=${this.authenticated}`);
|
|
382
|
+
resolve(false);
|
|
383
|
+
}, 1e4);
|
|
384
|
+
const check = () => {
|
|
385
|
+
if (this.connected && this.authenticated) {
|
|
386
|
+
logger.info(`[${serverName}] Connection successful`);
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
resolve(true);
|
|
389
|
+
} else if (!this.connecting) {
|
|
390
|
+
logger.info(`[${serverName}] No longer connecting, giving up`);
|
|
391
|
+
clearTimeout(timeout);
|
|
392
|
+
resolve(false);
|
|
393
|
+
} else {
|
|
394
|
+
setTimeout(check, 200);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
check();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async callTool(name, args, permissionOverrides) {
|
|
401
|
+
const { logger, serverName, trackEvent } = this.context;
|
|
402
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
403
|
+
throw new SocketConnectionError(`[${serverName}] Bridge not connected`);
|
|
404
|
+
}
|
|
405
|
+
if (!this.selectedDeviceId && !this.discoveryComplete) {
|
|
406
|
+
this.discoveryPromise ??= this.discoverAndSelectExtension().finally(() => {
|
|
407
|
+
this.discoveryPromise = null;
|
|
408
|
+
});
|
|
409
|
+
await this.discoveryPromise;
|
|
410
|
+
}
|
|
411
|
+
const toolUseId = crypto.randomUUID();
|
|
412
|
+
const isTabsContext = name === "tabs_context_mcp";
|
|
413
|
+
const startTime = Date.now();
|
|
414
|
+
const timeoutMs = isTabsContext ? this.tabsContextCollectionTimeoutMs : this.toolCallTimeoutMs;
|
|
415
|
+
trackEvent?.("chrome_bridge_tool_call_started", {
|
|
416
|
+
tool_name: name,
|
|
417
|
+
tool_use_id: toolUseId
|
|
418
|
+
});
|
|
419
|
+
const effectivePermissionMode = permissionOverrides?.permissionMode ?? this.permissionMode;
|
|
420
|
+
const effectiveAllowedDomains = permissionOverrides?.allowedDomains ?? this.allowedDomains;
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const timer = setTimeout(() => {
|
|
423
|
+
const pending = this.pendingCalls.get(toolUseId);
|
|
424
|
+
if (pending) {
|
|
425
|
+
this.pendingCalls.delete(toolUseId);
|
|
426
|
+
const durationMs = Date.now() - pending.startTime;
|
|
427
|
+
if (isTabsContext && pending.results.length > 0) {
|
|
428
|
+
trackEvent?.("chrome_bridge_tool_call_completed", {
|
|
429
|
+
tool_name: name,
|
|
430
|
+
tool_use_id: toolUseId,
|
|
431
|
+
duration_ms: durationMs
|
|
432
|
+
});
|
|
433
|
+
resolve(this.mergeTabsResults(pending.results));
|
|
434
|
+
} else {
|
|
435
|
+
logger.warn(`[${serverName}] Tool call timeout: ${name} (${toolUseId.slice(0, 8)}) after ${durationMs}ms, pending calls: ${this.pendingCalls.size}`);
|
|
436
|
+
trackEvent?.("chrome_bridge_tool_call_timeout", {
|
|
437
|
+
tool_name: name,
|
|
438
|
+
tool_use_id: toolUseId,
|
|
439
|
+
duration_ms: durationMs,
|
|
440
|
+
timeout_ms: timeoutMs
|
|
441
|
+
});
|
|
442
|
+
reject(new SocketConnectionError(`[${serverName}] Tool call timed out: ${name}`));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}, timeoutMs);
|
|
446
|
+
this.pendingCalls.set(toolUseId, {
|
|
447
|
+
resolve,
|
|
448
|
+
reject,
|
|
449
|
+
timer,
|
|
450
|
+
results: [],
|
|
451
|
+
isTabsContext,
|
|
452
|
+
onPermissionRequest: permissionOverrides?.onPermissionRequest,
|
|
453
|
+
startTime,
|
|
454
|
+
toolName: name
|
|
455
|
+
});
|
|
456
|
+
const message = {
|
|
457
|
+
type: "tool_call",
|
|
458
|
+
tool_use_id: toolUseId,
|
|
459
|
+
client_type: this.context.clientTypeId,
|
|
460
|
+
tool: name,
|
|
461
|
+
args
|
|
462
|
+
};
|
|
463
|
+
if (this.selectedDeviceId) {
|
|
464
|
+
message.target_device_id = this.selectedDeviceId;
|
|
465
|
+
}
|
|
466
|
+
if (effectivePermissionMode) {
|
|
467
|
+
message.permission_mode = effectivePermissionMode;
|
|
468
|
+
}
|
|
469
|
+
if (effectiveAllowedDomains?.length) {
|
|
470
|
+
message.allowed_domains = effectiveAllowedDomains;
|
|
471
|
+
}
|
|
472
|
+
if (permissionOverrides?.onPermissionRequest) {
|
|
473
|
+
message.handle_permission_prompts = true;
|
|
474
|
+
}
|
|
475
|
+
logger.debug(`[${serverName}] Sending tool_call: ${name} (${toolUseId.slice(0, 8)})`);
|
|
476
|
+
this.ws.send(JSON.stringify(message));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
isConnected() {
|
|
480
|
+
return this.connected && this.authenticated && this.ws?.readyState === WebSocket.OPEN;
|
|
481
|
+
}
|
|
482
|
+
disconnect() {
|
|
483
|
+
this.cleanup();
|
|
484
|
+
}
|
|
485
|
+
setNotificationHandler(handler) {
|
|
486
|
+
this.notificationHandler = handler;
|
|
487
|
+
}
|
|
488
|
+
async setPermissionMode(mode, allowedDomains) {
|
|
489
|
+
this.permissionMode = mode;
|
|
490
|
+
this.allowedDomains = allowedDomains;
|
|
491
|
+
}
|
|
492
|
+
async discoverAndSelectExtension() {
|
|
493
|
+
const { logger, serverName } = this.context;
|
|
494
|
+
this.persistedDeviceId ??= this.context.getPersistedDeviceId?.();
|
|
495
|
+
let extensions = await this.queryBridgeExtensions();
|
|
496
|
+
if (extensions.length === 0) {
|
|
497
|
+
logger.info(`[${serverName}] No extensions connected, waiting up to ${PEER_WAIT_TIMEOUT_MS}ms for peer_connected`);
|
|
498
|
+
const peerArrived = await this.waitForPeerConnected(PEER_WAIT_TIMEOUT_MS);
|
|
499
|
+
if (peerArrived) {
|
|
500
|
+
extensions = await this.queryBridgeExtensions();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
this.discoveryComplete = true;
|
|
504
|
+
if (extensions.length === 0) {
|
|
505
|
+
logger.info(`[${serverName}] No extensions found after waiting`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (extensions.length === 1) {
|
|
509
|
+
const ext = extensions[0];
|
|
510
|
+
if (!this.isLocalExtension(ext)) {
|
|
511
|
+
this.context.onRemoteExtensionWarning?.(ext);
|
|
512
|
+
}
|
|
513
|
+
this.selectExtension(ext.deviceId);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (this.persistedDeviceId) {
|
|
517
|
+
const persisted = extensions.find((e) => e.deviceId === this.persistedDeviceId);
|
|
518
|
+
if (persisted) {
|
|
519
|
+
logger.info(`[${serverName}] Auto-connecting to persisted extension: ${persisted.name || persisted.deviceId.slice(0, 8)}`);
|
|
520
|
+
this.selectExtension(persisted.deviceId);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.broadcastPairingRequest();
|
|
525
|
+
this.pairingInProgress = true;
|
|
526
|
+
}
|
|
527
|
+
async queryBridgeExtensions() {
|
|
528
|
+
const raw = await new Promise((resolve) => {
|
|
529
|
+
const timeout = setTimeout(() => {
|
|
530
|
+
this.pendingDiscovery = null;
|
|
531
|
+
resolve([]);
|
|
532
|
+
}, DISCOVERY_TIMEOUT_MS);
|
|
533
|
+
this.pendingDiscovery = { resolve, timeout };
|
|
534
|
+
this.ws?.send(JSON.stringify({ type: "list_extensions" }));
|
|
535
|
+
});
|
|
536
|
+
const byDeviceId = new Map;
|
|
537
|
+
for (const ext of raw) {
|
|
538
|
+
const existing = byDeviceId.get(ext.deviceId);
|
|
539
|
+
if (!existing || ext.connectedAt > existing.connectedAt) {
|
|
540
|
+
byDeviceId.set(ext.deviceId, ext);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return [...byDeviceId.values()];
|
|
544
|
+
}
|
|
545
|
+
selectExtension(deviceId) {
|
|
546
|
+
const { logger, serverName } = this.context;
|
|
547
|
+
this.selectedDeviceId = deviceId;
|
|
548
|
+
this.previousSelectedDeviceId = undefined;
|
|
549
|
+
logger.info(`[${serverName}] Selected Chrome extension: ${deviceId.slice(0, 8)}...`);
|
|
550
|
+
}
|
|
551
|
+
isLocalExtension(ext) {
|
|
552
|
+
if (!ext.osPlatform)
|
|
553
|
+
return false;
|
|
554
|
+
return ext.osPlatform === localPlatformLabel();
|
|
555
|
+
}
|
|
556
|
+
waitForPeerConnected(timeoutMs) {
|
|
557
|
+
return new Promise((resolve) => {
|
|
558
|
+
const timer = setTimeout(() => {
|
|
559
|
+
this.peerConnectedWaiters = this.peerConnectedWaiters.filter((w) => w !== onPeer);
|
|
560
|
+
resolve(false);
|
|
561
|
+
}, timeoutMs);
|
|
562
|
+
const onPeer = (arrived) => {
|
|
563
|
+
clearTimeout(timer);
|
|
564
|
+
resolve(arrived);
|
|
565
|
+
};
|
|
566
|
+
this.peerConnectedWaiters.push(onPeer);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
broadcastPairingRequest() {
|
|
570
|
+
const requestId = crypto.randomUUID();
|
|
571
|
+
this.pendingPairingRequestId = requestId;
|
|
572
|
+
this.ws?.send(JSON.stringify({
|
|
573
|
+
type: "pairing_request",
|
|
574
|
+
request_id: requestId,
|
|
575
|
+
client_type: this.context.clientTypeId
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
async switchBrowser() {
|
|
579
|
+
const extensions = await this.queryBridgeExtensions();
|
|
580
|
+
const currentDeviceId = this.selectedDeviceId ?? this.previousSelectedDeviceId;
|
|
581
|
+
if (extensions.length === 0 || extensions.length === 1 && (!currentDeviceId || extensions[0].deviceId === currentDeviceId)) {
|
|
582
|
+
return "no_other_browsers";
|
|
583
|
+
}
|
|
584
|
+
this.previousSelectedDeviceId = this.selectedDeviceId;
|
|
585
|
+
this.selectedDeviceId = undefined;
|
|
586
|
+
this.discoveryComplete = false;
|
|
587
|
+
this.pairingInProgress = false;
|
|
588
|
+
const requestId = crypto.randomUUID();
|
|
589
|
+
this.pendingPairingRequestId = requestId;
|
|
590
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
this.ws.send(JSON.stringify({
|
|
594
|
+
type: "pairing_request",
|
|
595
|
+
request_id: requestId,
|
|
596
|
+
client_type: this.context.clientTypeId
|
|
597
|
+
}));
|
|
598
|
+
if (this.pendingSwitchResolve) {
|
|
599
|
+
this.pendingSwitchResolve(null);
|
|
600
|
+
}
|
|
601
|
+
return new Promise((resolve) => {
|
|
602
|
+
const timer = setTimeout(() => {
|
|
603
|
+
if (this.pendingPairingRequestId === requestId) {
|
|
604
|
+
this.pendingPairingRequestId = undefined;
|
|
605
|
+
}
|
|
606
|
+
this.pendingSwitchResolve = null;
|
|
607
|
+
resolve(null);
|
|
608
|
+
}, 120000);
|
|
609
|
+
this.pendingSwitchResolve = (result) => {
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
this.pendingSwitchResolve = null;
|
|
612
|
+
resolve(result);
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async connect() {
|
|
617
|
+
const { logger, serverName, bridgeConfig, trackEvent } = this.context;
|
|
618
|
+
if (!bridgeConfig) {
|
|
619
|
+
logger.error(`[${serverName}] No bridge config provided`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (this.connecting) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
this.connecting = true;
|
|
626
|
+
this.authenticated = false;
|
|
627
|
+
this.connectionStartTime = Date.now();
|
|
628
|
+
this.closeSocket();
|
|
629
|
+
let userId;
|
|
630
|
+
let token;
|
|
631
|
+
if (bridgeConfig.devUserId) {
|
|
632
|
+
userId = bridgeConfig.devUserId;
|
|
633
|
+
logger.debug(`[${serverName}] Using dev user ID for bridge connection`);
|
|
634
|
+
} else {
|
|
635
|
+
logger.debug(`[${serverName}] Fetching user ID for bridge connection`);
|
|
636
|
+
const fetchedUserId = await bridgeConfig.getUserId();
|
|
637
|
+
if (!fetchedUserId) {
|
|
638
|
+
const durationMs = Date.now() - this.connectionStartTime;
|
|
639
|
+
logger.error(`[${serverName}] No user ID available after ${durationMs}ms`);
|
|
640
|
+
trackEvent?.("chrome_bridge_connection_failed", {
|
|
641
|
+
duration_ms: durationMs,
|
|
642
|
+
error_type: "no_user_id",
|
|
643
|
+
reconnect_attempt: this.reconnectAttempts
|
|
644
|
+
});
|
|
645
|
+
this.connecting = false;
|
|
646
|
+
this.context.onAuthenticationError?.();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
userId = fetchedUserId;
|
|
650
|
+
logger.debug(`[${serverName}] Fetching OAuth token for bridge connection`);
|
|
651
|
+
token = await bridgeConfig.getOAuthToken();
|
|
652
|
+
if (!token) {
|
|
653
|
+
const durationMs = Date.now() - this.connectionStartTime;
|
|
654
|
+
logger.error(`[${serverName}] No OAuth token available after ${durationMs}ms`);
|
|
655
|
+
trackEvent?.("chrome_bridge_connection_failed", {
|
|
656
|
+
duration_ms: durationMs,
|
|
657
|
+
error_type: "no_oauth_token",
|
|
658
|
+
reconnect_attempt: this.reconnectAttempts
|
|
659
|
+
});
|
|
660
|
+
this.connecting = false;
|
|
661
|
+
this.context.onAuthenticationError?.();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const wsUrl = `${bridgeConfig.url}/chrome/${userId}`;
|
|
666
|
+
logger.info(`[${serverName}] Connecting to bridge: ${wsUrl}`);
|
|
667
|
+
trackEvent?.("chrome_bridge_connection_started", {
|
|
668
|
+
bridge_url: wsUrl
|
|
669
|
+
});
|
|
670
|
+
try {
|
|
671
|
+
this.ws = new WebSocket(wsUrl);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
const durationMs = Date.now() - this.connectionStartTime;
|
|
674
|
+
logger.error(`[${serverName}] Failed to create WebSocket after ${durationMs}ms:`, error);
|
|
675
|
+
trackEvent?.("chrome_bridge_connection_failed", {
|
|
676
|
+
duration_ms: durationMs,
|
|
677
|
+
error_type: "websocket_error",
|
|
678
|
+
reconnect_attempt: this.reconnectAttempts
|
|
679
|
+
});
|
|
680
|
+
this.connecting = false;
|
|
681
|
+
this.scheduleReconnect();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.ws.on("open", () => {
|
|
685
|
+
logger.info(`[${serverName}] WebSocket connected, sending connect message`);
|
|
686
|
+
const connectMessage = {
|
|
687
|
+
type: "connect",
|
|
688
|
+
client_type: this.context.clientTypeId
|
|
689
|
+
};
|
|
690
|
+
if (bridgeConfig.devUserId) {
|
|
691
|
+
connectMessage.dev_user_id = bridgeConfig.devUserId;
|
|
692
|
+
} else {
|
|
693
|
+
connectMessage.oauth_token = token;
|
|
694
|
+
}
|
|
695
|
+
this.ws?.send(JSON.stringify(connectMessage));
|
|
696
|
+
});
|
|
697
|
+
this.ws.on("message", (data) => {
|
|
698
|
+
try {
|
|
699
|
+
const message = JSON.parse(data.toString());
|
|
700
|
+
logger.debug(`[${serverName}] Bridge received: ${JSON.stringify(message)}`);
|
|
701
|
+
this.handleMessage(message);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
logger.error(`[${serverName}] Failed to parse bridge message:`, error);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
this.ws.on("close", (code) => {
|
|
707
|
+
const durationSinceConnect = this.connectionEstablishedTime ? Date.now() - this.connectionEstablishedTime : 0;
|
|
708
|
+
logger.info(`[${serverName}] Bridge connection closed (code: ${code}, duration: ${durationSinceConnect}ms)`);
|
|
709
|
+
trackEvent?.("chrome_bridge_disconnected", {
|
|
710
|
+
close_code: code,
|
|
711
|
+
duration_since_connect_ms: durationSinceConnect,
|
|
712
|
+
reconnect_attempt: this.reconnectAttempts + 1
|
|
713
|
+
});
|
|
714
|
+
this.connected = false;
|
|
715
|
+
this.authenticated = false;
|
|
716
|
+
this.connecting = false;
|
|
717
|
+
this.connectionEstablishedTime = null;
|
|
718
|
+
this.scheduleReconnect();
|
|
719
|
+
});
|
|
720
|
+
this.ws.on("error", (error) => {
|
|
721
|
+
const durationMs = this.connectionStartTime ? Date.now() - this.connectionStartTime : 0;
|
|
722
|
+
logger.error(`[${serverName}] Bridge WebSocket error after ${durationMs}ms: ${error.message}`);
|
|
723
|
+
trackEvent?.("chrome_bridge_connection_failed", {
|
|
724
|
+
duration_ms: durationMs,
|
|
725
|
+
error_type: "websocket_error",
|
|
726
|
+
reconnect_attempt: this.reconnectAttempts
|
|
727
|
+
});
|
|
728
|
+
this.connected = false;
|
|
729
|
+
this.authenticated = false;
|
|
730
|
+
this.connecting = false;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
handleMessage(message) {
|
|
734
|
+
const { logger, serverName, trackEvent } = this.context;
|
|
735
|
+
switch (message.type) {
|
|
736
|
+
case "paired": {
|
|
737
|
+
const durationMs = this.connectionStartTime ? Date.now() - this.connectionStartTime : 0;
|
|
738
|
+
logger.info(`[${serverName}] Paired with Chrome extension (duration: ${durationMs}ms)`);
|
|
739
|
+
this.connected = true;
|
|
740
|
+
this.authenticated = true;
|
|
741
|
+
this.connecting = false;
|
|
742
|
+
this.reconnectAttempts = 0;
|
|
743
|
+
this.connectionEstablishedTime = Date.now();
|
|
744
|
+
trackEvent?.("chrome_bridge_connection_succeeded", {
|
|
745
|
+
duration_ms: durationMs,
|
|
746
|
+
status: "paired"
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "waiting": {
|
|
751
|
+
const durationMs = this.connectionStartTime ? Date.now() - this.connectionStartTime : 0;
|
|
752
|
+
logger.info(`[${serverName}] Waiting for Chrome extension to connect (duration: ${durationMs}ms)`);
|
|
753
|
+
this.connected = true;
|
|
754
|
+
this.authenticated = true;
|
|
755
|
+
this.connecting = false;
|
|
756
|
+
this.reconnectAttempts = 0;
|
|
757
|
+
this.connectionEstablishedTime = Date.now();
|
|
758
|
+
trackEvent?.("chrome_bridge_connection_succeeded", {
|
|
759
|
+
duration_ms: durationMs,
|
|
760
|
+
status: "waiting"
|
|
761
|
+
});
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "peer_connected":
|
|
765
|
+
logger.info(`[${serverName}] Chrome extension connected to bridge`);
|
|
766
|
+
trackEvent?.("chrome_bridge_peer_connected", null);
|
|
767
|
+
if (!this.selectedDeviceId) {
|
|
768
|
+
this.discoveryComplete = false;
|
|
769
|
+
}
|
|
770
|
+
if (this.previousSelectedDeviceId && message.deviceId === this.previousSelectedDeviceId && !this.pendingSwitchResolve) {
|
|
771
|
+
logger.info(`[${serverName}] Previously selected extension reconnected, auto-reselecting`);
|
|
772
|
+
this.selectExtension(this.previousSelectedDeviceId);
|
|
773
|
+
this.previousSelectedDeviceId = undefined;
|
|
774
|
+
}
|
|
775
|
+
if (this.peerConnectedWaiters.length > 0) {
|
|
776
|
+
const waiters = this.peerConnectedWaiters;
|
|
777
|
+
this.peerConnectedWaiters = [];
|
|
778
|
+
for (const waiter of waiters) {
|
|
779
|
+
waiter(true);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case "peer_disconnected":
|
|
784
|
+
logger.info(`[${serverName}] Chrome extension disconnected from bridge`);
|
|
785
|
+
trackEvent?.("chrome_bridge_peer_disconnected", null);
|
|
786
|
+
if (message.deviceId && message.deviceId === this.selectedDeviceId) {
|
|
787
|
+
logger.info(`[${serverName}] Selected extension disconnected, clearing selection`);
|
|
788
|
+
this.previousSelectedDeviceId = this.selectedDeviceId;
|
|
789
|
+
this.selectedDeviceId = undefined;
|
|
790
|
+
this.discoveryComplete = false;
|
|
791
|
+
}
|
|
792
|
+
break;
|
|
793
|
+
case "extensions_list":
|
|
794
|
+
if (this.pendingDiscovery) {
|
|
795
|
+
clearTimeout(this.pendingDiscovery.timeout);
|
|
796
|
+
this.pendingDiscovery.resolve(message.extensions ?? []);
|
|
797
|
+
this.pendingDiscovery = null;
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
case "pairing_response": {
|
|
801
|
+
const requestId = message.request_id;
|
|
802
|
+
const responseDeviceId = message.device_id;
|
|
803
|
+
const responseName = message.name;
|
|
804
|
+
if (this.pendingPairingRequestId === requestId && responseDeviceId && responseName) {
|
|
805
|
+
this.pendingPairingRequestId = undefined;
|
|
806
|
+
this.pairingInProgress = false;
|
|
807
|
+
this.selectExtension(responseDeviceId);
|
|
808
|
+
this.context.onExtensionPaired?.(responseDeviceId, responseName);
|
|
809
|
+
logger.info(`[${serverName}] Paired with "${responseName}" (${responseDeviceId.slice(0, 8)})`);
|
|
810
|
+
if (this.pendingSwitchResolve) {
|
|
811
|
+
this.pendingSwitchResolve({
|
|
812
|
+
deviceId: responseDeviceId,
|
|
813
|
+
name: responseName
|
|
814
|
+
});
|
|
815
|
+
this.pendingSwitchResolve = null;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case "ping":
|
|
821
|
+
this.ws?.send(JSON.stringify({ type: "pong" }));
|
|
822
|
+
break;
|
|
823
|
+
case "pong":
|
|
824
|
+
break;
|
|
825
|
+
case "tool_result":
|
|
826
|
+
this.handleToolResult(message);
|
|
827
|
+
break;
|
|
828
|
+
case "permission_request":
|
|
829
|
+
this.handlePermissionRequest(message);
|
|
830
|
+
break;
|
|
831
|
+
case "notification":
|
|
832
|
+
if (this.notificationHandler) {
|
|
833
|
+
this.notificationHandler({
|
|
834
|
+
method: message.method,
|
|
835
|
+
params: message.params
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
case "error":
|
|
840
|
+
logger.warn(`[${serverName}] Bridge error: ${message.error}`);
|
|
841
|
+
if (this.selectedDeviceId) {
|
|
842
|
+
this.selectedDeviceId = undefined;
|
|
843
|
+
this.discoveryComplete = false;
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
default:
|
|
847
|
+
logger.warn(`[${serverName}] Unrecognized bridge message type: ${message.type}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async handlePermissionRequest(message) {
|
|
851
|
+
const { logger, serverName } = this.context;
|
|
852
|
+
const toolUseId = message.tool_use_id;
|
|
853
|
+
const requestId = message.request_id;
|
|
854
|
+
if (!toolUseId || !requestId) {
|
|
855
|
+
logger.warn(`[${serverName}] permission_request missing tool_use_id or request_id`);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const pending = this.pendingCalls.get(toolUseId);
|
|
859
|
+
if (!pending?.onPermissionRequest) {
|
|
860
|
+
logger.debug(`[${serverName}] Ignoring permission_request for unknown tool_use_id ${toolUseId.slice(0, 8)} (not our call)`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const request = {
|
|
864
|
+
toolUseId,
|
|
865
|
+
requestId,
|
|
866
|
+
toolType: message.tool_type ?? "unknown",
|
|
867
|
+
url: message.url ?? "",
|
|
868
|
+
actionData: message.action_data
|
|
869
|
+
};
|
|
870
|
+
try {
|
|
871
|
+
const allowed = await pending.onPermissionRequest(request);
|
|
872
|
+
this.sendPermissionResponse(requestId, allowed);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger.error(`[${serverName}] Error handling permission request:`, error);
|
|
875
|
+
this.sendPermissionResponse(requestId, false);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
sendPermissionResponse(requestId, allowed) {
|
|
879
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
880
|
+
const message = {
|
|
881
|
+
type: "permission_response",
|
|
882
|
+
request_id: requestId,
|
|
883
|
+
allowed
|
|
884
|
+
};
|
|
885
|
+
if (this.selectedDeviceId) {
|
|
886
|
+
message.target_device_id = this.selectedDeviceId;
|
|
887
|
+
}
|
|
888
|
+
this.ws.send(JSON.stringify(message));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
handleToolResult(message) {
|
|
892
|
+
const { logger, serverName, trackEvent } = this.context;
|
|
893
|
+
const toolUseId = message.tool_use_id;
|
|
894
|
+
if (!toolUseId) {
|
|
895
|
+
logger.warn(`[${serverName}] Received tool_result without tool_use_id`);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const pending = this.pendingCalls.get(toolUseId);
|
|
899
|
+
if (!pending) {
|
|
900
|
+
logger.debug(`[${serverName}] Received tool_result for unknown call: ${toolUseId.slice(0, 8)}`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const durationMs = Date.now() - pending.startTime;
|
|
904
|
+
const normalized = this.normalizeBridgeResponse(message);
|
|
905
|
+
const isError = Boolean(message.is_error) || "error" in normalized;
|
|
906
|
+
if (pending.isTabsContext && !this.selectedDeviceId) {
|
|
907
|
+
pending.results.push(normalized);
|
|
908
|
+
} else {
|
|
909
|
+
clearTimeout(pending.timer);
|
|
910
|
+
this.pendingCalls.delete(toolUseId);
|
|
911
|
+
if (isError) {
|
|
912
|
+
const errorContent = normalized.error?.content;
|
|
913
|
+
let errorMessage = "Unknown error";
|
|
914
|
+
if (Array.isArray(errorContent)) {
|
|
915
|
+
const textItem = errorContent.find((item) => typeof item === "object" && item !== null && ("text" in item));
|
|
916
|
+
if (textItem?.text) {
|
|
917
|
+
errorMessage = textItem.text.slice(0, 200);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
logger.warn(`[${serverName}] Tool call error: ${pending.toolName} (${toolUseId.slice(0, 8)}) after ${durationMs}ms`);
|
|
921
|
+
trackEvent?.("chrome_bridge_tool_call_error", {
|
|
922
|
+
tool_name: pending.toolName,
|
|
923
|
+
tool_use_id: toolUseId,
|
|
924
|
+
duration_ms: durationMs,
|
|
925
|
+
error_message: errorMessage
|
|
926
|
+
});
|
|
927
|
+
} else {
|
|
928
|
+
logger.debug(`[${serverName}] Tool call completed: ${pending.toolName} (${toolUseId.slice(0, 8)}) in ${durationMs}ms`);
|
|
929
|
+
trackEvent?.("chrome_bridge_tool_call_completed", {
|
|
930
|
+
tool_name: pending.toolName,
|
|
931
|
+
tool_use_id: toolUseId,
|
|
932
|
+
duration_ms: durationMs
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
pending.resolve(normalized);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
normalizeBridgeResponse(message) {
|
|
939
|
+
if (message.result || message.error) {
|
|
940
|
+
return message;
|
|
941
|
+
}
|
|
942
|
+
if (message.content) {
|
|
943
|
+
if (message.is_error) {
|
|
944
|
+
return { error: { content: message.content } };
|
|
945
|
+
}
|
|
946
|
+
return { result: { content: message.content } };
|
|
947
|
+
}
|
|
948
|
+
return message;
|
|
949
|
+
}
|
|
950
|
+
mergeTabsResults(results) {
|
|
951
|
+
const mergedTabs = [];
|
|
952
|
+
for (const result of results) {
|
|
953
|
+
const msg = result;
|
|
954
|
+
const resultData = msg.result;
|
|
955
|
+
const content = resultData?.content;
|
|
956
|
+
if (!content || !Array.isArray(content))
|
|
957
|
+
continue;
|
|
958
|
+
for (const item of content) {
|
|
959
|
+
if (item.type === "text" && item.text) {
|
|
960
|
+
try {
|
|
961
|
+
const parsed = JSON.parse(item.text);
|
|
962
|
+
if (Array.isArray(parsed)) {
|
|
963
|
+
mergedTabs.push(...parsed);
|
|
964
|
+
} else if (parsed?.availableTabs && Array.isArray(parsed.availableTabs)) {
|
|
965
|
+
mergedTabs.push(...parsed.availableTabs);
|
|
966
|
+
}
|
|
967
|
+
} catch {}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (mergedTabs.length > 0) {
|
|
972
|
+
const tabListText = mergedTabs.map((t) => {
|
|
973
|
+
const tab = t;
|
|
974
|
+
return ` \u2022 tabId ${tab.tabId}: "${tab.title}" (${tab.url})`;
|
|
975
|
+
}).join(`
|
|
976
|
+
`);
|
|
977
|
+
return {
|
|
978
|
+
result: {
|
|
979
|
+
content: [
|
|
980
|
+
{
|
|
981
|
+
type: "text",
|
|
982
|
+
text: JSON.stringify({ availableTabs: mergedTabs })
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
type: "text",
|
|
986
|
+
text: `
|
|
987
|
+
|
|
988
|
+
Tab Context:
|
|
989
|
+
- Available tabs:
|
|
990
|
+
${tabListText}`
|
|
991
|
+
}
|
|
992
|
+
]
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return results[0];
|
|
997
|
+
}
|
|
998
|
+
scheduleReconnect() {
|
|
999
|
+
const { logger, serverName, trackEvent } = this.context;
|
|
1000
|
+
if (this.reconnectTimer)
|
|
1001
|
+
return;
|
|
1002
|
+
this.reconnectAttempts++;
|
|
1003
|
+
if (this.reconnectAttempts > 100) {
|
|
1004
|
+
logger.warn(`[${serverName}] Giving up bridge reconnection after 100 attempts`);
|
|
1005
|
+
trackEvent?.("chrome_bridge_reconnect_exhausted", {
|
|
1006
|
+
total_attempts: 100
|
|
1007
|
+
});
|
|
1008
|
+
this.reconnectAttempts = 0;
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const delay = Math.min(2000 * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
|
|
1012
|
+
if (this.reconnectAttempts <= 10 || this.reconnectAttempts % 10 === 0) {
|
|
1013
|
+
logger.info(`[${serverName}] Bridge reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
|
|
1014
|
+
}
|
|
1015
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1016
|
+
this.reconnectTimer = null;
|
|
1017
|
+
this.connect();
|
|
1018
|
+
}, delay);
|
|
1019
|
+
}
|
|
1020
|
+
closeSocket() {
|
|
1021
|
+
if (this.ws) {
|
|
1022
|
+
this.ws.removeAllListeners();
|
|
1023
|
+
this.ws.close();
|
|
1024
|
+
this.ws = null;
|
|
1025
|
+
}
|
|
1026
|
+
this.connected = false;
|
|
1027
|
+
this.authenticated = false;
|
|
1028
|
+
this.selectedDeviceId = undefined;
|
|
1029
|
+
this.discoveryComplete = false;
|
|
1030
|
+
this.pendingPairingRequestId = undefined;
|
|
1031
|
+
this.pairingInProgress = false;
|
|
1032
|
+
if (this.pendingSwitchResolve) {
|
|
1033
|
+
this.pendingSwitchResolve(null);
|
|
1034
|
+
this.pendingSwitchResolve = null;
|
|
1035
|
+
}
|
|
1036
|
+
if (this.pendingDiscovery) {
|
|
1037
|
+
clearTimeout(this.pendingDiscovery.timeout);
|
|
1038
|
+
this.pendingDiscovery.resolve([]);
|
|
1039
|
+
this.pendingDiscovery = null;
|
|
1040
|
+
}
|
|
1041
|
+
if (this.peerConnectedWaiters.length > 0) {
|
|
1042
|
+
const waiters = this.peerConnectedWaiters;
|
|
1043
|
+
this.peerConnectedWaiters = [];
|
|
1044
|
+
for (const waiter of waiters) {
|
|
1045
|
+
waiter(false);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
cleanup() {
|
|
1050
|
+
if (this.reconnectTimer) {
|
|
1051
|
+
clearTimeout(this.reconnectTimer);
|
|
1052
|
+
this.reconnectTimer = null;
|
|
1053
|
+
}
|
|
1054
|
+
for (const [id, pending] of this.pendingCalls) {
|
|
1055
|
+
clearTimeout(pending.timer);
|
|
1056
|
+
pending.reject(new SocketConnectionError("Bridge client disconnected"));
|
|
1057
|
+
this.pendingCalls.delete(id);
|
|
1058
|
+
}
|
|
1059
|
+
this.closeSocket();
|
|
1060
|
+
this.reconnectAttempts = 0;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function createBridgeClient(context) {
|
|
1064
|
+
return new BridgeClient(context);
|
|
1065
|
+
}
|
|
1066
|
+
var DISCOVERY_TIMEOUT_MS = 5000, PEER_WAIT_TIMEOUT_MS = 1e4;
|
|
1067
|
+
var init_bridgeClient = __esm(() => {
|
|
1068
|
+
init_mcpSocketClient();
|
|
1069
|
+
init_types2();
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// packages/@ant/claude-for-chrome-mcp/src/browserTools.ts
|
|
1073
|
+
var BROWSER_TOOLS;
|
|
1074
|
+
var init_browserTools = __esm(() => {
|
|
1075
|
+
BROWSER_TOOLS = [
|
|
1076
|
+
{
|
|
1077
|
+
name: "javascript_tool",
|
|
1078
|
+
description: "Execute JavaScript code in the context of the current page. The code runs in the page's context and can interact with the DOM, window object, and page variables. Returns the result of the last expression or any thrown errors. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1079
|
+
inputSchema: {
|
|
1080
|
+
type: "object",
|
|
1081
|
+
properties: {
|
|
1082
|
+
action: {
|
|
1083
|
+
type: "string",
|
|
1084
|
+
description: "Must be set to 'javascript_exec'"
|
|
1085
|
+
},
|
|
1086
|
+
text: {
|
|
1087
|
+
type: "string",
|
|
1088
|
+
description: "The JavaScript code to execute. The code will be evaluated in the page context. The result of the last expression will be returned automatically. Do NOT use 'return' statements - just write the expression you want to evaluate (e.g., 'window.myData.value' not 'return window.myData.value'). You can access and modify the DOM, call page functions, and interact with page variables."
|
|
1089
|
+
},
|
|
1090
|
+
tabId: {
|
|
1091
|
+
type: "number",
|
|
1092
|
+
description: "Tab ID to execute the code in. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
required: ["action", "text", "tabId"]
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: "read_page",
|
|
1100
|
+
description: "Get an accessibility tree representation of elements on the page. By default returns all elements including non-visible ones. Output is limited to 50000 characters by default. If the output exceeds this limit, you will receive an error asking you to specify a smaller depth or focus on a specific element using ref_id. Optionally filter for only interactive elements. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1101
|
+
inputSchema: {
|
|
1102
|
+
type: "object",
|
|
1103
|
+
properties: {
|
|
1104
|
+
filter: {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
enum: ["interactive", "all"],
|
|
1107
|
+
description: 'Filter elements: "interactive" for buttons/links/inputs only, "all" for all elements including non-visible ones (default: all elements)'
|
|
1108
|
+
},
|
|
1109
|
+
tabId: {
|
|
1110
|
+
type: "number",
|
|
1111
|
+
description: "Tab ID to read from. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1112
|
+
},
|
|
1113
|
+
depth: {
|
|
1114
|
+
type: "number",
|
|
1115
|
+
description: "Maximum depth of the tree to traverse (default: 15). Use a smaller depth if output is too large."
|
|
1116
|
+
},
|
|
1117
|
+
ref_id: {
|
|
1118
|
+
type: "string",
|
|
1119
|
+
description: "Reference ID of a parent element to read. Will return the specified element and all its children. Use this to focus on a specific part of the page when output is too large."
|
|
1120
|
+
},
|
|
1121
|
+
max_chars: {
|
|
1122
|
+
type: "number",
|
|
1123
|
+
description: "Maximum characters for output (default: 50000). Set to a higher value if your client can handle large outputs."
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
required: ["tabId"]
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: "find",
|
|
1131
|
+
description: `Find elements on the page using natural language. Can search for elements by their purpose (e.g., "search bar", "login button") or by text content (e.g., "organic mango product"). Returns up to 20 matching elements with references that can be used with other tools. If more than 20 matches exist, you'll be notified to use a more specific query. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.`,
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: {
|
|
1135
|
+
query: {
|
|
1136
|
+
type: "string",
|
|
1137
|
+
description: 'Natural language description of what to find (e.g., "search bar", "add to cart button", "product title containing organic")'
|
|
1138
|
+
},
|
|
1139
|
+
tabId: {
|
|
1140
|
+
type: "number",
|
|
1141
|
+
description: "Tab ID to search in. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
required: ["query", "tabId"]
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: "form_input",
|
|
1149
|
+
description: "Set values in form elements using element reference ID from the read_page tool. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1150
|
+
inputSchema: {
|
|
1151
|
+
type: "object",
|
|
1152
|
+
properties: {
|
|
1153
|
+
ref: {
|
|
1154
|
+
type: "string",
|
|
1155
|
+
description: 'Element reference ID from the read_page tool (e.g., "ref_1", "ref_2")'
|
|
1156
|
+
},
|
|
1157
|
+
value: {
|
|
1158
|
+
type: ["string", "boolean", "number"],
|
|
1159
|
+
description: "The value to set. For checkboxes use boolean, for selects use option value or text, for other inputs use appropriate string/number"
|
|
1160
|
+
},
|
|
1161
|
+
tabId: {
|
|
1162
|
+
type: "number",
|
|
1163
|
+
description: "Tab ID to set form value in. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
required: ["ref", "value", "tabId"]
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
name: "computer",
|
|
1171
|
+
description: `Use a mouse and keyboard to interact with a web browser, and take screenshots. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.
|
|
1172
|
+
* Whenever you intend to click on an element like an icon, you should consult a screenshot to determine the coordinates of the element before moving the cursor.
|
|
1173
|
+
* If you tried clicking on a program or link but it failed to load, even after waiting, try adjusting your click location so that the tip of the cursor visually falls on the element that you want to click.
|
|
1174
|
+
* Make sure to click any buttons, links, icons, etc with the cursor tip in the center of the element. Don't click boxes on their edges unless asked.`,
|
|
1175
|
+
inputSchema: {
|
|
1176
|
+
type: "object",
|
|
1177
|
+
properties: {
|
|
1178
|
+
action: {
|
|
1179
|
+
type: "string",
|
|
1180
|
+
enum: [
|
|
1181
|
+
"left_click",
|
|
1182
|
+
"right_click",
|
|
1183
|
+
"type",
|
|
1184
|
+
"screenshot",
|
|
1185
|
+
"wait",
|
|
1186
|
+
"scroll",
|
|
1187
|
+
"key",
|
|
1188
|
+
"left_click_drag",
|
|
1189
|
+
"double_click",
|
|
1190
|
+
"triple_click",
|
|
1191
|
+
"zoom",
|
|
1192
|
+
"scroll_to",
|
|
1193
|
+
"hover"
|
|
1194
|
+
],
|
|
1195
|
+
description: "The action to perform:\n* `left_click`: Click the left mouse button at the specified coordinates.\n* `right_click`: Click the right mouse button at the specified coordinates to open context menus.\n* `double_click`: Double-click the left mouse button at the specified coordinates.\n* `triple_click`: Triple-click the left mouse button at the specified coordinates.\n* `type`: Type a string of text.\n* `screenshot`: Take a screenshot of the screen.\n* `wait`: Wait for a specified number of seconds.\n* `scroll`: Scroll up, down, left, or right at the specified coordinates.\n* `key`: Press a specific keyboard key.\n* `left_click_drag`: Drag from start_coordinate to coordinate.\n* `zoom`: Take a screenshot of a specific region for closer inspection.\n* `scroll_to`: Scroll an element into view using its element reference ID from read_page or find tools.\n* `hover`: Move the mouse cursor to the specified coordinates or element without clicking. Useful for revealing tooltips, dropdown menus, or triggering hover states."
|
|
1196
|
+
},
|
|
1197
|
+
coordinate: {
|
|
1198
|
+
type: "array",
|
|
1199
|
+
items: { type: "number" },
|
|
1200
|
+
minItems: 2,
|
|
1201
|
+
maxItems: 2,
|
|
1202
|
+
description: "(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates. Required for `left_click`, `right_click`, `double_click`, `triple_click`, and `scroll`. For `left_click_drag`, this is the end position."
|
|
1203
|
+
},
|
|
1204
|
+
text: {
|
|
1205
|
+
type: "string",
|
|
1206
|
+
description: 'The text to type (for `type` action) or the key(s) to press (for `key` action). For `key` action: Provide space-separated keys (e.g., "Backspace Backspace Delete"). Supports keyboard shortcuts using the platform\'s modifier key (use "cmd" on Mac, "ctrl" on Windows/Linux, e.g., "cmd+a" or "ctrl+a" for select all).'
|
|
1207
|
+
},
|
|
1208
|
+
duration: {
|
|
1209
|
+
type: "number",
|
|
1210
|
+
minimum: 0,
|
|
1211
|
+
maximum: 30,
|
|
1212
|
+
description: "The number of seconds to wait. Required for `wait`. Maximum 30 seconds."
|
|
1213
|
+
},
|
|
1214
|
+
scroll_direction: {
|
|
1215
|
+
type: "string",
|
|
1216
|
+
enum: ["up", "down", "left", "right"],
|
|
1217
|
+
description: "The direction to scroll. Required for `scroll`."
|
|
1218
|
+
},
|
|
1219
|
+
scroll_amount: {
|
|
1220
|
+
type: "number",
|
|
1221
|
+
minimum: 1,
|
|
1222
|
+
maximum: 10,
|
|
1223
|
+
description: "The number of scroll wheel ticks. Optional for `scroll`, defaults to 3."
|
|
1224
|
+
},
|
|
1225
|
+
start_coordinate: {
|
|
1226
|
+
type: "array",
|
|
1227
|
+
items: { type: "number" },
|
|
1228
|
+
minItems: 2,
|
|
1229
|
+
maxItems: 2,
|
|
1230
|
+
description: "(x, y): The starting coordinates for `left_click_drag`."
|
|
1231
|
+
},
|
|
1232
|
+
region: {
|
|
1233
|
+
type: "array",
|
|
1234
|
+
items: { type: "number" },
|
|
1235
|
+
minItems: 4,
|
|
1236
|
+
maxItems: 4,
|
|
1237
|
+
description: "(x0, y0, x1, y1): The rectangular region to capture for `zoom`. Coordinates define a rectangle from top-left (x0, y0) to bottom-right (x1, y1) in pixels from the viewport origin. Required for `zoom` action. Useful for inspecting small UI elements like icons, buttons, or text."
|
|
1238
|
+
},
|
|
1239
|
+
repeat: {
|
|
1240
|
+
type: "number",
|
|
1241
|
+
minimum: 1,
|
|
1242
|
+
maximum: 100,
|
|
1243
|
+
description: "Number of times to repeat the key sequence. Only applicable for `key` action. Must be a positive integer between 1 and 100. Default is 1. Useful for navigation tasks like pressing arrow keys multiple times."
|
|
1244
|
+
},
|
|
1245
|
+
ref: {
|
|
1246
|
+
type: "string",
|
|
1247
|
+
description: 'Element reference ID from read_page or find tools (e.g., "ref_1", "ref_2"). Required for `scroll_to` action. Can be used as alternative to `coordinate` for click actions.'
|
|
1248
|
+
},
|
|
1249
|
+
modifiers: {
|
|
1250
|
+
type: "string",
|
|
1251
|
+
description: 'Modifier keys for click actions. Supports: "ctrl", "shift", "alt", "cmd" (or "meta"), "win" (or "windows"). Can be combined with "+" (e.g., "ctrl+shift", "cmd+alt"). Optional.'
|
|
1252
|
+
},
|
|
1253
|
+
tabId: {
|
|
1254
|
+
type: "number",
|
|
1255
|
+
description: "Tab ID to execute the action on. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
required: ["action", "tabId"]
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: "navigate",
|
|
1263
|
+
description: "Navigate to a URL, or go forward/back in browser history. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1264
|
+
inputSchema: {
|
|
1265
|
+
type: "object",
|
|
1266
|
+
properties: {
|
|
1267
|
+
url: {
|
|
1268
|
+
type: "string",
|
|
1269
|
+
description: 'The URL to navigate to. Can be provided with or without protocol (defaults to https://). Use "forward" to go forward in history or "back" to go back in history.'
|
|
1270
|
+
},
|
|
1271
|
+
tabId: {
|
|
1272
|
+
type: "number",
|
|
1273
|
+
description: "Tab ID to navigate. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
required: ["url", "tabId"]
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "resize_window",
|
|
1281
|
+
description: "Resize the current browser window to specified dimensions. Useful for testing responsive designs or setting up specific screen sizes. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1282
|
+
inputSchema: {
|
|
1283
|
+
type: "object",
|
|
1284
|
+
properties: {
|
|
1285
|
+
width: {
|
|
1286
|
+
type: "number",
|
|
1287
|
+
description: "Target window width in pixels"
|
|
1288
|
+
},
|
|
1289
|
+
height: {
|
|
1290
|
+
type: "number",
|
|
1291
|
+
description: "Target window height in pixels"
|
|
1292
|
+
},
|
|
1293
|
+
tabId: {
|
|
1294
|
+
type: "number",
|
|
1295
|
+
description: "Tab ID to get the window for. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
required: ["width", "height", "tabId"]
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
name: "gif_creator",
|
|
1303
|
+
description: "Manage GIF recording and export for browser automation sessions. Control when to start/stop recording browser actions (clicks, scrolls, navigation), then export as an animated GIF with visual overlays (click indicators, action labels, progress bar, watermark). All operations are scoped to the tab's group. When starting recording, take a screenshot immediately after to capture the initial state as the first frame. When stopping recording, take a screenshot immediately before to capture the final state as the last frame. For export, either provide 'coordinate' to drag/drop upload to a page element, or set 'download: true' to download the GIF.",
|
|
1304
|
+
inputSchema: {
|
|
1305
|
+
type: "object",
|
|
1306
|
+
properties: {
|
|
1307
|
+
action: {
|
|
1308
|
+
type: "string",
|
|
1309
|
+
enum: ["start_recording", "stop_recording", "export", "clear"],
|
|
1310
|
+
description: "Action to perform: 'start_recording' (begin capturing), 'stop_recording' (stop capturing but keep frames), 'export' (generate and export GIF), 'clear' (discard frames)"
|
|
1311
|
+
},
|
|
1312
|
+
tabId: {
|
|
1313
|
+
type: "number",
|
|
1314
|
+
description: "Tab ID to identify which tab group this operation applies to"
|
|
1315
|
+
},
|
|
1316
|
+
download: {
|
|
1317
|
+
type: "boolean",
|
|
1318
|
+
description: "Always set this to true for the 'export' action only. This causes the gif to be downloaded in the browser."
|
|
1319
|
+
},
|
|
1320
|
+
filename: {
|
|
1321
|
+
type: "string",
|
|
1322
|
+
description: "Optional filename for exported GIF (default: 'recording-[timestamp].gif'). For 'export' action only."
|
|
1323
|
+
},
|
|
1324
|
+
options: {
|
|
1325
|
+
type: "object",
|
|
1326
|
+
description: "Optional GIF enhancement options for 'export' action. Properties: showClickIndicators (bool), showDragPaths (bool), showActionLabels (bool), showProgressBar (bool), showWatermark (bool), quality (number 1-30). All default to true except quality (default: 10).",
|
|
1327
|
+
properties: {
|
|
1328
|
+
showClickIndicators: {
|
|
1329
|
+
type: "boolean",
|
|
1330
|
+
description: "Show orange circles at click locations (default: true)"
|
|
1331
|
+
},
|
|
1332
|
+
showDragPaths: {
|
|
1333
|
+
type: "boolean",
|
|
1334
|
+
description: "Show red arrows for drag actions (default: true)"
|
|
1335
|
+
},
|
|
1336
|
+
showActionLabels: {
|
|
1337
|
+
type: "boolean",
|
|
1338
|
+
description: "Show black labels describing actions (default: true)"
|
|
1339
|
+
},
|
|
1340
|
+
showProgressBar: {
|
|
1341
|
+
type: "boolean",
|
|
1342
|
+
description: "Show orange progress bar at bottom (default: true)"
|
|
1343
|
+
},
|
|
1344
|
+
showWatermark: {
|
|
1345
|
+
type: "boolean",
|
|
1346
|
+
description: "Show Claude logo watermark (default: true)"
|
|
1347
|
+
},
|
|
1348
|
+
quality: {
|
|
1349
|
+
type: "number",
|
|
1350
|
+
description: "GIF compression quality, 1-30 (lower = better quality, slower encoding). Default: 10"
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
required: ["action", "tabId"]
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
name: "upload_image",
|
|
1360
|
+
description: "Upload a previously captured screenshot or user-uploaded image to a file input or drag & drop target. Supports two approaches: (1) ref - for targeting specific elements, especially hidden file inputs, (2) coordinate - for drag & drop to visible locations like Google Docs. Provide either ref or coordinate, not both.",
|
|
1361
|
+
inputSchema: {
|
|
1362
|
+
type: "object",
|
|
1363
|
+
properties: {
|
|
1364
|
+
imageId: {
|
|
1365
|
+
type: "string",
|
|
1366
|
+
description: "ID of a previously captured screenshot (from the computer tool's screenshot action) or a user-uploaded image"
|
|
1367
|
+
},
|
|
1368
|
+
ref: {
|
|
1369
|
+
type: "string",
|
|
1370
|
+
description: 'Element reference ID from read_page or find tools (e.g., "ref_1", "ref_2"). Use this for file inputs (especially hidden ones) or specific elements. Provide either ref or coordinate, not both.'
|
|
1371
|
+
},
|
|
1372
|
+
coordinate: {
|
|
1373
|
+
type: "array",
|
|
1374
|
+
items: {
|
|
1375
|
+
type: "number"
|
|
1376
|
+
},
|
|
1377
|
+
description: "Viewport coordinates [x, y] for drag & drop to a visible location. Use this for drag & drop targets like Google Docs. Provide either ref or coordinate, not both."
|
|
1378
|
+
},
|
|
1379
|
+
tabId: {
|
|
1380
|
+
type: "number",
|
|
1381
|
+
description: "Tab ID where the target element is located. This is where the image will be uploaded to."
|
|
1382
|
+
},
|
|
1383
|
+
filename: {
|
|
1384
|
+
type: "string",
|
|
1385
|
+
description: 'Optional filename for the uploaded file (default: "image.png")'
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
required: ["imageId", "tabId"]
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
name: "get_page_text",
|
|
1393
|
+
description: "Extract raw text content from the page, prioritizing article content. Ideal for reading articles, blog posts, or other text-heavy pages. Returns plain text without HTML formatting. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
type: "object",
|
|
1396
|
+
properties: {
|
|
1397
|
+
tabId: {
|
|
1398
|
+
type: "number",
|
|
1399
|
+
description: "Tab ID to extract text from. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
required: ["tabId"]
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
name: "tabs_context_mcp",
|
|
1407
|
+
title: "Tabs Context",
|
|
1408
|
+
description: "Get context information about the current MCP tab group. Returns all tab IDs inside the group if it exists. CRITICAL: You must get the context at least once before using other browser automation tools so you know what tabs exist. Each new conversation should create its own new tab (using tabs_create_mcp) rather than reusing existing tabs, unless the user explicitly asks to use an existing tab.",
|
|
1409
|
+
inputSchema: {
|
|
1410
|
+
type: "object",
|
|
1411
|
+
properties: {
|
|
1412
|
+
createIfEmpty: {
|
|
1413
|
+
type: "boolean",
|
|
1414
|
+
description: "Creates a new MCP tab group if none exists, creates a new Window with a new tab group containing an empty tab (which can be used for this conversation). If a MCP tab group already exists, this parameter has no effect."
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
required: []
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
name: "tabs_create_mcp",
|
|
1422
|
+
title: "Tabs Create",
|
|
1423
|
+
description: "Creates a new empty tab in the MCP tab group. CRITICAL: You must get the context using tabs_context_mcp at least once before using other browser automation tools so you know what tabs exist.",
|
|
1424
|
+
inputSchema: {
|
|
1425
|
+
type: "object",
|
|
1426
|
+
properties: {},
|
|
1427
|
+
required: []
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
name: "update_plan",
|
|
1432
|
+
description: "Present a plan to the user for approval before taking actions. The user will see the domains you intend to visit and your approach. Once approved, you can proceed with actions on the approved domains without additional permission prompts.",
|
|
1433
|
+
inputSchema: {
|
|
1434
|
+
type: "object",
|
|
1435
|
+
properties: {
|
|
1436
|
+
domains: {
|
|
1437
|
+
type: "array",
|
|
1438
|
+
items: { type: "string" },
|
|
1439
|
+
description: "List of domains you will visit (e.g., ['github.com', 'stackoverflow.com']). These domains will be approved for the session when the user accepts the plan."
|
|
1440
|
+
},
|
|
1441
|
+
approach: {
|
|
1442
|
+
type: "array",
|
|
1443
|
+
items: { type: "string" },
|
|
1444
|
+
description: "High-level description of what you will do. Focus on outcomes and key actions, not implementation details. Be concise - aim for 3-7 items."
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
required: ["domains", "approach"]
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: "read_console_messages",
|
|
1452
|
+
description: "Read browser console messages (console.log, console.error, console.warn, etc.) from a specific tab. Useful for debugging JavaScript errors, viewing application logs, or understanding what's happening in the browser console. Returns console messages from the current domain only. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs. IMPORTANT: Always provide a pattern to filter messages - without a pattern, you may get too many irrelevant messages.",
|
|
1453
|
+
inputSchema: {
|
|
1454
|
+
type: "object",
|
|
1455
|
+
properties: {
|
|
1456
|
+
tabId: {
|
|
1457
|
+
type: "number",
|
|
1458
|
+
description: "Tab ID to read console messages from. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1459
|
+
},
|
|
1460
|
+
onlyErrors: {
|
|
1461
|
+
type: "boolean",
|
|
1462
|
+
description: "If true, only return error and exception messages. Default is false (return all message types)."
|
|
1463
|
+
},
|
|
1464
|
+
clear: {
|
|
1465
|
+
type: "boolean",
|
|
1466
|
+
description: "If true, clear the console messages after reading to avoid duplicates on subsequent calls. Default is false."
|
|
1467
|
+
},
|
|
1468
|
+
pattern: {
|
|
1469
|
+
type: "string",
|
|
1470
|
+
description: "Regex pattern to filter console messages. Only messages matching this pattern will be returned (e.g., 'error|warning' to find errors and warnings, 'MyApp' to filter app-specific logs). You should always provide a pattern to avoid getting too many irrelevant messages."
|
|
1471
|
+
},
|
|
1472
|
+
limit: {
|
|
1473
|
+
type: "number",
|
|
1474
|
+
description: "Maximum number of messages to return. Defaults to 100. Increase only if you need more results."
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
required: ["tabId"]
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
name: "read_network_requests",
|
|
1482
|
+
description: "Read HTTP network requests (XHR, Fetch, documents, images, etc.) from a specific tab. Useful for debugging API calls, monitoring network activity, or understanding what requests a page is making. Returns all network requests made by the current page, including cross-origin requests. Requests are automatically cleared when the page navigates to a different domain. If you don't have a valid tab ID, use tabs_context_mcp first to get available tabs.",
|
|
1483
|
+
inputSchema: {
|
|
1484
|
+
type: "object",
|
|
1485
|
+
properties: {
|
|
1486
|
+
tabId: {
|
|
1487
|
+
type: "number",
|
|
1488
|
+
description: "Tab ID to read network requests from. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1489
|
+
},
|
|
1490
|
+
urlPattern: {
|
|
1491
|
+
type: "string",
|
|
1492
|
+
description: "Optional URL pattern to filter requests. Only requests whose URL contains this string will be returned (e.g., '/api/' to filter API calls, 'example.com' to filter by domain)."
|
|
1493
|
+
},
|
|
1494
|
+
clear: {
|
|
1495
|
+
type: "boolean",
|
|
1496
|
+
description: "If true, clear the network requests after reading to avoid duplicates on subsequent calls. Default is false."
|
|
1497
|
+
},
|
|
1498
|
+
limit: {
|
|
1499
|
+
type: "number",
|
|
1500
|
+
description: "Maximum number of requests to return. Defaults to 100. Increase only if you need more results."
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
required: ["tabId"]
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "shortcuts_list",
|
|
1508
|
+
description: "List all available shortcuts and workflows (shortcuts and workflows are interchangeable). Returns shortcuts with their commands, descriptions, and whether they are workflows. Use shortcuts_execute to run a shortcut or workflow.",
|
|
1509
|
+
inputSchema: {
|
|
1510
|
+
type: "object",
|
|
1511
|
+
properties: {
|
|
1512
|
+
tabId: {
|
|
1513
|
+
type: "number",
|
|
1514
|
+
description: "Tab ID to list shortcuts from. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
required: ["tabId"]
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
name: "shortcuts_execute",
|
|
1522
|
+
description: "Execute a shortcut or workflow by running it in a new sidepanel window using the current tab (shortcuts and workflows are interchangeable). Use shortcuts_list first to see available shortcuts. This starts the execution and returns immediately - it does not wait for completion.",
|
|
1523
|
+
inputSchema: {
|
|
1524
|
+
type: "object",
|
|
1525
|
+
properties: {
|
|
1526
|
+
tabId: {
|
|
1527
|
+
type: "number",
|
|
1528
|
+
description: "Tab ID to execute the shortcut on. Must be a tab in the current group. Use tabs_context_mcp first if you don't have a valid tab ID."
|
|
1529
|
+
},
|
|
1530
|
+
shortcutId: {
|
|
1531
|
+
type: "string",
|
|
1532
|
+
description: "The ID of the shortcut to execute"
|
|
1533
|
+
},
|
|
1534
|
+
command: {
|
|
1535
|
+
type: "string",
|
|
1536
|
+
description: "The command name of the shortcut to execute (e.g., 'debug', 'summarize'). Do not include the leading slash."
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
required: ["tabId"]
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
name: "switch_browser",
|
|
1544
|
+
description: "Switch which Chrome browser is used for browser automation. Call this when the user wants to connect to a different Chrome browser. Broadcasts a connection request to all Chrome browsers with the extension installed \u2014 the user clicks 'Connect' in the desired browser.",
|
|
1545
|
+
inputSchema: {
|
|
1546
|
+
type: "object",
|
|
1547
|
+
properties: {},
|
|
1548
|
+
required: []
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
];
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// packages/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts
|
|
1555
|
+
class McpSocketPool {
|
|
1556
|
+
clients = new Map;
|
|
1557
|
+
tabRoutes = new Map;
|
|
1558
|
+
context;
|
|
1559
|
+
notificationHandler = null;
|
|
1560
|
+
constructor(context) {
|
|
1561
|
+
this.context = context;
|
|
1562
|
+
}
|
|
1563
|
+
setNotificationHandler(handler) {
|
|
1564
|
+
this.notificationHandler = handler;
|
|
1565
|
+
for (const client of this.clients.values()) {
|
|
1566
|
+
client.setNotificationHandler(handler);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async ensureConnected() {
|
|
1570
|
+
const { logger, serverName } = this.context;
|
|
1571
|
+
this.refreshClients();
|
|
1572
|
+
const connectPromises = [];
|
|
1573
|
+
for (const client of this.clients.values()) {
|
|
1574
|
+
if (!client.isConnected()) {
|
|
1575
|
+
connectPromises.push(client.ensureConnected().catch(() => false));
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (connectPromises.length > 0) {
|
|
1579
|
+
await Promise.all(connectPromises);
|
|
1580
|
+
}
|
|
1581
|
+
const connectedCount = this.getConnectedClients().length;
|
|
1582
|
+
if (connectedCount === 0) {
|
|
1583
|
+
logger.info(`[${serverName}] No connected sockets in pool`);
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
logger.info(`[${serverName}] Socket pool: ${connectedCount} connected`);
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
async callTool(name, args, _permissionOverrides) {
|
|
1590
|
+
if (name === "tabs_context_mcp") {
|
|
1591
|
+
return this.callTabsContext(args);
|
|
1592
|
+
}
|
|
1593
|
+
const tabId = args.tabId;
|
|
1594
|
+
if (tabId !== undefined) {
|
|
1595
|
+
const socketPath = this.tabRoutes.get(tabId);
|
|
1596
|
+
if (socketPath) {
|
|
1597
|
+
const client = this.clients.get(socketPath);
|
|
1598
|
+
if (client?.isConnected()) {
|
|
1599
|
+
return client.callTool(name, args);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const connected = this.getConnectedClients();
|
|
1604
|
+
if (connected.length === 0) {
|
|
1605
|
+
throw new SocketConnectionError(`[${this.context.serverName}] No connected sockets available`);
|
|
1606
|
+
}
|
|
1607
|
+
return connected[0].callTool(name, args);
|
|
1608
|
+
}
|
|
1609
|
+
async setPermissionMode(mode, allowedDomains) {
|
|
1610
|
+
const connected = this.getConnectedClients();
|
|
1611
|
+
await Promise.all(connected.map((client) => client.setPermissionMode(mode, allowedDomains)));
|
|
1612
|
+
}
|
|
1613
|
+
isConnected() {
|
|
1614
|
+
return this.getConnectedClients().length > 0;
|
|
1615
|
+
}
|
|
1616
|
+
disconnect() {
|
|
1617
|
+
for (const client of this.clients.values()) {
|
|
1618
|
+
client.disconnect();
|
|
1619
|
+
}
|
|
1620
|
+
this.clients.clear();
|
|
1621
|
+
this.tabRoutes.clear();
|
|
1622
|
+
}
|
|
1623
|
+
getConnectedClients() {
|
|
1624
|
+
return [...this.clients.values()].filter((c) => c.isConnected());
|
|
1625
|
+
}
|
|
1626
|
+
async callTabsContext(args) {
|
|
1627
|
+
const { logger, serverName } = this.context;
|
|
1628
|
+
const connected = this.getConnectedClients();
|
|
1629
|
+
if (connected.length === 0) {
|
|
1630
|
+
throw new SocketConnectionError(`[${serverName}] No connected sockets available`);
|
|
1631
|
+
}
|
|
1632
|
+
if (connected.length === 1) {
|
|
1633
|
+
const result = await connected[0].callTool("tabs_context_mcp", args);
|
|
1634
|
+
this.updateTabRoutes(result, this.getSocketPathForClient(connected[0]));
|
|
1635
|
+
return result;
|
|
1636
|
+
}
|
|
1637
|
+
const results = await Promise.allSettled(connected.map(async (client) => {
|
|
1638
|
+
const result = await client.callTool("tabs_context_mcp", args);
|
|
1639
|
+
const socketPath = this.getSocketPathForClient(client);
|
|
1640
|
+
return { result, socketPath };
|
|
1641
|
+
}));
|
|
1642
|
+
const mergedTabs = [];
|
|
1643
|
+
this.tabRoutes.clear();
|
|
1644
|
+
for (const settledResult of results) {
|
|
1645
|
+
if (settledResult.status !== "fulfilled") {
|
|
1646
|
+
logger.info(`[${serverName}] tabs_context_mcp failed on one socket: ${settledResult.reason}`);
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const { result, socketPath } = settledResult.value;
|
|
1650
|
+
this.updateTabRoutes(result, socketPath);
|
|
1651
|
+
const tabs = this.extractTabs(result);
|
|
1652
|
+
if (tabs) {
|
|
1653
|
+
mergedTabs.push(...tabs);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (mergedTabs.length > 0) {
|
|
1657
|
+
const tabListText = mergedTabs.map((t) => {
|
|
1658
|
+
const tab = t;
|
|
1659
|
+
return ` \u2022 tabId ${tab.tabId}: "${tab.title}" (${tab.url})`;
|
|
1660
|
+
}).join(`
|
|
1661
|
+
`);
|
|
1662
|
+
return {
|
|
1663
|
+
result: {
|
|
1664
|
+
content: [
|
|
1665
|
+
{
|
|
1666
|
+
type: "text",
|
|
1667
|
+
text: JSON.stringify({ availableTabs: mergedTabs })
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
type: "text",
|
|
1671
|
+
text: `
|
|
1672
|
+
|
|
1673
|
+
Tab Context:
|
|
1674
|
+
- Available tabs:
|
|
1675
|
+
${tabListText}`
|
|
1676
|
+
}
|
|
1677
|
+
]
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
for (const settledResult of results) {
|
|
1682
|
+
if (settledResult.status === "fulfilled") {
|
|
1683
|
+
return settledResult.value.result;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
throw new SocketConnectionError(`[${serverName}] All sockets failed for tabs_context_mcp`);
|
|
1687
|
+
}
|
|
1688
|
+
updateTabRoutes(result, socketPath) {
|
|
1689
|
+
const tabs = this.extractTabs(result);
|
|
1690
|
+
if (!tabs)
|
|
1691
|
+
return;
|
|
1692
|
+
for (const tab of tabs) {
|
|
1693
|
+
if (typeof tab === "object" && tab !== null && "tabId" in tab) {
|
|
1694
|
+
const tabId = tab.tabId;
|
|
1695
|
+
this.tabRoutes.set(tabId, socketPath);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
extractTabs(result) {
|
|
1700
|
+
if (!result || typeof result !== "object")
|
|
1701
|
+
return null;
|
|
1702
|
+
const asResponse = result;
|
|
1703
|
+
const content = asResponse.result?.content;
|
|
1704
|
+
if (!content || !Array.isArray(content))
|
|
1705
|
+
return null;
|
|
1706
|
+
for (const item of content) {
|
|
1707
|
+
if (item.type === "text" && item.text) {
|
|
1708
|
+
try {
|
|
1709
|
+
const parsed = JSON.parse(item.text);
|
|
1710
|
+
if (Array.isArray(parsed))
|
|
1711
|
+
return parsed;
|
|
1712
|
+
if (parsed && Array.isArray(parsed.availableTabs)) {
|
|
1713
|
+
return parsed.availableTabs;
|
|
1714
|
+
}
|
|
1715
|
+
} catch {}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
getSocketPathForClient(client) {
|
|
1721
|
+
for (const [path, c] of this.clients.entries()) {
|
|
1722
|
+
if (c === client)
|
|
1723
|
+
return path;
|
|
1724
|
+
}
|
|
1725
|
+
return "";
|
|
1726
|
+
}
|
|
1727
|
+
refreshClients() {
|
|
1728
|
+
const socketPaths = this.getAvailableSocketPaths();
|
|
1729
|
+
const { logger, serverName } = this.context;
|
|
1730
|
+
for (const path of socketPaths) {
|
|
1731
|
+
if (!this.clients.has(path)) {
|
|
1732
|
+
logger.info(`[${serverName}] Adding socket to pool: ${path}`);
|
|
1733
|
+
const clientContext = {
|
|
1734
|
+
...this.context,
|
|
1735
|
+
socketPath: path,
|
|
1736
|
+
getSocketPath: undefined,
|
|
1737
|
+
getSocketPaths: undefined
|
|
1738
|
+
};
|
|
1739
|
+
const client = createMcpSocketClient(clientContext);
|
|
1740
|
+
client.disableAutoReconnect = true;
|
|
1741
|
+
if (this.notificationHandler) {
|
|
1742
|
+
client.setNotificationHandler(this.notificationHandler);
|
|
1743
|
+
}
|
|
1744
|
+
this.clients.set(path, client);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
for (const [path, client] of this.clients.entries()) {
|
|
1748
|
+
if (!socketPaths.includes(path)) {
|
|
1749
|
+
logger.info(`[${serverName}] Removing stale socket from pool: ${path}`);
|
|
1750
|
+
client.disconnect();
|
|
1751
|
+
this.clients.delete(path);
|
|
1752
|
+
for (const [tabId, socketPath] of this.tabRoutes.entries()) {
|
|
1753
|
+
if (socketPath === path) {
|
|
1754
|
+
this.tabRoutes.delete(tabId);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
getAvailableSocketPaths() {
|
|
1761
|
+
return this.context.getSocketPaths?.() ?? [];
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function createMcpSocketPool(context) {
|
|
1765
|
+
return new McpSocketPool(context);
|
|
1766
|
+
}
|
|
1767
|
+
var init_mcpSocketPool = __esm(() => {
|
|
1768
|
+
init_mcpSocketClient();
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
// packages/@ant/claude-for-chrome-mcp/src/toolCalls.ts
|
|
1772
|
+
async function handleToolCallConnected(context, socketClient, name, args, permissionOverrides) {
|
|
1773
|
+
const response = await socketClient.callTool(name, args, permissionOverrides);
|
|
1774
|
+
context.logger.silly(`[${context.serverName}] Received result from socket bridge: ${JSON.stringify(response)}`);
|
|
1775
|
+
if (response === null || response === undefined) {
|
|
1776
|
+
return {
|
|
1777
|
+
content: [{ type: "text", text: "Tool execution completed" }]
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
const { result, error } = response;
|
|
1781
|
+
const contentData = error || result;
|
|
1782
|
+
const isError = !!error;
|
|
1783
|
+
if (!contentData) {
|
|
1784
|
+
return {
|
|
1785
|
+
content: [{ type: "text", text: "Tool execution completed" }]
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (isError && isAuthenticationError(contentData.content)) {
|
|
1789
|
+
context.onAuthenticationError();
|
|
1790
|
+
}
|
|
1791
|
+
const { content } = contentData;
|
|
1792
|
+
if (content && Array.isArray(content)) {
|
|
1793
|
+
if (isError) {
|
|
1794
|
+
return {
|
|
1795
|
+
content: content.map((item) => {
|
|
1796
|
+
if (typeof item === "object" && item !== null && "type" in item) {
|
|
1797
|
+
return item;
|
|
1798
|
+
}
|
|
1799
|
+
return { type: "text", text: String(item) };
|
|
1800
|
+
}),
|
|
1801
|
+
isError: true
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const convertedContent = content.map((item) => {
|
|
1805
|
+
if (typeof item === "object" && item !== null && "type" in item && "source" in item) {
|
|
1806
|
+
const typedItem = item;
|
|
1807
|
+
if (typedItem.type === "image" && typeof typedItem.source === "object" && typedItem.source !== null && "data" in typedItem.source) {
|
|
1808
|
+
return {
|
|
1809
|
+
type: "image",
|
|
1810
|
+
data: typedItem.source.data,
|
|
1811
|
+
mimeType: "media_type" in typedItem.source ? typedItem.source.media_type || "image/png" : "image/png"
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (typeof item === "object" && item !== null && "type" in item) {
|
|
1816
|
+
return item;
|
|
1817
|
+
}
|
|
1818
|
+
return { type: "text", text: String(item) };
|
|
1819
|
+
});
|
|
1820
|
+
return {
|
|
1821
|
+
content: convertedContent,
|
|
1822
|
+
isError
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (typeof content === "string") {
|
|
1826
|
+
return {
|
|
1827
|
+
content: [{ type: "text", text: content }],
|
|
1828
|
+
isError
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
context.logger.warn(`[${context.serverName}] Unexpected result format from socket bridge`, response);
|
|
1832
|
+
return {
|
|
1833
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1834
|
+
isError
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
function handleToolCallDisconnected(context) {
|
|
1838
|
+
const text = context.onToolCallDisconnected();
|
|
1839
|
+
return {
|
|
1840
|
+
content: [{ type: "text", text }]
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
async function handleSetPermissionMode(socketClient, args) {
|
|
1844
|
+
const validModes = [
|
|
1845
|
+
"ask",
|
|
1846
|
+
"skip_all_permission_checks",
|
|
1847
|
+
"follow_a_plan"
|
|
1848
|
+
];
|
|
1849
|
+
const mode = args.mode;
|
|
1850
|
+
const permissionMode = mode && validModes.includes(mode) ? mode : "ask";
|
|
1851
|
+
if (socketClient.setPermissionMode) {
|
|
1852
|
+
await socketClient.setPermissionMode(permissionMode, args.allowed_domains);
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
content: [
|
|
1856
|
+
{ type: "text", text: `Permission mode set to: ${permissionMode}` }
|
|
1857
|
+
]
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
async function handleSwitchBrowser(context, socketClient) {
|
|
1861
|
+
if (!context.bridgeConfig) {
|
|
1862
|
+
return {
|
|
1863
|
+
content: [
|
|
1864
|
+
{
|
|
1865
|
+
type: "text",
|
|
1866
|
+
text: "Browser switching is only available with bridge connections."
|
|
1867
|
+
}
|
|
1868
|
+
],
|
|
1869
|
+
isError: true
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
const isConnected = await socketClient.ensureConnected();
|
|
1873
|
+
if (!isConnected) {
|
|
1874
|
+
return handleToolCallDisconnected(context);
|
|
1875
|
+
}
|
|
1876
|
+
const result = await socketClient.switchBrowser?.() ?? null;
|
|
1877
|
+
if (result === "no_other_browsers") {
|
|
1878
|
+
return {
|
|
1879
|
+
content: [
|
|
1880
|
+
{
|
|
1881
|
+
type: "text",
|
|
1882
|
+
text: "No other browsers available to switch to. Open Chrome with the Claude extension in another browser to switch."
|
|
1883
|
+
}
|
|
1884
|
+
],
|
|
1885
|
+
isError: true
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
if (result) {
|
|
1889
|
+
return {
|
|
1890
|
+
content: [
|
|
1891
|
+
{ type: "text", text: `Connected to browser "${result.name}".` }
|
|
1892
|
+
]
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
return {
|
|
1896
|
+
content: [
|
|
1897
|
+
{
|
|
1898
|
+
type: "text",
|
|
1899
|
+
text: "No browser responded within the timeout. Make sure Chrome is open with the Claude extension installed, then try again."
|
|
1900
|
+
}
|
|
1901
|
+
],
|
|
1902
|
+
isError: true
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function isAuthenticationError(content) {
|
|
1906
|
+
const errorText = Array.isArray(content) ? content.map((item) => {
|
|
1907
|
+
if (typeof item === "string")
|
|
1908
|
+
return item;
|
|
1909
|
+
if (typeof item === "object" && item !== null && "text" in item && typeof item.text === "string") {
|
|
1910
|
+
return item.text;
|
|
1911
|
+
}
|
|
1912
|
+
return "";
|
|
1913
|
+
}).join(" ") : String(content);
|
|
1914
|
+
return errorText.toLowerCase().includes("re-authenticated");
|
|
1915
|
+
}
|
|
1916
|
+
var handleToolCall = async (context, socketClient, name, args, permissionOverrides) => {
|
|
1917
|
+
if (name === "set_permission_mode") {
|
|
1918
|
+
return handleSetPermissionMode(socketClient, args);
|
|
1919
|
+
}
|
|
1920
|
+
if (name === "switch_browser") {
|
|
1921
|
+
return handleSwitchBrowser(context, socketClient);
|
|
1922
|
+
}
|
|
1923
|
+
try {
|
|
1924
|
+
const isConnected = await socketClient.ensureConnected();
|
|
1925
|
+
context.logger.silly(`[${context.serverName}] Server is connected: ${isConnected}. Received tool call: ${name} with args: ${JSON.stringify(args)}.`);
|
|
1926
|
+
if (isConnected) {
|
|
1927
|
+
return await handleToolCallConnected(context, socketClient, name, args, permissionOverrides);
|
|
1928
|
+
}
|
|
1929
|
+
return handleToolCallDisconnected(context);
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
context.logger.info(`[${context.serverName}] Error calling tool:`, error);
|
|
1932
|
+
if (error instanceof SocketConnectionError) {
|
|
1933
|
+
return handleToolCallDisconnected(context);
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
content: [
|
|
1937
|
+
{
|
|
1938
|
+
type: "text",
|
|
1939
|
+
text: `Error calling tool, please try again. : ${error instanceof Error ? error.message : String(error)}`
|
|
1940
|
+
}
|
|
1941
|
+
],
|
|
1942
|
+
isError: true
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
var init_toolCalls = __esm(() => {
|
|
1947
|
+
init_mcpSocketClient();
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
// packages/@ant/claude-for-chrome-mcp/src/mcpServer.ts
|
|
1951
|
+
function createChromeSocketClient(context) {
|
|
1952
|
+
return context.bridgeConfig ? createBridgeClient(context) : context.getSocketPaths ? createMcpSocketPool(context) : createMcpSocketClient(context);
|
|
1953
|
+
}
|
|
1954
|
+
function createClaudeForChromeMcpServer(context, existingSocketClient) {
|
|
1955
|
+
const { serverName, logger } = context;
|
|
1956
|
+
const socketClient = existingSocketClient ?? createChromeSocketClient(context);
|
|
1957
|
+
const server = new Server({
|
|
1958
|
+
name: serverName,
|
|
1959
|
+
version: "1.0.0"
|
|
1960
|
+
}, {
|
|
1961
|
+
capabilities: {
|
|
1962
|
+
tools: {},
|
|
1963
|
+
logging: {}
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1967
|
+
if (context.isDisabled?.()) {
|
|
1968
|
+
return { tools: [] };
|
|
1969
|
+
}
|
|
1970
|
+
return {
|
|
1971
|
+
tools: context.bridgeConfig ? BROWSER_TOOLS : BROWSER_TOOLS.filter((t) => t.name !== "switch_browser")
|
|
1972
|
+
};
|
|
1973
|
+
});
|
|
1974
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1975
|
+
logger.info(`[${serverName}] Executing tool: ${request.params.name}`);
|
|
1976
|
+
return handleToolCall(context, socketClient, request.params.name, request.params.arguments || {});
|
|
1977
|
+
});
|
|
1978
|
+
socketClient.setNotificationHandler((notification) => {
|
|
1979
|
+
logger.info(`[${serverName}] Forwarding MCP notification: ${notification.method}`);
|
|
1980
|
+
server.notification({
|
|
1981
|
+
method: notification.method,
|
|
1982
|
+
params: notification.params
|
|
1983
|
+
}).catch((error) => {
|
|
1984
|
+
logger.info(`[${serverName}] Failed to forward MCP notification: ${error.message}`);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
return server;
|
|
1988
|
+
}
|
|
1989
|
+
var init_mcpServer = __esm(() => {
|
|
1990
|
+
init_server();
|
|
1991
|
+
init_types();
|
|
1992
|
+
init_bridgeClient();
|
|
1993
|
+
init_browserTools();
|
|
1994
|
+
init_mcpSocketClient();
|
|
1995
|
+
init_mcpSocketPool();
|
|
1996
|
+
init_toolCalls();
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
// packages/@ant/claude-for-chrome-mcp/src/index.ts
|
|
2000
|
+
var init_src = __esm(() => {
|
|
2001
|
+
init_bridgeClient();
|
|
2002
|
+
init_browserTools();
|
|
2003
|
+
init_mcpServer();
|
|
2004
|
+
init_types2();
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
export { localPlatformLabel, BridgeClient, createBridgeClient, BROWSER_TOOLS, createChromeSocketClient, createClaudeForChromeMcpServer, init_src };
|