@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.
- package/README.md +33 -3
- package/docs/app-to-agent.md +117 -60
- package/package.json +10 -5
- package/plugin/scripts/fx.mjs +7 -9
- package/site/dist/assets/{AccountNotFoundScreen-C4njXc69-BOfmiSH1.js → AccountNotFoundScreen-C4njXc69-DyVH4CEz.js} +1 -1
- package/site/dist/assets/{AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js → AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js} +1 -1
- package/site/dist/assets/{Address-H8kD1d9O-C85sJ79B.js → Address-H8kD1d9O-BdFZmRwI.js} +1 -1
- package/site/dist/assets/{AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js → AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js} +1 -1
- package/site/dist/assets/{AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js → AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js} +1 -1
- package/site/dist/assets/{ApiController-CYdiyu9B.js → ApiController-CTLbMCcH.js} +4 -4
- package/site/dist/assets/{ArrowsRightLeftIcon-cp60yobq.js → ArrowsRightLeftIcon-B8g9adpz.js} +1 -1
- package/site/dist/assets/{AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js → AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js} +1 -1
- package/site/dist/assets/{AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js → AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js} +1 -1
- package/site/dist/assets/{AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js → AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js} +1 -1
- package/site/dist/assets/{AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js → AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js} +1 -1
- package/site/dist/assets/{AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js → AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js} +1 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +68 -0
- package/site/dist/assets/{CaptchaScreen-DbpvkIGx-D8eDcAkc.js → CaptchaScreen-DbpvkIGx-7rSZIuM-.js} +1 -1
- package/site/dist/assets/{CheckCircleIcon-NCOQc0Gs.js → CheckCircleIcon-B1l17ioc.js} +1 -1
- package/site/dist/assets/{CheckCircleIcon-BM409mGI.js → CheckCircleIcon-BGSI8nKf.js} +1 -1
- package/site/dist/assets/{Checkbox-D1EDeo41-rHlvsZzP.js → Checkbox-D1EDeo41-BUfq0w99.js} +1 -1
- package/site/dist/assets/{ChevronDownIcon-CW-4MSpE.js → ChevronDownIcon-B6uJvPHu.js} +1 -1
- package/site/dist/assets/{Chip-CZKIKt9K-CutJG010.js → Chip-CZKIKt9K-r9Nk__0-.js} +1 -1
- package/site/dist/assets/{CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js → CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js} +1 -1
- package/site/dist/assets/{ConnectEmailForm-B-VRE0MN-N9J6T_YL.js → ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js} +1 -1
- package/site/dist/assets/{ConnectLedgerScreen-DWHABGot-h0uYIUi6.js → ConnectLedgerScreen-DWHABGot-CZcCt31O.js} +1 -1
- package/site/dist/assets/{ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js → ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js} +1 -1
- package/site/dist/assets/{ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js → ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js} +1 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +1 -0
- package/site/dist/assets/{ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js → ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js} +1 -1
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +74 -0
- package/site/dist/assets/{ConnectionStatusScreen-fUfROm9m-BeWLchcf.js → ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js} +1 -1
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +1 -0
- package/site/dist/assets/{ConnectorUtil-Cq4lWnfe.js → ConnectorUtil-DH4NePk5.js} +1 -1
- package/site/dist/assets/{CopyToClipboard-i_OQSBJr-DujUHNzW.js → CopyToClipboard-i_OQSBJr-_G88JCVR.js} +1 -1
- package/site/dist/assets/{CopyableText-CQapvaMr-DMIdpDaU.js → CopyableText-CQapvaMr-CGGfc_zM.js} +1 -1
- package/site/dist/assets/{CrossAppAuthScreen-CLw7ymZq-Csju14vF.js → CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js} +1 -1
- package/site/dist/assets/{CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js → CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js} +1 -1
- package/site/dist/assets/{DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js → DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js} +1 -1
- package/site/dist/assets/{DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js → DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js} +1 -1
- package/site/dist/assets/{DepositAddressAddressStepView-4vdkb55L-B3vY888G.js → DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js} +1 -1
- package/site/dist/assets/{DepositAddressScreen-DBLRs7aI-DmdQaFI2.js → DepositAddressScreen-DBLRs7aI-DeituDNh.js} +1 -1
- package/site/dist/assets/{DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js → DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js} +1 -1
- package/site/dist/assets/{EmailInputForm-Cqo1mda8-B2NG3K5V.js → EmailInputForm-Cqo1mda8-BmbxvPHC.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js → EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js → EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js → EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js → EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js → EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js} +1 -1
- package/site/dist/assets/{EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js → EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js} +1 -1
- package/site/dist/assets/{EnrollTotp-Dibew4dp-SA2pvNEc.js → EnrollTotp-Dibew4dp-BXbYV8nI.js} +1 -1
- package/site/dist/assets/{EnvelopeIcon-Dh2vSVj4.js → EnvelopeIcon-DjPofnRI.js} +1 -1
- package/site/dist/assets/{ErrorBanner-BcpGRt0h-BKOYTyCi.js → ErrorBanner-BcpGRt0h-BhH6_V-9.js} +1 -1
- package/site/dist/assets/{ErrorMessage-D8VaAP5m-CrZOnUyg.js → ErrorMessage-D8VaAP5m-Bs7uX2a9.js} +1 -1
- package/site/dist/assets/{ErrorScreen-DipdTWzE-c8g8En21.js → ErrorScreen-DipdTWzE-BgPRgnOb.js} +1 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +1 -0
- package/site/dist/assets/{ExclamationCircleIcon-DUA2wMfO.js → ExclamationCircleIcon-BE-WGrut.js} +1 -1
- package/site/dist/assets/{ExclamationTriangleIcon-eZfaqcFq.js → ExclamationTriangleIcon-Ca1Xlc_f.js} +1 -1
- package/site/dist/assets/{FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js → FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js} +1 -1
- package/site/dist/assets/{FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js → FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js} +1 -1
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +491 -0
- package/site/dist/assets/{FingerPrintIcon-Dm6kxmg7.js → FingerPrintIcon-DOCbS93y.js} +1 -1
- package/site/dist/assets/{FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js → FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js} +1 -1
- package/site/dist/assets/{FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js → FundWalletMethodHeader-BWNkokCT-LPv35wkD.js} +1 -1
- package/site/dist/assets/{FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js → FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js} +1 -1
- package/site/dist/assets/{FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js → FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js} +1 -1
- package/site/dist/assets/{FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js → FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js} +1 -1
- package/site/dist/assets/{GlobeAltIcon-vqrVV5ho.js → GlobeAltIcon-BBRl2MMn.js} +1 -1
- package/site/dist/assets/{GooglePay-B53WnudL-BX5_WtCx.js → GooglePay-B53WnudL-BGiVpB8y.js} +1 -1
- package/site/dist/assets/{HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js → HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js} +3 -3
- package/site/dist/assets/{InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js → InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js} +1 -1
- package/site/dist/assets/{InfoBanner-Cb3p1z12-DLBl_Ywy.js → InfoBanner-Cb3p1z12-_YqQmRiu.js} +1 -1
- package/site/dist/assets/{InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js → InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js} +1 -1
- package/site/dist/assets/{InstallWalletScreen-DTA8dwLv-CxreyXoD.js → InstallWalletScreen-DTA8dwLv-BMU_wa61.js} +1 -1
- package/site/dist/assets/{JsonTree-BHzNC-ic-DcNcbSoM.js → JsonTree-BHzNC-ic-DcVr0YF0.js} +1 -1
- package/site/dist/assets/{LabelXs-oqZNqbm_-iafrj25N.js → LabelXs-oqZNqbm_-CNprGVQM.js} +1 -1
- package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +1 -0
- package/site/dist/assets/{Layouts-BMRfo5hw-CeCOyCLK.js → Layouts-BMRfo5hw-e-sepi6F.js} +1 -1
- package/site/dist/assets/{Link-BdDilT2T-BEDEdHuS.js → Link-BdDilT2T-BdwlRJch.js} +1 -1
- package/site/dist/assets/{LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js → LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js} +1 -1
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +1 -0
- package/site/dist/assets/{LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js → LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js} +1 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +1 -0
- package/site/dist/assets/{LinkPhoneScreen-oIUpfj0w-C0DuD14j.js → LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js} +1 -1
- package/site/dist/assets/{Loader-UAZqxsnw-Cy7hEE2f.js → Loader-UAZqxsnw-DJEVblm1.js} +1 -1
- package/site/dist/assets/{LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js → LoadingSkeleton-BMsgO5PV-D2atjsP5.js} +1 -1
- package/site/dist/assets/{LockClosedIcon-BgY_5v_Z.js → LockClosedIcon-9EFzWK1C.js} +1 -1
- package/site/dist/assets/{LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js → LoginFailedScreen-CN0dJV6U-By3Si77a.js} +1 -1
- package/site/dist/assets/{ManualTransferScreen-CHqcuqbs-tXiZDHmN.js → ManualTransferScreen-CHqcuqbs-BAHPY8rs.js} +1 -1
- package/site/dist/assets/{MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js → MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js} +1 -1
- package/site/dist/assets/{MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js → MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js} +1 -1
- package/site/dist/assets/{MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js → MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js} +1 -1
- package/site/dist/assets/{MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js → MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js} +1 -1
- package/site/dist/assets/{ModalFooter-BCuEK_nz-c-khvG4Y.js → ModalFooter-BCuEK_nz-C_71qsaz.js} +1 -1
- package/site/dist/assets/{MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js → MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js} +1 -1
- package/site/dist/assets/{OAuthStatusScreen-ufbOa-da-CeiGcSia.js → OAuthStatusScreen-ufbOa-da-BjAb_WsY.js} +1 -1
- package/site/dist/assets/{OpenLink-CUpJ1mOr-DfAvJRyK.js → OpenLink-CUpJ1mOr-BLWFsPOW.js} +1 -1
- package/site/dist/assets/{PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js → PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js} +1 -1
- package/site/dist/assets/{PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js → PasskeyStatusScreen-Bla8MaOk-DCEg761l.js} +1 -1
- package/site/dist/assets/{PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js → PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js} +1 -1
- package/site/dist/assets/{PhoneIcon-CvIiPwYQ.js → PhoneIcon-CZ47md80.js} +1 -1
- package/site/dist/assets/{PinInput-DbZ0b1i1-CDXZBdQx.js → PinInput-DbZ0b1i1-BrTXu3Km.js} +1 -1
- package/site/dist/assets/{PrivyPluginContext-DA-yS5vO-CtQWBiV2.js → PrivyPluginContext-DA-yS5vO-CY60yubk.js} +1 -1
- package/site/dist/assets/{QrCode-DbnZwlkH-h6Bhoe5U.js → QrCode-DbnZwlkH-CcMrWV5D.js} +2 -2
- package/site/dist/assets/{RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js → RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js} +1 -1
- package/site/dist/assets/{RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js → RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js} +1 -1
- package/site/dist/assets/{RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js → RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js} +1 -1
- package/site/dist/assets/{SIWXUtil-TUCxa7aA.js → SIWXUtil-Cpiv91-M.js} +1 -1
- package/site/dist/assets/{Screen-D30meWsY-CCeVNuWs.js → Screen-D30meWsY-vEusJ7KX.js} +1 -1
- package/site/dist/assets/{ScreenHeader-CHmc4-Lu-DjyrH7IK.js → ScreenHeader-CHmc4-Lu-BvGTLg53.js} +1 -1
- package/site/dist/assets/{ScreenLayout-OwaQQcxH-CrQM6kvA.js → ScreenLayout-OwaQQcxH-cfOTmyl0.js} +1 -1
- package/site/dist/assets/{SelectSourceAsset-CWFog6gm-D7yTw87h.js → SelectSourceAsset-CWFog6gm-DxYklZYs.js} +1 -1
- package/site/dist/assets/{SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js → SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js} +1 -1
- package/site/dist/assets/{SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js → SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js} +1 -1
- package/site/dist/assets/{ShieldCheckIcon-o6RHYev1.js → ShieldCheckIcon-eFjsosCA.js} +1 -1
- package/site/dist/assets/{SignRequestScreen-BHKv32KR-DDpMYyJ1.js → SignRequestScreen-BHKv32KR-z8ixfIma.js} +1 -1
- package/site/dist/assets/{SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js → SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js} +1 -1
- package/site/dist/assets/{StackedContainer-B2vaEl56-eQCM2cmv.js → StackedContainer-B2vaEl56-Do5dXB8-.js} +1 -1
- package/site/dist/assets/{StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js → StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js} +1 -1
- package/site/dist/assets/{Subtitle-CV-2yKE4-zxWw2m1Q.js → Subtitle-CV-2yKE4-DV0o1av3.js} +1 -1
- package/site/dist/assets/{TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js → TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js} +1 -1
- package/site/dist/assets/{Title-BnzYV3Is-B_JpTatT.js → Title-BnzYV3Is-C3qOPnO_.js} +1 -1
- package/site/dist/assets/{TodoList-DnyULl18-C8Eyy28X.js → TodoList-DnyULl18-BvxvubbF.js} +1 -1
- package/site/dist/assets/{TransactionDetails-e9nesl30-C4MpIHR-.js → TransactionDetails-e9nesl30-WWflkOIi.js} +1 -1
- package/site/dist/assets/{TransactionErrorView-BDBNqA_f-p1T9WVMx.js → TransactionErrorView-BDBNqA_f-CotYKcgf.js} +1 -1
- package/site/dist/assets/{TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js → TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js} +1 -1
- package/site/dist/assets/{TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js → TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js} +1 -1
- package/site/dist/assets/{TurnstileWrapper-B7sNddys-Jwy1tJtm.js → TurnstileWrapper-B7sNddys-oPbuMTQt.js} +1 -1
- package/site/dist/assets/{UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js → UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js} +1 -1
- package/site/dist/assets/{UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js → UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js} +1 -1
- package/site/dist/assets/{UserLimitReachedScreen-5we2DHn8-D7m70C4b.js → UserLimitReachedScreen-5we2DHn8-CaVN0bti.js} +1 -1
- package/site/dist/assets/{Value-DTgR824E-BRuIGiec.js → Value-DTgR824E-Bp6WGZZm.js} +1 -1
- package/site/dist/assets/{WalletCards-DH1rqayz-Bgp9Fsgs.js → WalletCards-DH1rqayz-D3cOTb9x.js} +1 -1
- package/site/dist/assets/{WalletIcon-gKf0gTw6.js → WalletIcon-DOIZhb2l.js} +1 -1
- package/site/dist/assets/{WalletInfoCard-sq_2wgFg-DpNSAGte.js → WalletInfoCard-sq_2wgFg-bOqfl5MA.js} +1 -1
- package/site/dist/assets/{WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js → WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js} +1 -1
- package/site/dist/assets/{WalletLink-DK5LB5Ha-DHaL350n.js → WalletLink-DK5LB5Ha-DjUZvqs-.js} +1 -1
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +1 -0
- package/site/dist/assets/{WarningBanner-ZZqCEtZK-2eU4Qo74.js → WarningBanner-ZZqCEtZK-Bc2YKZb7.js} +1 -1
- package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +1 -0
- package/site/dist/assets/{_esm-BdeM6pxJ.js → _esm-BZP3cXTv.js} +1 -1
- package/site/dist/assets/_esm-ZmzBJpEs.js +6 -0
- package/site/dist/assets/{accounts-D96UKWkC.js → accounts-D4AXF17G.js} +1 -1
- package/site/dist/assets/accounts-IkdFx9nq.js +1 -0
- package/site/dist/assets/{baseSepolia-DX-qGsik.js → baseSepolia-Bn1j1Ysw.js} +1 -1
- package/site/dist/assets/{basic-oIwwSyu4.js → basic-Cz1YiCSk.js} +4 -4
- package/site/dist/assets/{ccip-BZvRTL6g.js → ccip-B1J3oTZx.js} +2 -2
- package/site/dist/assets/ccip-Bd7261zy.js +1 -0
- package/site/dist/assets/{ccip-65sIfSPR.js → ccip-D6EHKpjI.js} +1 -1
- package/site/dist/assets/{chain-DM3oH6Px.js → chain-scefkYIH.js} +3 -6
- package/site/dist/assets/check-DHnllI8N.js +1 -0
- package/site/dist/assets/{chevron-down-BSsbXkK-.js → chevron-down-1y-r4bGO.js} +1 -1
- package/site/dist/assets/{circle-alert-DWaeLZNj.js → circle-alert-OwxamXUX.js} +1 -1
- package/site/dist/assets/{circle-check-big-Chb1ivw9.js → circle-check-big-3sZGVC10.js} +1 -1
- package/site/dist/assets/{circle-x-AV9Cet0R.js → circle-x-DQjNUV9J.js} +1 -1
- package/site/dist/assets/client-B-BpRtwY.js +1 -0
- package/site/dist/assets/client-BfIR1FvH.js +9 -0
- package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +3 -0
- package/site/dist/assets/{copy-Ckr0AkL6.js → copy-DMOX_yn7.js} +1 -1
- package/site/dist/assets/core-GIEmxmAa.js +12 -0
- package/site/dist/assets/{createLucideIcon-IPW50ckY.js → createLucideIcon-BsERoFTA.js} +1 -1
- package/site/dist/assets/{defineChain-CiuCXUZW.js → defineChain-B-SQDLTG.js} +1 -1
- package/site/dist/assets/{dijkstra-Drfl93V4.js → dijkstra-CY95afk4.js} +1 -1
- package/site/dist/assets/dist-Cp6IXeJd.js +1 -0
- package/site/dist/assets/{dist-Cm7pNAtL.js → dist-CrNxmdZj.js} +1 -1
- package/site/dist/assets/{dist-DnGANz51.js → dist-DYGtf4F0.js} +4 -4
- package/site/dist/assets/embed.worker-Cnl4VGr3.js +1 -0
- package/site/dist/assets/{errors-BQ3N-RVt-BLqjTPNR.js → errors-BQ3N-RVt-CZPAWCgC.js} +1 -1
- package/site/dist/assets/{esm-B5cbIQaz.js → esm-Co94ENUV.js} +17 -17
- package/site/dist/assets/{esm-ByALO6mA.js → esm-bb8SANZh.js} +1 -1
- package/site/dist/assets/{ethers-zTjbodk5-wHhL_BQ1.js → ethers-zTjbodk5-Cs648CnD.js} +1 -1
- package/site/dist/assets/{eventemitter3-CL76jPsW.js → eventemitter3-Dzc_0Uq2.js} +1 -1
- package/site/dist/assets/{external-link-D-uehwM2.js → external-link-BBPG3rG7.js} +1 -1
- package/site/dist/assets/{farcaster-DPlSjvF5-BqTvDXBR.js → farcaster-DPlSjvF5-CzBCmgyL.js} +1 -1
- package/site/dist/assets/{features-DQ0UYy6K.js → features-CuiHbgEJ.js} +1 -1
- package/site/dist/assets/{fingerprint-pattern-D8ytA16Q.js → fingerprint-pattern-CjLtyIxB.js} +1 -1
- package/site/dist/assets/floating-ui.react-DmaCfayr.js +1 -0
- package/site/dist/assets/{floating-ui.react-dom-DKWOL9UG.js → floating-ui.react-dom-CaoMF8mP.js} +1 -1
- package/site/dist/assets/{get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js → get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js} +1 -1
- package/site/dist/assets/{getErc20Balance-jXNpFf0N-Bb9WM31M.js → getErc20Balance-jXNpFf0N-q4bV8kQq.js} +1 -1
- package/site/dist/assets/{getErc20TokenInfo-BNTrzLMP-KPrdn04K.js → getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js} +1 -1
- package/site/dist/assets/{guards-BIW-GiMJ-CHfXo8XI.js → guards-BIW-GiMJ-D3tWTQAR.js} +1 -1
- package/site/dist/assets/{hourglass-CPG1HZmf.js → hourglass-D_GfQdJr.js} +1 -1
- package/site/dist/assets/{id-UqEwL2BI-BkKm533U.js → id-UqEwL2BI-CTlvRMA5.js} +1 -1
- package/site/dist/assets/{index-BdEw5wyl-ChSJvPZk.js → index-BdEw5wyl-kLbhqLbv.js} +1 -1
- package/site/dist/assets/index-C2h3EneX.js +8 -0
- package/site/dist/assets/{index-CWARkn2w-Cezl_uBr.js → index-CWARkn2w-DxuckIFn.js} +1 -1
- package/site/dist/assets/{index-DvMddJcd-BatvgC-S.js → index-DvMddJcd-CNBb4VUn.js} +1 -1
- package/site/dist/assets/{index-YiCw0Y_x-CX5i3Z87.js → index-YiCw0Y_x-Czl0tWfF.js} +5 -6
- package/site/dist/assets/{index.es-BmqyoBEM.js → index.es-Crd0TyUS.js} +1 -1
- package/site/dist/assets/index.es-DQbup1WG.js +3 -0
- package/site/dist/assets/{isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js → isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js} +1 -1
- package/site/dist/assets/{jsx-runtime-BbDfbRii.js → jsx-runtime-CndCZlyt.js} +1 -1
- package/site/dist/assets/{landmark-CH6LV6F0.js → landmark-BZPqMFEf.js} +1 -1
- package/site/dist/assets/lib-DGtzi2DK.js +1 -0
- package/site/dist/assets/{localBatchGatewayRequest-3kLrX_c7.js → localBatchGatewayRequest-B71J8sQ9.js} +7 -10
- package/site/dist/assets/{lock-DcskI4Bb.js → lock-DyN_c-ud.js} +1 -1
- package/site/dist/assets/{mail-Bs0r5yOi.js → mail-V_mcPb64.js} +1 -1
- package/site/dist/assets/{modal-context-CwmSeFcv-DruX9J1E.js → modal-context-CwmSeFcv-DPWNJrfK.js} +1 -1
- package/site/dist/assets/{ofetch.CWycOUEr-CHp-crOQ.js → ofetch.CWycOUEr-Duby3iHT.js} +1 -1
- package/site/dist/assets/parseEther-CHS0GNkM.js +1 -0
- package/site/dist/assets/{pkce-DXF0yKjP-ChQhE0st.js → pkce-DXF0yKjP-7_VY-d83.js} +1 -1
- package/site/dist/assets/preload-helper-CRzLMJPS.js +1 -0
- package/site/dist/assets/{privy-context-81KmsBw2-CjWGzyg7.js → privy-context-81KmsBw2-DiR6-Yq8.js} +1 -1
- package/site/dist/assets/{react-DrJ0MmMO.js → react-BfYA1q0G.js} +1 -1
- package/site/dist/assets/react-CK2HRI1t.js +1 -0
- package/site/dist/assets/{react-DQnMl2Xv.js → react-DxGeY8B8.js} +1 -1
- package/site/dist/assets/{react-dom-5dW7aKoP.js → react-dom-BI2Koe4o.js} +1 -1
- package/site/dist/assets/{reservoir-x-nGuZkT-BMX127i6.js → reservoir-x-nGuZkT-BguVFH3D.js} +1 -1
- package/site/dist/assets/roles-BDPDWrwo.js +1 -0
- package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +1 -0
- package/site/dist/assets/secp256k1-BeYL8aZl.js +1 -0
- package/site/dist/assets/{secp256k1-_MleGpqL.js → secp256k1-Bh4MSkhz.js} +1 -1
- package/site/dist/assets/secp256k1-Ci4cqHl0.js +1 -0
- package/site/dist/assets/secp256k1-DsxXqbXv.js +1 -0
- package/site/dist/assets/secp256k1-N9RUmafO.js +1 -0
- package/site/dist/assets/{serializeAuthorizationList-CX8hGL56.js → serializeAuthorizationList-DGMik9Wx.js} +1 -1
- package/site/dist/assets/{serializeSignature-DfxkPnu-.js → serializeSignature-CWKpxoab.js} +1 -1
- package/site/dist/assets/sha2-C6s7zZmb.js +1 -0
- package/site/dist/assets/{shared-DRZUmnL5-xqAUARWo.js → shared-DRZUmnL5-BewE5wK8.js} +1 -1
- package/site/dist/assets/{shared-FM0rljBt--Avg6q2R.js → shared-FM0rljBt-CxLACri_.js} +1 -1
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +1 -0
- package/site/dist/assets/{simulateTransaction-CfuOrae4-64lWYJAL.js → simulateTransaction-CfuOrae4-PjA_tGjX.js} +1 -1
- package/site/dist/assets/{smartphone-CcEZ-XAy.js → smartphone-3p8nFSTE.js} +1 -1
- package/site/dist/assets/{storage-Ufl800Ne-D0bZBw-H.js → storage-Ufl800Ne-1qCR2JMO.js} +7 -8
- package/site/dist/assets/{stringify-BlDm7tuc.js → stringify-DqUj9VzO.js} +2 -3
- package/site/dist/assets/stripe.esm-DEi2D291.js +2 -0
- package/site/dist/assets/{styles-BsotlekN-CXEB4Ud9.js → styles-BsotlekN-Aq27nZGc.js} +1 -1
- package/site/dist/assets/{styles-Cm9H63WO-Br4QtzUL.js → styles-Cm9H63WO-p_ic_dxE.js} +1 -1
- package/site/dist/assets/{styles-DVyDvTdj-BTABn2K8.js → styles-DVyDvTdj-CjsWqAld.js} +1 -1
- package/site/dist/assets/{styles-hslXy6xG-D-49b1rk.js → styles-hslXy6xG-D5QHIrJU.js} +1 -1
- package/site/dist/assets/{telegram-B-JqnkqZ-RBoY3PLx.js → telegram-B-JqnkqZ-Dqkm4rDn.js} +1 -1
- package/site/dist/assets/{to-ui-error-DWe-qwS3-Cx2bk1kZ.js → to-ui-error-DWe-qwS3-Bi1HUQI9.js} +1 -1
- package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +2 -0
- package/site/dist/assets/transformers.web-BTyOEJ0C.js +40 -0
- package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
- package/site/dist/assets/{triangle-alert-CWjYH75H.js → triangle-alert-BG3Qw-Hq.js} +1 -1
- package/site/dist/assets/{twitch-5IOe4sIQ-lDtgk0os.js → twitch-5IOe4sIQ-CaXHFlAu.js} +1 -1
- package/site/dist/assets/{use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js → use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js} +1 -1
- package/site/dist/assets/{use-unlink-wallet-BKK4_5Ur-CC2pq27z.js → use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js} +1 -1
- package/site/dist/assets/{useActiveWallet-B-yZYGc6-D7Ik-LxJ.js → useActiveWallet-B-yZYGc6-BkK6rFBm.js} +1 -1
- package/site/dist/assets/{useGetSolPrice-x7gfUIHJ-D1hmQfpA.js → useGetSolPrice-x7gfUIHJ-CLVXqv6I.js} +1 -1
- package/site/dist/assets/{useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js → useGetTokenPrice-CaoUkb79-DAXXs3rs.js} +1 -1
- package/site/dist/assets/{useI18n-C-5n_noF-BcqC76yj.js → useI18n-C-5n_noF-CGpkgIVy.js} +1 -1
- package/site/dist/assets/{useWalletBalance-CZnP5f_f-Dw2GIJ4g.js → useWalletBalance-CZnP5f_f-Cfd64m4n.js} +1 -1
- package/site/dist/assets/{useWallets-X1ndvJ_U-DRjz7uYD.js → useWallets-X1ndvJ_U-CAYqg7Za.js} +1 -1
- package/site/dist/assets/{w3m-modal-BeVrBOIf.js → w3m-modal-B4H_uiPH.js} +2 -2
- package/site/dist/assets/{wallet-Bn85Sg2W.js → wallet-BNI12IYk.js} +1 -1
- package/site/dist/assets/wallet-CITSaPlB.js +2 -0
- package/site/dist/assets/{wallet-connect-BvJtUsDt-Y03hhqFN.js → wallet-connect-BvJtUsDt-CVlSNIXP.js} +1 -1
- package/site/dist/assets/{walletconnect-registry-xxG8XLCb-D1SSFvMp.js → walletconnect-registry-xxG8XLCb-BpzpBq5s.js} +1 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +12 -0
- package/site/dist/assets/{wui-ux-by-reown-D34ILNLS.js → wui-ux-by-reown-XyHdtV1O.js} +1 -1
- package/site/dist/assets/x-gjDrY2rk.js +1 -0
- package/site/dist/index.html +5 -5
- package/site/feed.js +16 -1
- package/site/no-model.js +4 -0
- package/site/worker.js +586 -59
- package/src/agent/agent-card.js +20 -11
- package/src/agent/mcp.js +190 -324
- package/src/agent/view-tools.js +46 -13
- package/src/agent/x402.js +340 -44
- package/src/core/embed.js +51 -52
- package/src/core/embed.worker.js +1 -1
- package/src/core/shard.js +7 -3
- package/src/discover/apps.js +41 -5
- package/src/discover/explore.js +50 -30
- package/src/market/reactions.js +2 -2
- package/src/publish/devnet.js +95 -0
- package/src/publish/init.js +74 -0
- package/src/publish/lint.js +2 -1
- package/src/publish/ship.js +559 -267
- package/src/publish/view-cli.js +14 -10
- package/src/publish/view.js +355 -218
- package/src/taste/taste-doc.js +3 -2
- package/src/taste/taste.js +2 -2
- package/src/taste/vault.js +3 -1
- package/template/README.md +49 -0
- package/template/app.json +27 -0
- package/template/eval/goals.md +7 -0
- package/template/eval/golden.jsonl +0 -0
- package/template/gitignore +3 -0
- package/template/package.json +13 -0
- package/template/wrangler.json +12 -0
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
- package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
- package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
- package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
- package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
- package/site/dist/assets/ccip-DA_zG341.js +0 -1
- package/site/dist/assets/check-BPgcwEPP.js +0 -1
- package/site/dist/assets/client-CZG0ndth.js +0 -2
- package/site/dist/assets/client-Dsa7_3V3.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
- package/site/dist/assets/core-snws5xwv.js +0 -12
- package/site/dist/assets/dist-Dl07uGq1.js +0 -1
- package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
- package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
- package/site/dist/assets/index-1HYFYA-W.js +0 -8
- package/site/dist/assets/index.es-C1v1or--.js +0 -3
- package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
- package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
- package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
- package/site/dist/assets/react-DCXkXSrv.js +0 -1
- package/site/dist/assets/roles-32M1l9jI.js +0 -1
- package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
- package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
- package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
- package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
- package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
- package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
- package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
- package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
- package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
- package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
- package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
- 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
|
|
2
|
+
// Every Fangorn app's tools, behind one stdio MCP server.
|
|
3
3
|
//
|
|
4
|
-
// fangorn-mcp [--
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
-
//
|
|
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
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
|
40
|
+
const [{ Fangorn }, { generatePrivateKey }] = await Promise.all([
|
|
46
41
|
import("@fangorn-network/sdk"), import("viem/accounts")]);
|
|
47
|
-
client = Fangorn.create({ privateKey: generatePrivateKey(), config:
|
|
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 →
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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>`.
|
|
294
|
-
inputSchema: { type: "object", properties: { app: { type: "string" }
|
|
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,
|
|
306
|
-
*
|
|
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,
|
|
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
|
|
200
|
+
async function openApp(app) {
|
|
327
201
|
const hit = findOpen(app);
|
|
328
|
-
if (hit
|
|
329
|
-
let
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
361
|
-
|
|
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
|
-
|
|
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
|
|
392
|
-
const o = await openApp(app
|
|
393
|
-
return text({ app: o.slug, name: o.name,
|
|
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
|
|
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"
|
|
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" },
|
|
435
|
-
|
|
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-
|
|
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 === "
|
|
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-
|
|
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-
|
|
466
|
-
assert(body(8).
|
|
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
|
-
//
|
|
471
|
-
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
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
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
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
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
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,
|
|
526
|
-
explored = { ex, dir: await ex.readDirectory(
|
|
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
|
-
|
|
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
|
|
556
|
-
//
|
|
557
|
-
//
|
|
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}
|
|
434
|
+
log(`ready: apps from block ${fromBlock}`);
|