@fangorn-network/westmarch 0.0.20-dev → 2026.10.2-2.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +33 -3
  2. package/docs/app-to-agent.md +117 -60
  3. package/package.json +10 -5
  4. package/plugin/scripts/fx.mjs +7 -9
  5. package/site/dist/assets/{AccountNotFoundScreen-C4njXc69-BOfmiSH1.js → AccountNotFoundScreen-C4njXc69-DyVH4CEz.js} +1 -1
  6. package/site/dist/assets/{AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js → AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js} +1 -1
  7. package/site/dist/assets/{Address-H8kD1d9O-C85sJ79B.js → Address-H8kD1d9O-BdFZmRwI.js} +1 -1
  8. package/site/dist/assets/{AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js → AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js} +1 -1
  9. package/site/dist/assets/{AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js → AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js} +1 -1
  10. package/site/dist/assets/{ApiController-CYdiyu9B.js → ApiController-CTLbMCcH.js} +4 -4
  11. package/site/dist/assets/{ArrowsRightLeftIcon-cp60yobq.js → ArrowsRightLeftIcon-B8g9adpz.js} +1 -1
  12. package/site/dist/assets/{AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js → AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js} +1 -1
  13. package/site/dist/assets/{AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js → AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js} +1 -1
  14. package/site/dist/assets/{AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js → AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js} +1 -1
  15. package/site/dist/assets/{AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js → AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js} +1 -1
  16. package/site/dist/assets/{AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js → AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js} +1 -1
  17. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +68 -0
  18. package/site/dist/assets/{CaptchaScreen-DbpvkIGx-D8eDcAkc.js → CaptchaScreen-DbpvkIGx-7rSZIuM-.js} +1 -1
  19. package/site/dist/assets/{CheckCircleIcon-NCOQc0Gs.js → CheckCircleIcon-B1l17ioc.js} +1 -1
  20. package/site/dist/assets/{CheckCircleIcon-BM409mGI.js → CheckCircleIcon-BGSI8nKf.js} +1 -1
  21. package/site/dist/assets/{Checkbox-D1EDeo41-rHlvsZzP.js → Checkbox-D1EDeo41-BUfq0w99.js} +1 -1
  22. package/site/dist/assets/{ChevronDownIcon-CW-4MSpE.js → ChevronDownIcon-B6uJvPHu.js} +1 -1
  23. package/site/dist/assets/{Chip-CZKIKt9K-CutJG010.js → Chip-CZKIKt9K-r9Nk__0-.js} +1 -1
  24. package/site/dist/assets/{CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js → CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js} +1 -1
  25. package/site/dist/assets/{ConnectEmailForm-B-VRE0MN-N9J6T_YL.js → ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js} +1 -1
  26. package/site/dist/assets/{ConnectLedgerScreen-DWHABGot-h0uYIUi6.js → ConnectLedgerScreen-DWHABGot-CZcCt31O.js} +1 -1
  27. package/site/dist/assets/{ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js → ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js} +1 -1
  28. package/site/dist/assets/{ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js → ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js} +1 -1
  29. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +1 -0
  30. package/site/dist/assets/{ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js → ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js} +1 -1
  31. package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +74 -0
  32. package/site/dist/assets/{ConnectionStatusScreen-fUfROm9m-BeWLchcf.js → ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js} +1 -1
  33. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +1 -0
  34. package/site/dist/assets/{ConnectorUtil-Cq4lWnfe.js → ConnectorUtil-DH4NePk5.js} +1 -1
  35. package/site/dist/assets/{CopyToClipboard-i_OQSBJr-DujUHNzW.js → CopyToClipboard-i_OQSBJr-_G88JCVR.js} +1 -1
  36. package/site/dist/assets/{CopyableText-CQapvaMr-DMIdpDaU.js → CopyableText-CQapvaMr-CGGfc_zM.js} +1 -1
  37. package/site/dist/assets/{CrossAppAuthScreen-CLw7ymZq-Csju14vF.js → CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js} +1 -1
  38. package/site/dist/assets/{CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js → CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js} +1 -1
  39. package/site/dist/assets/{DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js → DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js} +1 -1
  40. package/site/dist/assets/{DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js → DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js} +1 -1
  41. package/site/dist/assets/{DepositAddressAddressStepView-4vdkb55L-B3vY888G.js → DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js} +1 -1
  42. package/site/dist/assets/{DepositAddressScreen-DBLRs7aI-DmdQaFI2.js → DepositAddressScreen-DBLRs7aI-DeituDNh.js} +1 -1
  43. package/site/dist/assets/{DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js → DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js} +1 -1
  44. package/site/dist/assets/{EmailInputForm-Cqo1mda8-B2NG3K5V.js → EmailInputForm-Cqo1mda8-BmbxvPHC.js} +1 -1
  45. package/site/dist/assets/{EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js → EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js} +1 -1
  46. package/site/dist/assets/{EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js → EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js} +1 -1
  47. package/site/dist/assets/{EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js → EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js} +1 -1
  48. package/site/dist/assets/{EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js → EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js} +1 -1
  49. package/site/dist/assets/{EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js → EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js} +1 -1
  50. package/site/dist/assets/{EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js → EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js} +1 -1
  51. package/site/dist/assets/{EnrollTotp-Dibew4dp-SA2pvNEc.js → EnrollTotp-Dibew4dp-BXbYV8nI.js} +1 -1
  52. package/site/dist/assets/{EnvelopeIcon-Dh2vSVj4.js → EnvelopeIcon-DjPofnRI.js} +1 -1
  53. package/site/dist/assets/{ErrorBanner-BcpGRt0h-BKOYTyCi.js → ErrorBanner-BcpGRt0h-BhH6_V-9.js} +1 -1
  54. package/site/dist/assets/{ErrorMessage-D8VaAP5m-CrZOnUyg.js → ErrorMessage-D8VaAP5m-Bs7uX2a9.js} +1 -1
  55. package/site/dist/assets/{ErrorScreen-DipdTWzE-c8g8En21.js → ErrorScreen-DipdTWzE-BgPRgnOb.js} +1 -1
  56. package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +1 -0
  57. package/site/dist/assets/{ExclamationCircleIcon-DUA2wMfO.js → ExclamationCircleIcon-BE-WGrut.js} +1 -1
  58. package/site/dist/assets/{ExclamationTriangleIcon-eZfaqcFq.js → ExclamationTriangleIcon-Ca1Xlc_f.js} +1 -1
  59. package/site/dist/assets/{FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js → FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js} +1 -1
  60. package/site/dist/assets/{FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js → FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js} +1 -1
  61. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +491 -0
  62. package/site/dist/assets/{FingerPrintIcon-Dm6kxmg7.js → FingerPrintIcon-DOCbS93y.js} +1 -1
  63. package/site/dist/assets/{FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js → FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js} +1 -1
  64. package/site/dist/assets/{FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js → FundWalletMethodHeader-BWNkokCT-LPv35wkD.js} +1 -1
  65. package/site/dist/assets/{FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js → FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js} +1 -1
  66. package/site/dist/assets/{FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js → FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js} +1 -1
  67. package/site/dist/assets/{FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js → FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js} +1 -1
  68. package/site/dist/assets/{GlobeAltIcon-vqrVV5ho.js → GlobeAltIcon-BBRl2MMn.js} +1 -1
  69. package/site/dist/assets/{GooglePay-B53WnudL-BX5_WtCx.js → GooglePay-B53WnudL-BGiVpB8y.js} +1 -1
  70. package/site/dist/assets/{HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js → HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js} +3 -3
  71. package/site/dist/assets/{InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js → InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js} +1 -1
  72. package/site/dist/assets/{InfoBanner-Cb3p1z12-DLBl_Ywy.js → InfoBanner-Cb3p1z12-_YqQmRiu.js} +1 -1
  73. package/site/dist/assets/{InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js → InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js} +1 -1
  74. package/site/dist/assets/{InstallWalletScreen-DTA8dwLv-CxreyXoD.js → InstallWalletScreen-DTA8dwLv-BMU_wa61.js} +1 -1
  75. package/site/dist/assets/{JsonTree-BHzNC-ic-DcNcbSoM.js → JsonTree-BHzNC-ic-DcVr0YF0.js} +1 -1
  76. package/site/dist/assets/{LabelXs-oqZNqbm_-iafrj25N.js → LabelXs-oqZNqbm_-CNprGVQM.js} +1 -1
  77. package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +1 -0
  78. package/site/dist/assets/{Layouts-BMRfo5hw-CeCOyCLK.js → Layouts-BMRfo5hw-e-sepi6F.js} +1 -1
  79. package/site/dist/assets/{Link-BdDilT2T-BEDEdHuS.js → Link-BdDilT2T-BdwlRJch.js} +1 -1
  80. package/site/dist/assets/{LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js → LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js} +1 -1
  81. package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +1 -0
  82. package/site/dist/assets/{LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js → LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js} +1 -1
  83. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +1 -0
  84. package/site/dist/assets/{LinkPhoneScreen-oIUpfj0w-C0DuD14j.js → LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js} +1 -1
  85. package/site/dist/assets/{Loader-UAZqxsnw-Cy7hEE2f.js → Loader-UAZqxsnw-DJEVblm1.js} +1 -1
  86. package/site/dist/assets/{LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js → LoadingSkeleton-BMsgO5PV-D2atjsP5.js} +1 -1
  87. package/site/dist/assets/{LockClosedIcon-BgY_5v_Z.js → LockClosedIcon-9EFzWK1C.js} +1 -1
  88. package/site/dist/assets/{LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js → LoginFailedScreen-CN0dJV6U-By3Si77a.js} +1 -1
  89. package/site/dist/assets/{ManualTransferScreen-CHqcuqbs-tXiZDHmN.js → ManualTransferScreen-CHqcuqbs-BAHPY8rs.js} +1 -1
  90. package/site/dist/assets/{MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js → MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js} +1 -1
  91. package/site/dist/assets/{MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js → MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js} +1 -1
  92. package/site/dist/assets/{MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js → MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js} +1 -1
  93. package/site/dist/assets/{MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js → MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js} +1 -1
  94. package/site/dist/assets/{ModalFooter-BCuEK_nz-c-khvG4Y.js → ModalFooter-BCuEK_nz-C_71qsaz.js} +1 -1
  95. package/site/dist/assets/{MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js → MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js} +1 -1
  96. package/site/dist/assets/{OAuthStatusScreen-ufbOa-da-CeiGcSia.js → OAuthStatusScreen-ufbOa-da-BjAb_WsY.js} +1 -1
  97. package/site/dist/assets/{OpenLink-CUpJ1mOr-DfAvJRyK.js → OpenLink-CUpJ1mOr-BLWFsPOW.js} +1 -1
  98. package/site/dist/assets/{PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js → PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js} +1 -1
  99. package/site/dist/assets/{PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js → PasskeyStatusScreen-Bla8MaOk-DCEg761l.js} +1 -1
  100. package/site/dist/assets/{PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js → PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js} +1 -1
  101. package/site/dist/assets/{PhoneIcon-CvIiPwYQ.js → PhoneIcon-CZ47md80.js} +1 -1
  102. package/site/dist/assets/{PinInput-DbZ0b1i1-CDXZBdQx.js → PinInput-DbZ0b1i1-BrTXu3Km.js} +1 -1
  103. package/site/dist/assets/{PrivyPluginContext-DA-yS5vO-CtQWBiV2.js → PrivyPluginContext-DA-yS5vO-CY60yubk.js} +1 -1
  104. package/site/dist/assets/{QrCode-DbnZwlkH-h6Bhoe5U.js → QrCode-DbnZwlkH-CcMrWV5D.js} +2 -2
  105. package/site/dist/assets/{RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js → RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js} +1 -1
  106. package/site/dist/assets/{RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js → RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js} +1 -1
  107. package/site/dist/assets/{RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js → RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js} +1 -1
  108. package/site/dist/assets/{SIWXUtil-TUCxa7aA.js → SIWXUtil-Cpiv91-M.js} +1 -1
  109. package/site/dist/assets/{Screen-D30meWsY-CCeVNuWs.js → Screen-D30meWsY-vEusJ7KX.js} +1 -1
  110. package/site/dist/assets/{ScreenHeader-CHmc4-Lu-DjyrH7IK.js → ScreenHeader-CHmc4-Lu-BvGTLg53.js} +1 -1
  111. package/site/dist/assets/{ScreenLayout-OwaQQcxH-CrQM6kvA.js → ScreenLayout-OwaQQcxH-cfOTmyl0.js} +1 -1
  112. package/site/dist/assets/{SelectSourceAsset-CWFog6gm-D7yTw87h.js → SelectSourceAsset-CWFog6gm-DxYklZYs.js} +1 -1
  113. package/site/dist/assets/{SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js → SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js} +1 -1
  114. package/site/dist/assets/{SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js → SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js} +1 -1
  115. package/site/dist/assets/{ShieldCheckIcon-o6RHYev1.js → ShieldCheckIcon-eFjsosCA.js} +1 -1
  116. package/site/dist/assets/{SignRequestScreen-BHKv32KR-DDpMYyJ1.js → SignRequestScreen-BHKv32KR-z8ixfIma.js} +1 -1
  117. package/site/dist/assets/{SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js → SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js} +1 -1
  118. package/site/dist/assets/{StackedContainer-B2vaEl56-eQCM2cmv.js → StackedContainer-B2vaEl56-Do5dXB8-.js} +1 -1
  119. package/site/dist/assets/{StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js → StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js} +1 -1
  120. package/site/dist/assets/{Subtitle-CV-2yKE4-zxWw2m1Q.js → Subtitle-CV-2yKE4-DV0o1av3.js} +1 -1
  121. package/site/dist/assets/{TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js → TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js} +1 -1
  122. package/site/dist/assets/{Title-BnzYV3Is-B_JpTatT.js → Title-BnzYV3Is-C3qOPnO_.js} +1 -1
  123. package/site/dist/assets/{TodoList-DnyULl18-C8Eyy28X.js → TodoList-DnyULl18-BvxvubbF.js} +1 -1
  124. package/site/dist/assets/{TransactionDetails-e9nesl30-C4MpIHR-.js → TransactionDetails-e9nesl30-WWflkOIi.js} +1 -1
  125. package/site/dist/assets/{TransactionErrorView-BDBNqA_f-p1T9WVMx.js → TransactionErrorView-BDBNqA_f-CotYKcgf.js} +1 -1
  126. package/site/dist/assets/{TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js → TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js} +1 -1
  127. package/site/dist/assets/{TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js → TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js} +1 -1
  128. package/site/dist/assets/{TurnstileWrapper-B7sNddys-Jwy1tJtm.js → TurnstileWrapper-B7sNddys-oPbuMTQt.js} +1 -1
  129. package/site/dist/assets/{UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js → UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js} +1 -1
  130. package/site/dist/assets/{UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js → UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js} +1 -1
  131. package/site/dist/assets/{UserLimitReachedScreen-5we2DHn8-D7m70C4b.js → UserLimitReachedScreen-5we2DHn8-CaVN0bti.js} +1 -1
  132. package/site/dist/assets/{Value-DTgR824E-BRuIGiec.js → Value-DTgR824E-Bp6WGZZm.js} +1 -1
  133. package/site/dist/assets/{WalletCards-DH1rqayz-Bgp9Fsgs.js → WalletCards-DH1rqayz-D3cOTb9x.js} +1 -1
  134. package/site/dist/assets/{WalletIcon-gKf0gTw6.js → WalletIcon-DOIZhb2l.js} +1 -1
  135. package/site/dist/assets/{WalletInfoCard-sq_2wgFg-DpNSAGte.js → WalletInfoCard-sq_2wgFg-bOqfl5MA.js} +1 -1
  136. package/site/dist/assets/{WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js → WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js} +1 -1
  137. package/site/dist/assets/{WalletLink-DK5LB5Ha-DHaL350n.js → WalletLink-DK5LB5Ha-DjUZvqs-.js} +1 -1
  138. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +1 -0
  139. package/site/dist/assets/{WarningBanner-ZZqCEtZK-2eU4Qo74.js → WarningBanner-ZZqCEtZK-Bc2YKZb7.js} +1 -1
  140. package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +1 -0
  141. package/site/dist/assets/{_esm-BdeM6pxJ.js → _esm-BZP3cXTv.js} +1 -1
  142. package/site/dist/assets/_esm-ZmzBJpEs.js +6 -0
  143. package/site/dist/assets/{accounts-D96UKWkC.js → accounts-D4AXF17G.js} +1 -1
  144. package/site/dist/assets/accounts-IkdFx9nq.js +1 -0
  145. package/site/dist/assets/{baseSepolia-DX-qGsik.js → baseSepolia-Bn1j1Ysw.js} +1 -1
  146. package/site/dist/assets/{basic-oIwwSyu4.js → basic-Cz1YiCSk.js} +4 -4
  147. package/site/dist/assets/{ccip-BZvRTL6g.js → ccip-B1J3oTZx.js} +2 -2
  148. package/site/dist/assets/ccip-Bd7261zy.js +1 -0
  149. package/site/dist/assets/{ccip-65sIfSPR.js → ccip-D6EHKpjI.js} +1 -1
  150. package/site/dist/assets/{chain-DM3oH6Px.js → chain-scefkYIH.js} +3 -6
  151. package/site/dist/assets/check-DHnllI8N.js +1 -0
  152. package/site/dist/assets/{chevron-down-BSsbXkK-.js → chevron-down-1y-r4bGO.js} +1 -1
  153. package/site/dist/assets/{circle-alert-DWaeLZNj.js → circle-alert-OwxamXUX.js} +1 -1
  154. package/site/dist/assets/{circle-check-big-Chb1ivw9.js → circle-check-big-3sZGVC10.js} +1 -1
  155. package/site/dist/assets/{circle-x-AV9Cet0R.js → circle-x-DQjNUV9J.js} +1 -1
  156. package/site/dist/assets/client-B-BpRtwY.js +1 -0
  157. package/site/dist/assets/client-BfIR1FvH.js +9 -0
  158. package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +3 -0
  159. package/site/dist/assets/{copy-Ckr0AkL6.js → copy-DMOX_yn7.js} +1 -1
  160. package/site/dist/assets/core-GIEmxmAa.js +12 -0
  161. package/site/dist/assets/{createLucideIcon-IPW50ckY.js → createLucideIcon-BsERoFTA.js} +1 -1
  162. package/site/dist/assets/{defineChain-CiuCXUZW.js → defineChain-B-SQDLTG.js} +1 -1
  163. package/site/dist/assets/{dijkstra-Drfl93V4.js → dijkstra-CY95afk4.js} +1 -1
  164. package/site/dist/assets/dist-Cp6IXeJd.js +1 -0
  165. package/site/dist/assets/{dist-Cm7pNAtL.js → dist-CrNxmdZj.js} +1 -1
  166. package/site/dist/assets/{dist-DnGANz51.js → dist-DYGtf4F0.js} +4 -4
  167. package/site/dist/assets/embed.worker-Cnl4VGr3.js +1 -0
  168. package/site/dist/assets/{errors-BQ3N-RVt-BLqjTPNR.js → errors-BQ3N-RVt-CZPAWCgC.js} +1 -1
  169. package/site/dist/assets/{esm-B5cbIQaz.js → esm-Co94ENUV.js} +17 -17
  170. package/site/dist/assets/{esm-ByALO6mA.js → esm-bb8SANZh.js} +1 -1
  171. package/site/dist/assets/{ethers-zTjbodk5-wHhL_BQ1.js → ethers-zTjbodk5-Cs648CnD.js} +1 -1
  172. package/site/dist/assets/{eventemitter3-CL76jPsW.js → eventemitter3-Dzc_0Uq2.js} +1 -1
  173. package/site/dist/assets/{external-link-D-uehwM2.js → external-link-BBPG3rG7.js} +1 -1
  174. package/site/dist/assets/{farcaster-DPlSjvF5-BqTvDXBR.js → farcaster-DPlSjvF5-CzBCmgyL.js} +1 -1
  175. package/site/dist/assets/{features-DQ0UYy6K.js → features-CuiHbgEJ.js} +1 -1
  176. package/site/dist/assets/{fingerprint-pattern-D8ytA16Q.js → fingerprint-pattern-CjLtyIxB.js} +1 -1
  177. package/site/dist/assets/floating-ui.react-DmaCfayr.js +1 -0
  178. package/site/dist/assets/{floating-ui.react-dom-DKWOL9UG.js → floating-ui.react-dom-CaoMF8mP.js} +1 -1
  179. package/site/dist/assets/{get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js → get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js} +1 -1
  180. package/site/dist/assets/{getErc20Balance-jXNpFf0N-Bb9WM31M.js → getErc20Balance-jXNpFf0N-q4bV8kQq.js} +1 -1
  181. package/site/dist/assets/{getErc20TokenInfo-BNTrzLMP-KPrdn04K.js → getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js} +1 -1
  182. package/site/dist/assets/{guards-BIW-GiMJ-CHfXo8XI.js → guards-BIW-GiMJ-D3tWTQAR.js} +1 -1
  183. package/site/dist/assets/{hourglass-CPG1HZmf.js → hourglass-D_GfQdJr.js} +1 -1
  184. package/site/dist/assets/{id-UqEwL2BI-BkKm533U.js → id-UqEwL2BI-CTlvRMA5.js} +1 -1
  185. package/site/dist/assets/{index-BdEw5wyl-ChSJvPZk.js → index-BdEw5wyl-kLbhqLbv.js} +1 -1
  186. package/site/dist/assets/index-C2h3EneX.js +8 -0
  187. package/site/dist/assets/{index-CWARkn2w-Cezl_uBr.js → index-CWARkn2w-DxuckIFn.js} +1 -1
  188. package/site/dist/assets/{index-DvMddJcd-BatvgC-S.js → index-DvMddJcd-CNBb4VUn.js} +1 -1
  189. package/site/dist/assets/{index-YiCw0Y_x-CX5i3Z87.js → index-YiCw0Y_x-Czl0tWfF.js} +5 -6
  190. package/site/dist/assets/{index.es-BmqyoBEM.js → index.es-Crd0TyUS.js} +1 -1
  191. package/site/dist/assets/index.es-DQbup1WG.js +3 -0
  192. package/site/dist/assets/{isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js → isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js} +1 -1
  193. package/site/dist/assets/{jsx-runtime-BbDfbRii.js → jsx-runtime-CndCZlyt.js} +1 -1
  194. package/site/dist/assets/{landmark-CH6LV6F0.js → landmark-BZPqMFEf.js} +1 -1
  195. package/site/dist/assets/lib-DGtzi2DK.js +1 -0
  196. package/site/dist/assets/{localBatchGatewayRequest-3kLrX_c7.js → localBatchGatewayRequest-B71J8sQ9.js} +7 -10
  197. package/site/dist/assets/{lock-DcskI4Bb.js → lock-DyN_c-ud.js} +1 -1
  198. package/site/dist/assets/{mail-Bs0r5yOi.js → mail-V_mcPb64.js} +1 -1
  199. package/site/dist/assets/{modal-context-CwmSeFcv-DruX9J1E.js → modal-context-CwmSeFcv-DPWNJrfK.js} +1 -1
  200. package/site/dist/assets/{ofetch.CWycOUEr-CHp-crOQ.js → ofetch.CWycOUEr-Duby3iHT.js} +1 -1
  201. package/site/dist/assets/parseEther-CHS0GNkM.js +1 -0
  202. package/site/dist/assets/{pkce-DXF0yKjP-ChQhE0st.js → pkce-DXF0yKjP-7_VY-d83.js} +1 -1
  203. package/site/dist/assets/preload-helper-CRzLMJPS.js +1 -0
  204. package/site/dist/assets/{privy-context-81KmsBw2-CjWGzyg7.js → privy-context-81KmsBw2-DiR6-Yq8.js} +1 -1
  205. package/site/dist/assets/{react-DrJ0MmMO.js → react-BfYA1q0G.js} +1 -1
  206. package/site/dist/assets/react-CK2HRI1t.js +1 -0
  207. package/site/dist/assets/{react-DQnMl2Xv.js → react-DxGeY8B8.js} +1 -1
  208. package/site/dist/assets/{react-dom-5dW7aKoP.js → react-dom-BI2Koe4o.js} +1 -1
  209. package/site/dist/assets/{reservoir-x-nGuZkT-BMX127i6.js → reservoir-x-nGuZkT-BguVFH3D.js} +1 -1
  210. package/site/dist/assets/roles-BDPDWrwo.js +1 -0
  211. package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +1 -0
  212. package/site/dist/assets/secp256k1-BeYL8aZl.js +1 -0
  213. package/site/dist/assets/{secp256k1-_MleGpqL.js → secp256k1-Bh4MSkhz.js} +1 -1
  214. package/site/dist/assets/secp256k1-Ci4cqHl0.js +1 -0
  215. package/site/dist/assets/secp256k1-DsxXqbXv.js +1 -0
  216. package/site/dist/assets/secp256k1-N9RUmafO.js +1 -0
  217. package/site/dist/assets/{serializeAuthorizationList-CX8hGL56.js → serializeAuthorizationList-DGMik9Wx.js} +1 -1
  218. package/site/dist/assets/{serializeSignature-DfxkPnu-.js → serializeSignature-CWKpxoab.js} +1 -1
  219. package/site/dist/assets/sha2-C6s7zZmb.js +1 -0
  220. package/site/dist/assets/{shared-DRZUmnL5-xqAUARWo.js → shared-DRZUmnL5-BewE5wK8.js} +1 -1
  221. package/site/dist/assets/{shared-FM0rljBt--Avg6q2R.js → shared-FM0rljBt-CxLACri_.js} +1 -1
  222. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +1 -0
  223. package/site/dist/assets/{simulateTransaction-CfuOrae4-64lWYJAL.js → simulateTransaction-CfuOrae4-PjA_tGjX.js} +1 -1
  224. package/site/dist/assets/{smartphone-CcEZ-XAy.js → smartphone-3p8nFSTE.js} +1 -1
  225. package/site/dist/assets/{storage-Ufl800Ne-D0bZBw-H.js → storage-Ufl800Ne-1qCR2JMO.js} +7 -8
  226. package/site/dist/assets/{stringify-BlDm7tuc.js → stringify-DqUj9VzO.js} +2 -3
  227. package/site/dist/assets/stripe.esm-DEi2D291.js +2 -0
  228. package/site/dist/assets/{styles-BsotlekN-CXEB4Ud9.js → styles-BsotlekN-Aq27nZGc.js} +1 -1
  229. package/site/dist/assets/{styles-Cm9H63WO-Br4QtzUL.js → styles-Cm9H63WO-p_ic_dxE.js} +1 -1
  230. package/site/dist/assets/{styles-DVyDvTdj-BTABn2K8.js → styles-DVyDvTdj-CjsWqAld.js} +1 -1
  231. package/site/dist/assets/{styles-hslXy6xG-D-49b1rk.js → styles-hslXy6xG-D5QHIrJU.js} +1 -1
  232. package/site/dist/assets/{telegram-B-JqnkqZ-RBoY3PLx.js → telegram-B-JqnkqZ-Dqkm4rDn.js} +1 -1
  233. package/site/dist/assets/{to-ui-error-DWe-qwS3-Cx2bk1kZ.js → to-ui-error-DWe-qwS3-Bi1HUQI9.js} +1 -1
  234. package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +2 -0
  235. package/site/dist/assets/transformers.web-BTyOEJ0C.js +40 -0
  236. package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
  237. package/site/dist/assets/{triangle-alert-CWjYH75H.js → triangle-alert-BG3Qw-Hq.js} +1 -1
  238. package/site/dist/assets/{twitch-5IOe4sIQ-lDtgk0os.js → twitch-5IOe4sIQ-CaXHFlAu.js} +1 -1
  239. package/site/dist/assets/{use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js → use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js} +1 -1
  240. package/site/dist/assets/{use-unlink-wallet-BKK4_5Ur-CC2pq27z.js → use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js} +1 -1
  241. package/site/dist/assets/{useActiveWallet-B-yZYGc6-D7Ik-LxJ.js → useActiveWallet-B-yZYGc6-BkK6rFBm.js} +1 -1
  242. package/site/dist/assets/{useGetSolPrice-x7gfUIHJ-D1hmQfpA.js → useGetSolPrice-x7gfUIHJ-CLVXqv6I.js} +1 -1
  243. package/site/dist/assets/{useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js → useGetTokenPrice-CaoUkb79-DAXXs3rs.js} +1 -1
  244. package/site/dist/assets/{useI18n-C-5n_noF-BcqC76yj.js → useI18n-C-5n_noF-CGpkgIVy.js} +1 -1
  245. package/site/dist/assets/{useWalletBalance-CZnP5f_f-Dw2GIJ4g.js → useWalletBalance-CZnP5f_f-Cfd64m4n.js} +1 -1
  246. package/site/dist/assets/{useWallets-X1ndvJ_U-DRjz7uYD.js → useWallets-X1ndvJ_U-CAYqg7Za.js} +1 -1
  247. package/site/dist/assets/{w3m-modal-BeVrBOIf.js → w3m-modal-B4H_uiPH.js} +2 -2
  248. package/site/dist/assets/{wallet-Bn85Sg2W.js → wallet-BNI12IYk.js} +1 -1
  249. package/site/dist/assets/wallet-CITSaPlB.js +2 -0
  250. package/site/dist/assets/{wallet-connect-BvJtUsDt-Y03hhqFN.js → wallet-connect-BvJtUsDt-CVlSNIXP.js} +1 -1
  251. package/site/dist/assets/{walletconnect-registry-xxG8XLCb-D1SSFvMp.js → walletconnect-registry-xxG8XLCb-BpzpBq5s.js} +1 -1
  252. package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +12 -0
  253. package/site/dist/assets/{wui-ux-by-reown-D34ILNLS.js → wui-ux-by-reown-XyHdtV1O.js} +1 -1
  254. package/site/dist/assets/x-gjDrY2rk.js +1 -0
  255. package/site/dist/index.html +5 -5
  256. package/site/feed.js +16 -1
  257. package/site/no-model.js +4 -0
  258. package/site/worker.js +586 -59
  259. package/src/agent/agent-card.js +20 -11
  260. package/src/agent/mcp.js +190 -324
  261. package/src/agent/view-tools.js +46 -13
  262. package/src/agent/x402.js +340 -44
  263. package/src/core/embed.js +51 -52
  264. package/src/core/embed.worker.js +1 -1
  265. package/src/core/shard.js +7 -3
  266. package/src/discover/apps.js +41 -5
  267. package/src/discover/explore.js +50 -30
  268. package/src/market/reactions.js +2 -2
  269. package/src/publish/devnet.js +95 -0
  270. package/src/publish/init.js +74 -0
  271. package/src/publish/lint.js +2 -1
  272. package/src/publish/ship.js +559 -267
  273. package/src/publish/view-cli.js +14 -10
  274. package/src/publish/view.js +355 -218
  275. package/src/taste/taste-doc.js +3 -2
  276. package/src/taste/taste.js +2 -2
  277. package/src/taste/vault.js +3 -1
  278. package/template/README.md +49 -0
  279. package/template/app.json +27 -0
  280. package/template/eval/goals.md +7 -0
  281. package/template/eval/golden.jsonl +0 -0
  282. package/template/gitignore +3 -0
  283. package/template/package.json +13 -0
  284. package/template/wrangler.json +12 -0
  285. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
  286. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
  287. package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
  288. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
  289. package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
  290. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
  291. package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
  292. package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
  293. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
  294. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
  295. package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
  296. package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
  297. package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
  298. package/site/dist/assets/ccip-DA_zG341.js +0 -1
  299. package/site/dist/assets/check-BPgcwEPP.js +0 -1
  300. package/site/dist/assets/client-CZG0ndth.js +0 -2
  301. package/site/dist/assets/client-Dsa7_3V3.js +0 -9
  302. package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
  303. package/site/dist/assets/core-snws5xwv.js +0 -12
  304. package/site/dist/assets/dist-Dl07uGq1.js +0 -1
  305. package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
  306. package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
  307. package/site/dist/assets/index-1HYFYA-W.js +0 -8
  308. package/site/dist/assets/index.es-C1v1or--.js +0 -3
  309. package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
  310. package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
  311. package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
  312. package/site/dist/assets/react-DCXkXSrv.js +0 -1
  313. package/site/dist/assets/roles-32M1l9jI.js +0 -1
  314. package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
  315. package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
  316. package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
  317. package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
  318. package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
  319. package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
  320. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
  321. package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
  322. package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
  323. package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
  324. package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
  325. package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
  326. package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
  327. package/site/dist/assets/x-rlSp9U7W.js +0 -1
package/src/agent/mcp.js CHANGED
@@ -1,50 +1,45 @@
1
1
  #!/usr/bin/env node
2
- // Every Fangorn app's WebMCP tools, behind one stdio MCP server.
2
+ // Every Fangorn app's tools, behind one stdio MCP server.
3
3
  //
4
- // fangorn-mcp [--cdp <url>] [--headed] [--from-block <n>]
4
+ // fangorn-mcp [--from-block <n>]
5
5
  // claude mcp add fangorn -- fangorn-mcp
6
- // claude mcp add fangorn -e FANGORN_MCP_CDP=ws://browser:9222 -- fangorn-mcp
7
6
  //
8
- // WebMCP tools live on `document.modelContext` inside a loaded tab. There is no
9
- // endpoint, so an agent outside the browser cannot reach them, and one MCP
10
- // server per app would mean one registration per app. Instead this server is
11
- // the directory and the browser both:
7
+ // One MCP server for every app, so an agent registers once. It is a directory
8
+ // and a proxy:
12
9
  //
13
10
  // list-apps apps read off the chain (AppAgentChanged → verified cards)
14
- // open-app verify one app's card, open its page in a tab, and add its
15
- // tools to this server as `<app>__<tool>` (tools/list_changed)
11
+ // open-app verify one app's card and add its tools to this server as
12
+ // `<app>__<tool>` (tools/list_changed)
16
13
  // call-app-tool the same call by name, for clients that ignore list_changed
17
14
  //
18
- // One browser, one tab per opened app: a WebMCP browser at --cdp, or else a
19
- // local Chrome with WebMCP on, headless unless --headed. Calls
20
- // are relayed to the tab's own `modelContext.executeTool`, so the tools, their
21
- // schemas and their answers are the page's, and queries still run in the tab.
22
- // A local Chrome's profile persists (~/.cache/westmarch-mcp), so a page's
23
- // downloads, such as an embedding model, are fetched once. A tab or browser that
24
- // dies is reopened on the next call.
15
+ // An app's tools are its own MCP server's (the card's `mcp`, the app's Worker at
16
+ // /mcp), relayed. The Worker alone reads the app's lens from R2, so nothing is
17
+ // downloaded here and access is the app's to gate.
18
+ // A private app's paid tools answer with x402's PaymentRequired. With FANGORN_MCP_WALLET_KEY set,
19
+ // this server pays: a pass when the card sells one at or under FANGORN_MCP_MAX_PASS (USDC; unset,
20
+ // never), else that one call at or under FANGORN_MCP_MAX_PRICE (default 0.10). Without a wallet the
21
+ // price is handed back to the agent, to say to the person.
25
22
 
26
- import { spawn } from "node:child_process";
27
- import { mkdirSync, readFileSync, existsSync, rmSync, writeFileSync, renameSync } from "node:fs";
28
- import { createHash } from "node:crypto";
29
23
  import net from "node:net";
24
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
30
25
  import { homedir } from "node:os";
31
26
  import { createInterface } from "node:readline";
32
27
  import { APP_EXTENSION, listApps, toApp } from "../discover/apps.js";
33
28
 
34
- // The current AppRegistry's first event on Arbitrum Sepolia. A reader's
35
- // default, not deployment config: pass --from-block for another deployment.
36
- const DEFAULT_FROM_BLOCK = 311637349n;
37
29
  const log = (...a) => console.error("[fangorn-mcp]", ...a); // stdout is the protocol
38
30
  // FANGORN_MCP_TRACE=1: step timings on stderr, for finding where an answer waits.
39
31
  const T0 = performance.now();
40
32
  const trace = process.env.FANGORN_MCP_TRACE ? (what) => log(`${Math.round(performance.now() - T0)}ms ${what}`) : () => {};
41
33
 
34
+ // FANGORN_NETWORK: the SDK's network name ("baseSepolia"); unset, its default.
35
+ const network = async () => (await import("../publish/view.js")).network();
36
+
42
37
  let client;
43
38
  async function fangorn() {
44
39
  if (!client) {
45
- const [{ Fangorn, FangornConfig }, { generatePrivateKey }] = await Promise.all([
40
+ const [{ Fangorn }, { generatePrivateKey }] = await Promise.all([
46
41
  import("@fangorn-network/sdk"), import("viem/accounts")]);
47
- client = Fangorn.create({ privateKey: generatePrivateKey(), config: FangornConfig }); // reads only
42
+ client = Fangorn.create({ privateKey: generatePrivateKey(), config: await network() }); // reads only
48
43
  // list-apps verifies every card and open-app verifies one again: once a minute is enough.
49
44
  const verify = client.discoverApp.bind(client), seen = new Map();
50
45
  client.discoverApp = (url) => {
@@ -58,62 +53,66 @@ async function fangorn() {
58
53
  return client;
59
54
  }
60
55
 
61
- /** A card URL → the verified card and the page to open. A plain A2A card (no
62
- * Fangorn extension) opens unverified: the caller asked for it by URL. */
56
+ /** A card URL → its app's MCP endpoint (the card's `mcp`), once the card verifies on chain. */
63
57
  async function resolveCard(cardUrl) {
64
58
  const card = await (await fetch(cardUrl)).json();
65
- const fangornApp = card.capabilities?.extensions?.some((e) => e.uri === APP_EXTENSION);
66
- // Views only from a verified card, and only http(s) ones (toApp filters).
67
- const app = fangornApp ? toApp(await (await fangorn()).discoverApp(cardUrl), cardUrl) : null;
68
- const page = new URL(card.url);
69
- if (page.protocol !== "https:" && page.protocol !== "http:") throw new Error(`card.url is not http(s): ${card.url}`);
70
- return { card, page: page.toString(), verified: fangornApp, views: app?.views ?? [], paid: app?.paid ?? null };
59
+ if (!card.capabilities?.extensions?.some((e) => e.uri === APP_EXTENSION)) throw new Error(`${cardUrl} is not a Fangorn app card`);
60
+ const { mcp } = toApp(await (await fangorn()).discoverApp(cardUrl), cardUrl);
61
+ return { card, mcp };
71
62
  }
72
63
 
73
- // ── the network ─────────────────────────────────────────────────────────────
74
- // Every byte an answer waits on, cached where it is safe to:
75
- // - a content-addressed file (`…-<12 hex>.<ext>`, every shard) never changes,
76
- // so it is kept on disk forever. shard.js still checks it against the
77
- // manifest's sha256, so a bad cache entry fails loudly, not silently.
78
- // - catalogs, manifests and cards change on a rebake: held for a minute, so
79
- // one search (or resolving then verifying a card) does not fetch each twice.
80
- const CACHE = `${homedir()}/.cache/fangorn-mcp`;
81
- /** Stale-while-revalidate from disk: the last copy at once, a fresh one fetched
82
- * behind it for next time. Only for reads where stale is harmless. */
83
- function staleFetch(fetcher) {
84
- mkdirSync(`${CACHE}/stale`, { recursive: true });
85
- return async (url) => {
86
- const f = `${CACHE}/stale/${createHash("sha256").update(url).digest("hex")}`;
87
- const refresh = fetcher(url).then(async (r) => {
88
- if (!r.ok) return r;
89
- const b = new Uint8Array(await r.arrayBuffer());
90
- writeFileSync(`${f}.tmp`, b); renameSync(`${f}.tmp`, f);
91
- return new Response(b);
92
- });
93
- if (existsSync(f)) { refresh.catch(() => {}); return new Response(readFileSync(f)); }
94
- return refresh;
64
+ /** An app's own MCP server over streamable HTTP → `{ tools, call }`.
65
+ * The app's Worker is stateless (one JSON body per POST); a session id or an SSE
66
+ * reply from any other server is followed too. */
67
+ export async function remoteTools(url, fetcher = fetch, { access = null, wallet = process.env.FANGORN_MCP_WALLET_KEY,
68
+ maxCall = process.env.FANGORN_MCP_MAX_PRICE ?? "0.10", maxPass = process.env.FANGORN_MCP_MAX_PASS } = {}) {
69
+ let id = 0, session, bearer;
70
+ const rpc = async (method, params) => {
71
+ const r = await fetcher(url, { method: "POST", body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
72
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...(session && { "mcp-session-id": session }),
73
+ ...(bearer && { authorization: `Bearer ${bearer}` }) } });
74
+ session ??= r.headers.get("mcp-session-id") ?? undefined;
75
+ let body = await r.text();
76
+ if (!r.ok) throw new Error(`${url}: HTTP ${r.status} ${body.slice(0, 200)}`);
77
+ if (r.headers.get("content-type")?.includes("text/event-stream")) body = body.split("\n").filter((l) => l.startsWith("data:")).pop()?.slice(5) ?? "";
78
+ const m = JSON.parse(body);
79
+ if (m.error) throw new Error(`${url}: ${m.error.message}`);
80
+ return m.result;
95
81
  };
82
+ await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "fangorn-mcp", version: "0.1.0" } });
83
+ const { tools } = await rpc("tools/list", {});
84
+ const usdc = (n) => BigInt(Math.round(Number(n) * 10 ** (access?.decimals ?? 6)));
85
+ const call = async (name, args) => {
86
+ const params = { name, arguments: args ?? {} };
87
+ const r = await rpc("tools/call", params);
88
+ const asked = r?.isError && r.structuredContent?.x402Version === 2 ? r.structuredContent : null;
89
+ if (!asked) return r;
90
+ if (!wallet) return { ...r, content: [...r.content, { type: "text", text: "This server has no wallet (FANGORN_MCP_WALLET_KEY), so nothing was paid: tell the person the price." }] };
91
+ const x = await import("./x402.js");
92
+ const account = (await import("viem/accounts")).privateKeyToAccount(`0x${wallet.trim().replace(/^0x/i, "")}`); // a key file may lack 0x
93
+ if (access && maxPass != null && BigInt(access.pass) <= usdc(maxPass)) {
94
+ bearer = (await x.buyPass(access, { account, fetchImpl: fetcher })).token;
95
+ return rpc("tools/call", params);
96
+ }
97
+ const payment = await x.signPayment(x.choose(asked.accepts, { maxPrice: usdc(maxCall) }), account, { resource: asked.resource });
98
+ return rpc("tools/call", { ...params, _meta: { "x402/payment": payment } });
99
+ };
100
+ return { tools, call };
96
101
  }
97
102
 
103
+ // ── the network ─────────────────────────────────────────────────────────────
104
+ // Catalogs, manifests and cards change on a rebake: held for a minute, so one
105
+ // search (or resolving then verifying a card) does not fetch each twice.
106
+ const CACHE = `${homedir()}/.cache/fangorn-mcp`; // the embedding model (find-apps) and the chain logs read
98
107
  function cachingFetch(real = globalThis.fetch) {
99
- mkdirSync(`${CACHE}/blobs`, { recursive: true });
100
108
  const memo = new Map();
101
109
  return async (input, init) => {
102
110
  const url = String(input?.url ?? input);
103
111
  if ((init?.method ?? input?.method ?? "GET") !== "GET" || !/^https?:/.test(url)) return real(input, init);
104
112
  const path = new URL(url).pathname;
105
- if (/-[0-9a-f]{12}\.[a-z.]+$/.test(path)) {
106
- const f = `${CACHE}/blobs/${createHash("sha256").update(url).digest("hex")}`;
107
- if (existsSync(f)) return new Response(readFileSync(f));
108
- const r = await real(input, init);
109
- if (!r.ok) return r;
110
- const b = new Uint8Array(await r.arrayBuffer());
111
- writeFileSync(`${f}.tmp`, b); renameSync(`${f}.tmp`, f); // never a torn file
112
- return new Response(b, { headers: r.headers });
113
- }
114
113
  // A card, catalog or manifest is small: 8s without one means the host is
115
114
  // down, and a dead card in the directory must not stall a listing for a
116
- // TCP timeout. Shards get no deadline; a big file on a slow link is legitimate.
115
+ // TCP timeout.
117
116
  init = { ...init, signal: init?.signal ?? AbortSignal.timeout(8000) };
118
117
  if (/\/cdn\/catalog$|\/manifest$|\/agent-card\.json$/.test(path)) {
119
118
  const hit = memo.get(url);
@@ -128,140 +127,27 @@ function cachingFetch(real = globalThis.fetch) {
128
127
  };
129
128
  }
130
129
 
131
- // ── the browser ─────────────────────────────────────────────────────────────
132
- // Anything that speaks the Chrome DevTools Protocol and has WebMCP: a headless
133
- // Chromium service, a hosted browser, or the local Chrome. `--cdp` (or
134
- // FANGORN_MCP_CDP) names it as http(s)://host:port or a ws(s):// browser
135
- // endpoint. Without it, a local Chrome is reused if one is already serving
136
- // this profile (another session's), else launched.
137
- //
138
- // Everything goes over the one browser-level WebSocket (Target.* plus flat
139
- // sessions). That is the endpoint every CDP host exposes, where the per-tab
140
- // /json HTTP routes are not.
141
-
142
- const PROFILE = `${homedir()}/.cache/westmarch-mcp`;
143
-
144
- /** An http(s) DevTools address → its browser WebSocket, reached via the host we were given. */
145
- async function browserWs(http) {
146
- const v = await (await fetch(new URL("/json/version", http), { signal: AbortSignal.timeout(3000) })).json();
147
- const ws = new URL(v.webSocketDebuggerUrl), base = new URL(http);
148
- ws.host = base.host; // the service reports its own view of itself
149
- if (base.protocol === "https:") ws.protocol = "wss:";
150
- return ws.toString();
151
- }
152
-
153
- async function localChrome({ headed }) {
154
- mkdirSync(PROFILE, { recursive: true });
155
- const portFile = `${PROFILE}/DevToolsActivePort`;
156
- if (existsSync(portFile)) {
157
- const ws = await browserWs(`http://127.0.0.1:${readFileSync(portFile, "utf-8").split("\n")[0]}`).catch(() => null);
158
- if (ws) { log("reusing the Chrome already serving this profile"); return { ws }; }
159
- rmSync(portFile, { force: true }); // stale: that Chrome is gone
160
- }
161
- const bin = process.env.CHROME ?? "google-chrome";
162
- const proc = spawn(bin, [
163
- ...(headed ? [] : ["--headless=new"]),
164
- "--enable-features=WebMCP", "--remote-debugging-port=0", "--no-first-run",
165
- "--no-default-browser-check", `--user-data-dir=${PROFILE}`, "about:blank",
166
- ], { stdio: "ignore" });
167
- const failed = new Promise((_, j) => proc.on("error", (e) => j(new Error(
168
- `no browser: cannot launch ${bin} (${e.code}). Point --cdp / FANGORN_MCP_CDP at a WebMCP browser, or set CHROME.`))));
169
- // 30s: a cold start on a loaded machine or CI box has taken 16s.
170
- for (let i = 0; i < 300; i++) {
171
- await Promise.race([new Promise((r) => setTimeout(r, 100)), failed]);
172
- if (existsSync(portFile)) return { ws: await browserWs(`http://127.0.0.1:${readFileSync(portFile, "utf-8").split("\n")[0]}`), proc };
173
- }
174
- proc.kill();
175
- throw new Error(`${bin} did not start`);
176
- }
177
-
178
- let conn;
179
- /** The browser connection, opened once and reopened after it drops. */
180
- function browser(opts) {
181
- return conn ??= (async () => {
182
- const { cdp } = opts;
183
- const { ws: url, proc } = !cdp ? await localChrome(opts)
184
- : /^wss?:/.test(cdp) ? { ws: cdp } : { ws: await browserWs(cdp) };
185
- const ws = new WebSocket(url);
186
- await new Promise((r, j) => { ws.onopen = r; ws.onerror = () => j(new Error(`cannot connect to the browser at ${url}`)); });
187
- let id = 0; const pending = new Map(), dead = new Set();
188
- ws.onmessage = (m) => {
189
- const d = JSON.parse(m.data);
190
- if (d.method === "Target.detachedFromTarget") dead.add(d.params.sessionId);
191
- if (d.id !== undefined) { pending.get(d.id)?.(d); pending.delete(d.id); }
192
- };
193
- ws.onclose = () => { conn = null; for (const r of pending.values()) r({ error: { message: "browser connection closed" } }); };
194
- const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
195
- if (ws.readyState !== WebSocket.OPEN) return rej(new Error("browser connection closed"));
196
- pending.set(++id, (d) => (d.error ? rej(new Error(d.error.message)) : res(d.result)));
197
- ws.send(JSON.stringify({ id, method, params, ...(sessionId && { sessionId }) }));
198
- });
199
- const tabs = new Set();
200
- // Close what we opened. A shared or remote browser outlives this process.
201
- const bye = async () => {
202
- await Promise.race([Promise.all([...tabs].map((t) => send("Target.closeTarget", { targetId: t }).catch(() => {}))),
203
- new Promise((r) => setTimeout(r, 500))]);
204
- // A local Chrome may be serving another session: leave it running
205
- // while anyone else has a tab open; the last one out closes it.
206
- const others = await Promise.race([send("Target.getTargets").then((r) => r.targetInfos.filter((t) => t.type === "page" && t.url !== "about:blank").length).catch(() => 0),
207
- new Promise((r) => setTimeout(() => r(0), 500))]);
208
- // Never a --cdp browser: that one is somebody's service.
209
- if (!cdp && !others) await Promise.race([send("Browser.close").catch(() => {}), new Promise((r) => setTimeout(r, 500))]);
210
- proc?.kill();
211
- process.exit(0);
212
- };
213
- process.once("SIGINT", bye); process.once("SIGTERM", bye); process.stdin.once("end", bye);
214
- log(`browser: ${cdp ?? (proc ? "launched local Chrome" : "local Chrome")}`);
215
- return { send, dead, tabs };
216
- })().catch((e) => { conn = null; throw e; });
217
- }
218
-
219
- /** A new tab on `page`; returns `ev(expr)`, which evaluates in it. */
220
- async function openTab(page, opts) {
221
- const { send, dead, tabs } = await browser(opts);
222
- const { targetId } = await send("Target.createTarget", { url: page });
223
- tabs.add(targetId);
224
- const { sessionId } = await send("Target.attachToTarget", { targetId, flatten: true });
225
- return async (expression) => {
226
- if (dead.has(sessionId)) throw new Error(`tab closed: ${page}`);
227
- const r = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }, sessionId)
228
- .catch((e) => { throw new Error(`tab closed: ${page} (${e.message})`); });
229
- if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text);
230
- return r.result.value;
130
+ /**
131
+ * The chain logs a scan has read, on disk, so a new session reads only the
132
+ * blocks since (apps.js cachedLogs). Logs carry bigints; JSON does not.
133
+ * ponytail: two servers writing at once can tear the file. A torn file reads
134
+ * as empty and the next scan starts over; write-then-rename if that ever bites.
135
+ */
136
+ function fileStore(path) {
137
+ const big = (_, v) => (typeof v === "bigint" ? { $n: String(v) } : v);
138
+ const unbig = (_, v) => (v?.$n !== undefined ? BigInt(v.$n) : v);
139
+ let data;
140
+ try { data = JSON.parse(readFileSync(path, "utf8"), unbig); } catch { data = {}; }
141
+ return {
142
+ get: (k) => data[k],
143
+ set: (k, v) => {
144
+ data[k] = v;
145
+ try { mkdirSync(CACHE, { recursive: true }); writeFileSync(path, JSON.stringify(data, big)); }
146
+ catch (e) { log(`log cache not saved: ${e.message}`); }
147
+ },
231
148
  };
232
149
  }
233
150
 
234
- /** The tab's tools in MCP's shape. Waits for the page to register them. */
235
- async function tabTools(ev) {
236
- let hasWebMCP = false;
237
- for (let i = 0; i < 150; i++) {
238
- const got = await ev(`(async () => {
239
- const mc = document.modelContext;
240
- if (!mc?.getTools) return { webmcp: false, loaded: location.protocol.startsWith("http") && document.readyState === "complete" };
241
- return { webmcp: true, tools: (await mc.getTools()).map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) };
242
- })()`).catch(() => null);
243
- hasWebMCP ||= !!got?.webmcp;
244
- // modelContext is there from the first script or never; no need to wait out the page.
245
- if (got && !got.webmcp && got.loaded) break;
246
- if (got?.tools?.length) return got.tools.map((t) => ({ ...t, inputSchema: t.inputSchema ? JSON.parse(t.inputSchema) : { type: "object" } }));
247
- await new Promise((r) => setTimeout(r, 200));
248
- }
249
- throw new Error(hasWebMCP
250
- ? "the page registered no WebMCP tools in 30s (is card.url the page that registers them?)"
251
- : "this browser has no WebMCP (document.modelContext.getTools): it needs Chrome 150+ with --enable-features=WebMCP, or a Chromium built with it");
252
- }
253
-
254
- /** One call, relayed. WebMCP takes the arguments as a JSON string and returns one. */
255
- async function tabCall(ev, name, args) {
256
- const out = await ev(`document.modelContext.getTools().then((ts) => {
257
- const t = ts.find((t) => t.name === ${JSON.stringify(name)});
258
- if (!t) throw new Error("no tool " + ${JSON.stringify(name)});
259
- return document.modelContext.executeTool(t, ${JSON.stringify(JSON.stringify(args ?? {}))});
260
- })`);
261
- try { const r = JSON.parse(out); if (Array.isArray(r?.content)) return r; } catch {}
262
- return { content: [{ type: "text", text: String(out) }] };
263
- }
264
-
265
151
  /** JSON-RPC over newline-delimited stdio. Returns `notify` for server → client messages. */
266
152
  export function serve(handlers, { input = process.stdin, output = process.stdout } = {}) {
267
153
  const send = (m) => output.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
@@ -282,7 +168,7 @@ const slug = (name) => String(name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").r
282
168
 
283
169
  const META = [
284
170
  { name: "list-apps",
285
- description: "List the Fangorn apps registered on chain whose agent cards verify: name, description, card URL, and the WebMCP tools each page offers. Filter with `query`. Call open-app on one to use its tools.",
171
+ description: "List the Fangorn apps registered on chain whose agent cards verify: name, description, card URL, and its appId. Filter with `query`. Call open-app on one to use its tools.",
286
172
  inputSchema: { type: "object", properties: { query: { type: "string", description: "Case-insensitive match on name or description" } } } },
287
173
  { name: "find-apps",
288
174
  description: "Find Fangorn apps for a question, ranked by what their data is about (e.g. \"where can I learn about weather?\", \"music\"). Each app carries its site, agent card, ERC-8004 agent with an 8004scan link, ERC-8004 reputation (feedback count and average), record count and bound block. Only apps whose cards verify on chain. `relevant: true` marks the ones worth opening; the rest are sorted, not relevant. With no query, lists every app. Then open-app.",
@@ -290,8 +176,8 @@ const META = [
290
176
  query: { type: "string", description: "What the person is looking for, in their words" },
291
177
  limit: { type: "integer", minimum: 1, maximum: 50, default: 10 } } } },
292
178
  { name: "open-app",
293
- description: "Verify an app's card against the chain and add its tools to this server as `<app>__<tool>`. An app that publishes views gets fast data tools (describe, search, get, similar, count, browse) with no browser. `page: true` also opens the app's page in a browser for its own WebMCP tools (slower). `app` is a name from list-apps, an appId, or a card URL.",
294
- inputSchema: { type: "object", properties: { app: { type: "string" }, page: { type: "boolean", description: "Also load the page's own WebMCP tools in a browser" } }, required: ["app"] } },
179
+ description: "Verify an app's card against the chain and add its tools to this server as `<app>__<tool>`. The tools are the app's own MCP server's (typically describe, search, get, similar, count, browse). `app` is a name from list-apps, an appId, or a card URL.",
180
+ inputSchema: { type: "object", properties: { app: { type: "string" } }, required: ["app"] } },
295
181
  { name: "call-app-tool",
296
182
  description: "Call one tool of an opened app. Same as calling `<app>__<tool>` directly.",
297
183
  inputSchema: { type: "object", properties: { app: { type: "string" }, tool: { type: "string" }, arguments: { type: "object" } }, required: ["app", "tool"] } },
@@ -302,52 +188,34 @@ const META = [
302
188
  * findApps() → { apps: [{ name, desc, card, appId }], rejected }
303
189
  * exploreApps(query) → { apps: ranked explore.js entries, ranked_by }
304
190
  * findByName(name) → { card, appId } | null (optional: a direct registry read)
305
- * resolveCard(url) → { card, page, verified, views }
306
- * viewTools(app) → { tools, call(name, args) } | null (no browser)
307
- * openTab(page) → ev
308
- * tabTools(ev), tabCall(ev, name, args)
191
+ * resolveCard(url) → { card, mcp }
192
+ * remoteTools(url) → { tools, call(name, args) } (the app's own MCP server)
309
193
  * notify(method) set after serve()
310
194
  */
311
195
  export function handlers(deps) {
312
- const open = new Map(); // slug → { name, card, verified, page, views?, pages?, tools }
196
+ const open = new Map(); // slug → { slug, name, card, appId, tools, call }
313
197
 
314
198
  const findOpen = (app) => open.get(slug(app)) ?? [...open.values()].find((o) => o.card === app || o.appId === app);
315
- const retool = (o) => {
316
- const taken = new Set(o.views?.tools.map((t) => t.name));
317
- // A page tool that shares a data tool's name is kept, under `page-<name>`.
318
- o.pageNames = new Map((o.pageTools ?? []).map((t) => [taken.has(t.name) ? `page-${t.name}` : t.name, t.name]));
319
- o.tools = [...(o.views?.tools ?? []), ...(o.pageTools ?? []).map((t) => ({ ...t, name: taken.has(t.name) ? `page-${t.name}` : t.name }))];
320
- };
321
- async function openPage(o) {
322
- o.ev = await deps.openTab(o.page);
323
- o.pageTools = await deps.tabTools(o.ev);
324
- }
325
199
 
326
- async function openApp(app, { page = false } = {}) {
200
+ async function openApp(app) {
327
201
  const hit = findOpen(app);
328
- if (hit && (hit.ev || !page)) return hit;
329
- let o = hit;
330
- if (!o) {
331
- let cardUrl = app, appId;
332
- if (!/^https?:\/\//.test(app)) {
333
- // One registry read when the name is the app's own; the scan only when it is not.
334
- trace(`open ${app}: lookup`);
335
- const a = await deps.findByName?.(app)
336
- ?? (await deps.findApps()).apps.find((x) => x.appId === app || slug(x.name) === slug(app));
337
- if (!a) throw new Error(`no verified app named ${JSON.stringify(app)}; see list-apps`);
338
- ({ card: cardUrl, appId } = a);
339
- }
340
- trace(`open ${app}: resolve ${cardUrl}`);
341
- const { card, page: url, verified, views, paid } = await deps.resolveCard(cardUrl);
342
- trace(`open ${app}: verified`);
343
- let s = slug(card.name ?? app);
344
- while (open.has(s)) s += "-";
345
- o = { slug: s, name: card.name, card: cardUrl, appId, verified, page: url,
346
- views: deps.viewTools?.({ name: card.name, desc: card.description, views, paid }) ?? null };
202
+ if (hit) return hit;
203
+ let cardUrl = app, appId;
204
+ if (!/^https?:\/\//.test(app)) {
205
+ // One registry read when the name is the app's own; the scan only when it is not.
206
+ trace(`open ${app}: lookup`);
207
+ const a = await deps.findByName?.(app)
208
+ ?? (await deps.findApps()).apps.find((x) => x.appId === app || slug(x.name) === slug(app));
209
+ if (!a) throw new Error(`no verified app named ${JSON.stringify(app)}; see list-apps`);
210
+ ({ card: cardUrl, appId } = a);
347
211
  }
348
- // The browser only when asked, or when there is no other way in.
349
- if (page || !o.views) await openPage(o);
350
- retool(o);
212
+ trace(`open ${app}: resolve ${cardUrl}`);
213
+ const { card, mcp } = await deps.resolveCard(cardUrl);
214
+ if (!mcp) throw new Error(`${card.name ?? cardUrl} has no MCP server (its card has no \`mcp\`)`);
215
+ let s = slug(card.name ?? app);
216
+ while (open.has(s)) s += "-";
217
+ const access = card.capabilities?.extensions?.find((e) => e.uri === APP_EXTENSION)?.params?.access ?? null;
218
+ const o = { slug: s, name: card.name, card: cardUrl, appId, ...await deps.remoteTools(mcp, { access }) };
351
219
  trace(`open ${app}: ready`);
352
220
  open.set(o.slug, o);
353
221
  deps.notify?.("notifications/tools/list_changed");
@@ -357,18 +225,8 @@ export function handlers(deps) {
357
225
  async function call(app, tool, args) {
358
226
  const o = findOpen(app);
359
227
  if (!o) throw new Error(`${app} is not open; call open-app first`);
360
- if (!o.pageNames?.has(tool)) {
361
- if (o.views?.tools.some((t) => t.name === tool)) return o.views.call(tool, args);
362
- throw new Error(`${o.slug} has no tool ${tool}${o.ev ? "" : " (its page tools load with open-app page: true)"}`);
363
- }
364
- const name = o.pageNames.get(tool);
365
- try { return await deps.tabCall(o.ev, name, args); }
366
- catch (e) {
367
- if (!/tab closed/.test(e.message)) throw e;
368
- // The tab or the whole browser went away. Reopen once; the tools keep their names.
369
- await openPage(o);
370
- return deps.tabCall(o.ev, name, args);
371
- }
228
+ if (!o.tools.some((t) => t.name === tool)) throw new Error(`${o.slug} has no tool ${tool}`);
229
+ return o.call(tool, args);
372
230
  }
373
231
 
374
232
  const run = {
@@ -378,7 +236,7 @@ export function handlers(deps) {
378
236
  return text({
379
237
  apps: apps.filter((a) => !q || `${a.name} ${a.desc}`.toLowerCase().includes(q)).map((a) => ({
380
238
  app: slug(a.name), name: a.name, description: a.desc, card: a.card, appId: a.appId,
381
- tools: a.tools, open: open.has(slug(a.name)),
239
+ open: open.has(slug(a.name)),
382
240
  })),
383
241
  unverified: rejected.length,
384
242
  });
@@ -388,10 +246,9 @@ export function handlers(deps) {
388
246
  return text({ query: query ?? null, ranked_by, apps: apps.slice(0, Math.min(50, Math.max(1, limit)))
389
247
  .map((a) => ({ ...a, open: open.has(a.app) })) });
390
248
  },
391
- "open-app": async ({ app, page }) => {
392
- const o = await openApp(app, { page });
393
- return text({ app: o.slug, name: o.name, verified: o.verified,
394
- data_tools: !!o.views, page_tools: o.ev ? o.page : false,
249
+ "open-app": async ({ app }) => {
250
+ const o = await openApp(app);
251
+ return text({ app: o.slug, name: o.name, card: o.card,
395
252
  call_as: `${o.slug}__<tool>, or call-app-tool`, tools: o.tools });
396
253
  },
397
254
  "call-app-tool": async ({ app, tool, arguments: args }) => call(app, tool, args),
@@ -402,7 +259,7 @@ export function handlers(deps) {
402
259
  protocolVersion: p.protocolVersion ?? "2025-06-18",
403
260
  capabilities: { tools: { listChanged: true } },
404
261
  serverInfo: { name: "fangorn-mcp", version: "0.1.0" },
405
- instructions: "Fangorn apps are static sites anchored on chain. list-apps to find one, open-app to load it, then call its tools as <app>__<tool>. Start with <app>__describe when it exists.",
262
+ instructions: "Fangorn apps are anchored on chain and each serves its own MCP. list-apps to find one, open-app to load it, then call its tools as <app>__<tool>. Start with <app>__describe when it exists.",
406
263
  }),
407
264
  ping: async () => ({}),
408
265
  "tools/list": async () => ({ tools: [
@@ -427,15 +284,22 @@ if (process.argv[2] === "--selfcheck") {
427
284
  const input = new PassThrough(), output = new PassThrough();
428
285
  const replies = [];
429
286
  createInterface({ input: output }).on("line", (l) => replies.push(JSON.parse(l)));
287
+ const posts = [];
288
+ // The app's Worker: initialize, tools/list, tools/call over JSON-RPC POSTs.
289
+ const remote = await remoteTools("https://k.test/mcp", async (url, init) => {
290
+ const m = JSON.parse(init.body);
291
+ posts.push(m.method);
292
+ const result = m.method === "tools/list" ? { tools: [{ name: "search" }] }
293
+ : m.method === "tools/call" ? text({ via: "remote", ...m.params }) : {};
294
+ return new Response(JSON.stringify({ jsonrpc: "2.0", id: m.id, result }), { headers: { "content-type": "application/json" } });
295
+ });
430
296
  let opened = 0;
431
297
  const deps = {
432
- findApps: async () => ({ apps: [{ name: "Kings Foil", desc: "trials", card: "https://k.test/card", appId: "0xk", tools: ["greet"] }], rejected: [{}] }),
298
+ findApps: async () => ({ apps: [{ name: "Kings Foil", desc: "trials", card: "https://k.test/card", appId: "0xk" },
299
+ { name: "Bare", desc: "", card: "https://b.test/card", appId: "0xb" }], rejected: [{}] }),
433
300
  exploreApps: async (q) => ({ ranked_by: q ? "meaning" : null, apps: [{ app: "kings-foil", name: "Kings Foil", relevant: true }, { app: "b", name: "B", relevant: false }] }),
434
- resolveCard: async () => ({ card: { name: "Kings Foil" }, page: "https://k.test/p", verified: true }),
435
- openTab: async () => (opened++ ? "ev" : "dead"), // the first tab dies
436
- tabTools: async () => [{ name: "greet", description: "Hi.", inputSchema: { type: "object" } }],
437
- tabCall: async (ev, name, args) => (name === "boom" ? Promise.reject(new Error("no tool boom"))
438
- : ev === "dead" ? Promise.reject(new Error("tab closed: x")) : text({ ev, name, args })),
301
+ resolveCard: async (url) => (url.includes("b.test") ? { card: { name: "Bare" }, mcp: null } : { card: { name: "Kings Foil" }, mcp: "https://k.test/mcp" }),
302
+ remoteTools: async () => (opened++, remote),
439
303
  };
440
304
  deps.notify = serve(handlers(deps), { input, output });
441
305
  const rpc = (id, method, params) => input.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
@@ -445,10 +309,10 @@ if (process.argv[2] === "--selfcheck") {
445
309
  const body = (id) => JSON.parse(res(id).result.content[0].text);
446
310
  const assert = (c, m) => { if (!c) throw new Error(m); };
447
311
 
448
- rpc(1, "initialize", {}); rpc(2, "tools/list"); call(3, "list-apps", { query: "TRIAL" }); call(4, "kings-foil__greet"); await wait();
312
+ rpc(1, "initialize", {}); rpc(2, "tools/list"); call(3, "list-apps", { query: "TRIAL" }); call(4, "kings-foil__search"); await wait();
449
313
  assert(res(1).result.capabilities.tools.listChanged, "advertises list_changed");
450
314
  assert(res(2).result.tools.length === 4, "only the meta tools before anything is open");
451
- assert(body(3).apps[0].app === "kings-foil" && body(3).unverified === 1, "list-apps filters and slugs");
315
+ assert(body(3).apps.length === 1 && body(3).apps[0].app === "kings-foil" && body(3).unverified === 1, "list-apps filters and slugs");
452
316
  assert(res(4).result.isError && /not open/.test(res(4).result.content[0].text), "an unopened app's tool is an error");
453
317
 
454
318
  call(30, "find-apps", { query: "trials", limit: 1 }); await wait();
@@ -456,40 +320,44 @@ if (process.argv[2] === "--selfcheck") {
456
320
 
457
321
  call(5, "open-app", { app: "Kings Foil" }); await wait();
458
322
  call(6, "open-app", { app: "https://k.test/card" }); await wait();
459
- assert(body(5).tools[0].name === "greet" && opened === 1, "open once, by name or by card URL");
460
- call(50, "kings-foil__greet", { who: "b" }); await wait();
461
- assert(body(50).ev === "ev" && opened === 2, "a dead tab is reopened and the call retried");
323
+ assert(body(5).tools[0].name === "search" && opened === 1, "open once, by name or by card URL");
462
324
  assert(replies.some((r) => r.method === "notifications/tools/list_changed"), "opening notifies");
463
- rpc(7, "tools/list"); call(8, "kings-foil__greet", { who: "a" }); call(9, "call-app-tool", { app: "kings-foil", tool: "greet" });
464
- call(10, "call-app-tool", { app: "kings-foil", tool: "boom" }); call(11, "open-app", { app: "nope" }); rpc(12, "nope"); await wait();
465
- assert(res(7).result.tools.some((t) => t.name === "kings-foil__greet"), "opened tools are listed under the app");
466
- assert(body(8).args.who === "a" && body(9).name === "greet", "both call paths reach the tab");
325
+ rpc(7, "tools/list"); call(8, "kings-foil__search", { query: "x" }); call(9, "call-app-tool", { app: "kings-foil", tool: "search" });
326
+ call(10, "call-app-tool", { app: "kings-foil", tool: "boom" }); call(11, "open-app", { app: "nope" }); call(13, "open-app", { app: "Bare" }); rpc(12, "nope"); await wait();
327
+ assert(res(7).result.tools.some((t) => t.name === "kings-foil__search"), "opened tools are listed under the app");
328
+ assert(body(8).via === "remote" && body(8).arguments.query === "x" && body(9).name === "search", "both call paths reach the app's server");
329
+ assert(posts.join() === "initialize,tools/list,tools/call,tools/call", `initialize, list, then calls: ${posts}`);
467
330
  assert(res(10).result.isError && res(11).result.isError, "failures are tool errors");
331
+ assert(res(13).result.isError && /no MCP server/.test(res(13).result.content[0].text), "an app without `mcp` cannot open");
468
332
  assert(res(12).error.code === -32601, "unknown method");
469
-
470
- // An app with views: data tools, no browser, until page: true.
471
- {
472
- const out2 = new PassThrough(), in2 = new PassThrough(), r2 = [];
473
- createInterface({ input: out2 }).on("line", (l) => r2.push(JSON.parse(l)));
474
- let tabs = 0;
475
- const d2 = { ...deps,
476
- resolveCard: async () => ({ card: { name: "Kings Foil" }, page: "https://k.test/p", verified: true, views: ["https://k.test/v"] }),
477
- viewTools: (app) => ({ tools: [{ name: "search" }, { name: "greet" }], call: async (n, a) => text({ via: "views", n, a, views: app.views }) }),
478
- openTab: async () => (tabs++, "ev"),
479
- };
480
- d2.notify = serve(handlers(d2), { input: in2, output: out2 });
481
- const q = (id, name, args) => in2.write(JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } }) + "\n");
482
- const got = (id) => r2.find((r) => r.id === id)?.result;
483
- q(1, "open-app", { app: "Kings Foil" }); await wait(); q(2, "kings-foil__search", { query: "x" }); await wait();
484
- assert(tabs === 0 && JSON.parse(got(1).content[0].text).data_tools, "views mean no browser");
485
- assert(JSON.parse(got(2).content[0].text).via === "views", "data tools answer from the views");
486
- q(3, "open-app", { app: "kings-foil", page: true }); await wait();
487
- const names = JSON.parse(got(3).content[0].text).tools.map((t) => t.name);
488
- assert(tabs === 1 && names.includes("search") && names.includes("greet") && names.includes("page-greet"), `page: true adds page tools, a clash renamed: ${names}`);
489
- q(4, "kings-foil__page-greet", { who: "p" }); q(5, "kings-foil__greet", {}); await wait();
490
- assert(JSON.parse(got(4).content[0].text).ev === "ev" && JSON.parse(got(5).content[0].text).via === "views", "each name reaches its own side");
491
- }
492
- console.log("mcp.js self-check ok — one server, apps opened on demand, data tools without a browser, page tools on request");
333
+ // A private app: a paid tool answers with PaymentRequired; with a wallet this server pays the
334
+ // call (under FANGORN_MCP_MAX_PRICE), or buys a pass when the card sells one under FANGORN_MCP_MAX_PASS.
335
+ const { generatePrivateKey } = await import("viem/accounts");
336
+ const req = { scheme: "exact", network: "eip155:84532", amount: "10000", asset: `0x${"aa".repeat(20)}`, payTo: `0x${"5e".repeat(20)}`,
337
+ maxTimeoutSeconds: 120, extra: { name: "USDC", version: "2" } };
338
+ const asked = { x402Version: 2, error: "paid", resource: { url: "https://p.test/mcp" }, accepts: [req] };
339
+ const seen = [];
340
+ const paidServer = async (url, init) => {
341
+ if (url === "https://p.test/pass") {
342
+ if (!init.headers?.["PAYMENT-SIGNATURE"]) return new Response("{}", { status: 402, headers: { "PAYMENT-REQUIRED": btoa(JSON.stringify({ ...asked, accepts: [{ ...req, amount: "2000000" }] })) } });
343
+ return new Response(JSON.stringify({ token: "pass.tok", expires: 9e9 }));
344
+ }
345
+ const m = JSON.parse(init.body);
346
+ const paid = m.params?._meta?.["x402/payment"], pass = init.headers.authorization === "Bearer pass.tok";
347
+ if (m.method === "tools/call") seen.push(pass ? "pass" : paid ? `paid ${paid.accepted.amount}` : "asked");
348
+ const result = m.method === "tools/list" ? { tools: [{ name: "search" }] } : m.method !== "tools/call" ? {}
349
+ : paid || pass ? text({ hits: [1] }) : { isError: true, structuredContent: asked, content: [{ type: "text", text: JSON.stringify(asked) }] };
350
+ return new Response(JSON.stringify({ jsonrpc: "2.0", id: m.id, result }), { headers: { "content-type": "application/json" } });
351
+ };
352
+ const broke = await remoteTools("https://p.test/mcp", paidServer, { wallet: undefined });
353
+ if (!(await broke.call("search", {})).content.at(-1).text.includes("no wallet")) throw new Error("no wallet: the price goes back to the agent");
354
+ const access = { pass_url: "https://p.test/pass", pass: "2000000", call: "10000", decimals: 6 };
355
+ const perCall = await remoteTools("https://p.test/mcp", paidServer, { access, wallet: generatePrivateKey(), maxCall: "0.05" });
356
+ if (JSON.parse((await perCall.call("search", {})).content[0].text).hits[0] !== 1) throw new Error("a paid call");
357
+ const passer = await remoteTools("https://p.test/mcp", paidServer, { access, wallet: generatePrivateKey(), maxPass: "5" });
358
+ await passer.call("search", {}); await passer.call("search", {});
359
+ if (seen.join() !== "asked,asked,paid 10000,asked,pass,pass") throw new Error(`payments: ${seen}`);
360
+ console.log("mcp.js self-check ok — one server, apps opened on demand, calls relayed to each app's own MCP server; a private app's tools paid per call or with a pass bought once, and with no wallet the price is handed back");
493
361
  process.exit(0);
494
362
  }
495
363
 
@@ -499,31 +367,39 @@ globalThis.fetch = cachingFetch();
499
367
  // host with no working IPv6, every attempt times out and fetch fails outright.
500
368
  net.setDefaultAutoSelectFamilyAttemptTimeout?.(2000);
501
369
  const vt = await import("./view-tools.js");
502
- const stale = staleFetch(globalThis.fetch);
503
370
  const fb = argv.indexOf("--from-block");
504
- const fromBlock = fb >= 0 ? BigInt(argv[fb + 1]) : DEFAULT_FROM_BLOCK;
505
- const headed = argv.includes("--headed");
506
- const ci = argv.indexOf("--cdp");
507
- const cdp = ci >= 0 ? argv[ci + 1] : process.env.FANGORN_MCP_CDP || undefined;
508
- let found, foundAt = 0, explored, exploredAt = 0;
371
+ // The SDK's deployment by default: pass --from-block for another one.
372
+ const fromBlock = fb >= 0 ? BigInt(argv[fb + 1]) : (await network()).appRegistryFromBlock;
373
+ let found, foundAt = 0, explored, exploredAt = 0, pub, warming;
374
+ const chainClient = async () => {
375
+ if (!pub) {
376
+ const [{ createPublicClient, http }, net] = await Promise.all([import("viem"), network()]);
377
+ pub = createPublicClient({ chain: net.chain, transport: http(net.rpcUrl) });
378
+ }
379
+ return pub;
380
+ };
381
+ const store = fileStore(`${CACHE}/logs-${(await network()).chain.id}.json`);
382
+ // The embedder is for find-apps' ranking alone. Loading it blocks the thread for
383
+ // seconds, so not at startup, where it would hold up list-apps.
384
+ const warmEmbedder = () => (warming ??= vt.warm({ cacheDir: `${CACHE}/models` }).then((ok) => ok ? trace("embedder ready")
385
+ : log(`embedder unavailable, searching by words: ${vt.warmError}`)));
509
386
  const deps = {
510
- // ponytail: cached for a minute. The scan is a few RPC calls plus a fetch per card.
387
+ // ponytail: cached for a minute. The logs since the last scan, plus a fetch per card.
511
388
  findApps: async () => {
512
389
  if (!found || Date.now() - foundAt > 60_000) {
513
- const { apps, rejected } = await listApps(await fangorn(), { fromBlock });
514
- found = { rejected, apps: await Promise.all(apps.map(async (a) => {
515
- const card = await (await fetch(a.card)).json().catch(() => ({}));
516
- return { ...a, tools: card.skills?.filter((s) => s.tags?.includes("webmcp")).map((s) => s.id) ?? [] };
517
- })) };
390
+ const [f, head] = await Promise.all([fangorn(), chainClient().then((c) => c.getBlockNumber())]);
391
+ const { apps, rejected } = await listApps(f, { fromBlock, toBlock: head, store });
392
+ found = { apps, rejected };
518
393
  foundAt = Date.now();
519
394
  }
520
395
  return found;
521
396
  },
522
397
  // ponytail: cached for a minute, like findApps. Two getLogs, a card and catalog per app.
523
398
  exploreApps: async (query) => {
399
+ if (query?.trim()) warmEmbedder(); // loads behind the directory read; a first download ranks by words
524
400
  if (!explored || Date.now() - exploredAt > 60_000) {
525
- const [ex, { createPublicClient, http }, { arbitrumSepolia }] = await Promise.all([import("../discover/explore.js"), import("viem"), import("viem/chains")]);
526
- explored = { ex, dir: await ex.readDirectory(createPublicClient({ chain: arbitrumSepolia, transport: http() })) };
401
+ const [ex, client, net] = await Promise.all([import("../discover/explore.js"), chainClient(), network()]);
402
+ explored = { ex, dir: await ex.readDirectory(client, { chain: ex.chainOf(net), store }) };
527
403
  exploredAt = Date.now();
528
404
  }
529
405
  const { ex, dir } = explored;
@@ -542,27 +418,17 @@ const deps = {
542
418
  return null;
543
419
  },
544
420
  resolveCard,
545
- viewTools: (app) => {
546
- const v = vt.viewTools(app, { fetchCatalog: stale });
547
- if (v) vt.warm({ cacheDir: `${CACHE}/models` }); // already started at initialize; this is the fallback
548
- return v;
549
- },
550
- openTab: (page) => openTab(page, { headed, cdp }),
551
- tabTools, tabCall,
421
+ remoteTools: (url, opts) => remoteTools(url, fetch, opts),
552
422
  };
553
423
  const h = handlers(deps);
554
424
  // The client starts this server with the session, long before an agent opens an
555
- // app: load the SDK and the embedding model then, behind the agent's first
556
- // thought. After the initialize reply, not before: both block the thread while
557
- // they load, and the client is waiting on that reply.
425
+ // app: load the SDK then, behind the agent's first thought. After the initialize
426
+ // reply, not before: it blocks the thread while it loads, and the client is
427
+ // waiting on that reply.
558
428
  const reply = h.initialize;
559
429
  h.initialize = async (p) => {
560
- setImmediate(() => {
561
- fangorn().then(() => trace("sdk ready"), () => {});
562
- vt.warm({ cacheDir: `${CACHE}/models` }).then((ok) => ok ? trace("embedder ready")
563
- : log(`embedder unavailable, searching by words: ${vt.warmError}`));
564
- });
430
+ setImmediate(() => fangorn().then(() => trace("sdk ready"), () => {}));
565
431
  return reply(p);
566
432
  };
567
433
  deps.notify = serve(h);
568
- log(`ready: apps from block ${fromBlock}; browser ${cdp ?? "local Chrome"}, connected on the first open-app`);
434
+ log(`ready: apps from block ${fromBlock}`);