@fangorn-network/westmarch 2026.10.29 → 2026.11.4-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 +5 -5
- package/docs/app-to-agent.md +61 -625
- package/package.json +12 -17
- package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
- package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
- package/site/dist/assets/index-Djs8f08m.js +8 -0
- package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/site/dist/assets/{transformers.web-BTyOEJ0C.js → transformers.web-CwR_IYQV.js} +1 -1
- package/site/dist/index.html +3 -4
- package/site/worker.js +115 -338
- package/src/agent/agent-card.js +33 -45
- package/src/agent/mcp.js +168 -371
- package/src/agent/tools.js +2 -1
- package/src/agent/view-tools.js +4 -56
- package/src/agent/x402.js +182 -369
- package/src/core/roles.js +6 -6
- package/src/core/shard.js +4 -4
- package/src/discover/apps.js +67 -81
- package/src/discover/directory.js +2 -2
- package/src/market/reactions.js +21 -15
- package/src/publish/devnet.js +69 -18
- package/src/publish/init.js +1 -1
- package/src/publish/ship.js +74 -275
- package/src/publish/view.js +52 -53
- package/template/README.md +9 -11
- package/template/package.json +1 -1
- package/plugin/scripts/fx.mjs +0 -266
- package/site/dist/assets/AccountNotFoundScreen-C4njXc69-DyVH4CEz.js +0 -1
- package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-Dz1--SS9.js +0 -31
- package/site/dist/assets/Address-H8kD1d9O-BdFZmRwI.js +0 -17
- package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-CxaCAisL.js +0 -6
- package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-JGk7vndN.js +0 -1
- package/site/dist/assets/ApiController-CTLbMCcH.js +0 -5
- package/site/dist/assets/ArrowsRightLeftIcon-B8g9adpz.js +0 -1
- package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-CXD9_OP8.js +0 -1
- package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-BtJCglvU.js +0 -1
- package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-BgC2TsqH.js +0 -1
- package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-B9OzLeEO.js +0 -103
- package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-C7r5snUL.js +0 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-BTV0NRio.js +0 -68
- package/site/dist/assets/CaptchaScreen-DbpvkIGx-7rSZIuM-.js +0 -1
- package/site/dist/assets/CheckCircleIcon-B1l17ioc.js +0 -1
- package/site/dist/assets/CheckCircleIcon-BGSI8nKf.js +0 -1
- package/site/dist/assets/Checkbox-D1EDeo41-BUfq0w99.js +0 -52
- package/site/dist/assets/ChevronDownIcon-B6uJvPHu.js +0 -1
- package/site/dist/assets/Chip-CZKIKt9K-r9Nk__0-.js +0 -26
- package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DWWNHxmR.js +0 -16
- package/site/dist/assets/ConnectEmailForm-B-VRE0MN-CRQwS0Xu.js +0 -3
- package/site/dist/assets/ConnectLedgerScreen-DWHABGot-CZcCt31O.js +0 -10
- package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-C5IOf-SQ.js +0 -1
- package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-cwg74F3A.js +0 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-D28oeK6b.js +0 -1
- package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-EEdkKeTp.js +0 -95
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-C2mEzukj.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-CH_7r8iV.js +0 -7
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-Z33kis0B.js +0 -1
- package/site/dist/assets/ConnectorUtil-DH4NePk5.js +0 -21
- package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
- package/site/dist/assets/CopyToClipboard-i_OQSBJr-_G88JCVR.js +0 -39
- package/site/dist/assets/CopyableText-CQapvaMr-CGGfc_zM.js +0 -28
- package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-C9Okoc0V.js +0 -1
- package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-D2l6hdSl.js +0 -22
- package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-Dda3KKqS.js +0 -1
- package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-DG8kJPt1.js +0 -1
- package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-CcOUfn6f.js +0 -187
- package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DeituDNh.js +0 -8
- package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-FDg6K14-.js +0 -1
- package/site/dist/assets/EmailInputForm-Cqo1mda8-BmbxvPHC.js +0 -124
- package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-B05CQ9rx.js +0 -1
- package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-Cxd1b9XH.js +0 -1
- package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-CrJy0ViX.js +0 -23
- package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-Bakaa3zu.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-j78vKrcj.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-D3dr7W9_.js +0 -1
- package/site/dist/assets/EnrollTotp-Dibew4dp-BXbYV8nI.js +0 -66
- package/site/dist/assets/EnvelopeIcon-DjPofnRI.js +0 -1
- package/site/dist/assets/ErrorBanner-BcpGRt0h-BhH6_V-9.js +0 -21
- package/site/dist/assets/ErrorMessage-D8VaAP5m-Bs7uX2a9.js +0 -8
- package/site/dist/assets/ErrorScreen-DipdTWzE-BgPRgnOb.js +0 -4
- package/site/dist/assets/ErrorScreen-DipdTWzE-CvxXC6pz.js +0 -1
- package/site/dist/assets/ExclamationCircleIcon-BE-WGrut.js +0 -1
- package/site/dist/assets/ExclamationTriangleIcon-Ca1Xlc_f.js +0 -1
- package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-De2JvZdV.js +0 -75
- package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-bLp3hXoO.js +0 -28
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-Jr6D6JiZ.js +0 -491
- package/site/dist/assets/FingerPrintIcon-DOCbS93y.js +0 -1
- package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-A_G5kfa1.js +0 -1
- package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-LPv35wkD.js +0 -1
- package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-0hfGrfvf.js +0 -58
- package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-Dz0s_NWR.js +0 -1
- package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-ZGm-3-Io.js +0 -1
- package/site/dist/assets/GlobeAltIcon-BBRl2MMn.js +0 -1
- package/site/dist/assets/GooglePay-B53WnudL-BGiVpB8y.js +0 -1
- package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-CHdfIeD_.js +0 -4
- package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-Cgoiow3m.js +0 -1
- package/site/dist/assets/InfoBanner-Cb3p1z12-_YqQmRiu.js +0 -22
- package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-tuIrKQHH.js +0 -1
- package/site/dist/assets/InstallWalletScreen-DTA8dwLv-BMU_wa61.js +0 -1
- package/site/dist/assets/JsonTree-BHzNC-ic-DcVr0YF0.js +0 -34
- package/site/dist/assets/LabelXs-oqZNqbm_-CNprGVQM.js +0 -6
- package/site/dist/assets/LandingScreen-CGWUoSUG-fH6rsQen.js +0 -1
- package/site/dist/assets/Layouts-BMRfo5hw-e-sepi6F.js +0 -77
- package/site/dist/assets/Link-BdDilT2T-BdwlRJch.js +0 -40
- package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-BXKN2Qm5.js +0 -77
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-Cyn7Q8p1.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-CWiqnwLM.js +0 -117
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-YmDevJzf.js +0 -1
- package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-h7shW-Ay.js +0 -1
- package/site/dist/assets/Loader-UAZqxsnw-DJEVblm1.js +0 -62
- package/site/dist/assets/LoadingSkeleton-BMsgO5PV-D2atjsP5.js +0 -17
- package/site/dist/assets/LockClosedIcon-9EFzWK1C.js +0 -1
- package/site/dist/assets/LoginFailedScreen-CN0dJV6U-By3Si77a.js +0 -1
- package/site/dist/assets/ManualTransferScreen-CHqcuqbs-BAHPY8rs.js +0 -1
- package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CLjffFG1.js +0 -1
- package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-BmQg1Y61.js +0 -1
- package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-DmBtTtlw.js +0 -1
- package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-4a683Bmf.js +0 -1
- package/site/dist/assets/ModalFooter-BCuEK_nz-C_71qsaz.js +0 -403
- package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-DEHHcEf8.js +0 -16
- package/site/dist/assets/OAuthStatusScreen-ufbOa-da-BjAb_WsY.js +0 -1
- package/site/dist/assets/OpenLink-CUpJ1mOr-BLWFsPOW.js +0 -39
- package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-2PJTJxX-.js +0 -1
- package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-DCEg761l.js +0 -6
- package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-8nVjNc8k.js +0 -20
- package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
- package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
- package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
- package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
- package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
- package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
- package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
- package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
- package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
- package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
- package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
- package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
- package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
- package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
- package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
- package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
- package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
- package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
- package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
- package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
- package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
- package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
- package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
- package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
- package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
- package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
- package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
- package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
- package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
- package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
- package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
- package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
- package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
- package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
- package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
- package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
- package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
- package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
- package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
- package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
- package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
- package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
- package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
- package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
- package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
- package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
- package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
- package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
- package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
- package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
- package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
- package/site/dist/assets/PhoneIcon-CZ47md80.js +0 -1
- package/site/dist/assets/PinInput-DbZ0b1i1-BrTXu3Km.js +0 -247
- package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CY60yubk.js +0 -1
- package/site/dist/assets/QrCode-DbnZwlkH-CcMrWV5D.js +0 -22
- package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-DgeDnHOK.js +0 -1
- package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-CvJJwLhK.js +0 -1
- package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-CADtGtSV.js +0 -7
- package/site/dist/assets/SIWXUtil-Cpiv91-M.js +0 -1
- package/site/dist/assets/Screen-D30meWsY-vEusJ7KX.js +0 -175
- package/site/dist/assets/ScreenHeader-CHmc4-Lu-BvGTLg53.js +0 -31
- package/site/dist/assets/ScreenLayout-OwaQQcxH-cfOTmyl0.js +0 -1
- package/site/dist/assets/SelectSourceAsset-CWFog6gm-DxYklZYs.js +0 -140
- package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-oeqeo05x.js +0 -14
- package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-BxFee9UA.js +0 -11
- package/site/dist/assets/ShieldCheckIcon-eFjsosCA.js +0 -1
- package/site/dist/assets/SignRequestScreen-BHKv32KR-z8ixfIma.js +0 -26
- package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-Bc_lvdiF.js +0 -1
- package/site/dist/assets/StackedContainer-B2vaEl56-Do5dXB8-.js +0 -24
- package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-DudetZAc.js +0 -20
- package/site/dist/assets/Subtitle-CV-2yKE4-DV0o1av3.js +0 -13
- package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-Bo3sONvf.js +0 -1
- package/site/dist/assets/Title-BnzYV3Is-C3qOPnO_.js +0 -7
- package/site/dist/assets/TodoList-DnyULl18-BvxvubbF.js +0 -36
- package/site/dist/assets/TransactionDetails-e9nesl30-WWflkOIi.js +0 -109
- package/site/dist/assets/TransactionErrorView-BDBNqA_f-CotYKcgf.js +0 -178
- package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-sVFlWqa8.js +0 -31
- package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-CV2-mY19.js +0 -6
- package/site/dist/assets/TurnstileWrapper-B7sNddys-oPbuMTQt.js +0 -1
- package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-D_mjSpAy.js +0 -1
- package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-6RLG0ahv.js +0 -1
- package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-CaVN0bti.js +0 -1
- package/site/dist/assets/Value-DTgR824E-Bp6WGZZm.js +0 -27
- package/site/dist/assets/WalletCards-DH1rqayz-D3cOTb9x.js +0 -1
- package/site/dist/assets/WalletIcon-DOIZhb2l.js +0 -1
- package/site/dist/assets/WalletInfoCard-sq_2wgFg-bOqfl5MA.js +0 -29
- package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DWaVkMCA.js +0 -1
- package/site/dist/assets/WalletLink-DK5LB5Ha-DjUZvqs-.js +0 -20
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-B6P7DQXA.js +0 -1
- package/site/dist/assets/WarningBanner-ZZqCEtZK-Bc2YKZb7.js +0 -21
- package/site/dist/assets/__vite-browser-external-NciY4Vu1.js +0 -1
- package/site/dist/assets/_esm-BZP3cXTv.js +0 -1
- package/site/dist/assets/_esm-ZmzBJpEs.js +0 -6
- package/site/dist/assets/accounts-D4AXF17G.js +0 -1
- package/site/dist/assets/accounts-IkdFx9nq.js +0 -1
- package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
- package/site/dist/assets/baseSepolia-Bn1j1Ysw.js +0 -1
- package/site/dist/assets/basic-Cz1YiCSk.js +0 -1575
- package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
- package/site/dist/assets/ccip-B1J3oTZx.js +0 -7
- package/site/dist/assets/ccip-Bd7261zy.js +0 -1
- package/site/dist/assets/ccip-D6EHKpjI.js +0 -1
- package/site/dist/assets/chain-scefkYIH.js +0 -5
- package/site/dist/assets/check-DHnllI8N.js +0 -1
- package/site/dist/assets/chevron-down-1y-r4bGO.js +0 -1
- package/site/dist/assets/circle-alert-OwxamXUX.js +0 -1
- package/site/dist/assets/circle-check-big-3sZGVC10.js +0 -1
- package/site/dist/assets/circle-x-DQjNUV9J.js +0 -1
- package/site/dist/assets/client-B-BpRtwY.js +0 -1
- package/site/dist/assets/client-BfIR1FvH.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-YEuIn3jh.js +0 -3
- package/site/dist/assets/copy-DMOX_yn7.js +0 -1
- package/site/dist/assets/core-GIEmxmAa.js +0 -12
- package/site/dist/assets/createLucideIcon-BsERoFTA.js +0 -1
- package/site/dist/assets/defineChain-B-SQDLTG.js +0 -1
- package/site/dist/assets/dijkstra-CY95afk4.js +0 -1
- package/site/dist/assets/dist-CoS6E-3x.js +0 -1
- package/site/dist/assets/dist-Cp6IXeJd.js +0 -1
- package/site/dist/assets/dist-CrNxmdZj.js +0 -1
- package/site/dist/assets/dist-DYGtf4F0.js +0 -16
- package/site/dist/assets/embed.worker-Cnl4VGr3.js +0 -1
- package/site/dist/assets/errors-BQ3N-RVt-CZPAWCgC.js +0 -1
- package/site/dist/assets/esm-C3DslVJx.js +0 -1
- package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
- package/site/dist/assets/esm-Co94ENUV.js +0 -37
- package/site/dist/assets/esm-bb8SANZh.js +0 -1
- package/site/dist/assets/esm-n2lWo7kz.js +0 -1
- package/site/dist/assets/ethers-zTjbodk5-Cs648CnD.js +0 -1
- package/site/dist/assets/eventemitter3-Dzc_0Uq2.js +0 -1
- package/site/dist/assets/external-link-BBPG3rG7.js +0 -1
- package/site/dist/assets/farcaster-DPlSjvF5-CzBCmgyL.js +0 -1
- package/site/dist/assets/features-CuiHbgEJ.js +0 -2
- package/site/dist/assets/fingerprint-pattern-CjLtyIxB.js +0 -1
- package/site/dist/assets/floating-ui.react-DmaCfayr.js +0 -1
- package/site/dist/assets/floating-ui.react-dom-CaoMF8mP.js +0 -1
- package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
- package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
- package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-BNiOFXbK.js +0 -1
- package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
- package/site/dist/assets/getErc20Balance-jXNpFf0N-q4bV8kQq.js +0 -1
- package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-DcnTFrXN.js +0 -1
- package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
- package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
- package/site/dist/assets/guards-BIW-GiMJ-D3tWTQAR.js +0 -1
- package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
- package/site/dist/assets/hourglass-D_GfQdJr.js +0 -1
- package/site/dist/assets/id-UqEwL2BI-CTlvRMA5.js +0 -1
- package/site/dist/assets/index-BdEw5wyl-kLbhqLbv.js +0 -1
- package/site/dist/assets/index-C2h3EneX.js +0 -8
- package/site/dist/assets/index-CWARkn2w-DxuckIFn.js +0 -61
- package/site/dist/assets/index-DvMddJcd-CNBb4VUn.js +0 -12
- package/site/dist/assets/index-YiCw0Y_x-Czl0tWfF.js +0 -436
- package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
- package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
- package/site/dist/assets/index.es-Crd0TyUS.js +0 -2
- package/site/dist/assets/index.es-DQbup1WG.js +0 -3
- package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-Cjf3l05I.js +0 -1
- package/site/dist/assets/jsx-runtime-CndCZlyt.js +0 -1
- package/site/dist/assets/landmark-BZPqMFEf.js +0 -1
- package/site/dist/assets/lib-DGtzi2DK.js +0 -1
- package/site/dist/assets/localBatchGatewayRequest-B71J8sQ9.js +0 -29
- package/site/dist/assets/lock-DyN_c-ud.js +0 -1
- package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
- package/site/dist/assets/mail-V_mcPb64.js +0 -1
- package/site/dist/assets/modal-context-CwmSeFcv-DPWNJrfK.js +0 -1
- package/site/dist/assets/native-BKj27wfc.js +0 -1
- package/site/dist/assets/ofetch.CWycOUEr-Duby3iHT.js +0 -1
- package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
- package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
- package/site/dist/assets/parseEther-CHS0GNkM.js +0 -1
- package/site/dist/assets/pkce-DXF0yKjP-7_VY-d83.js +0 -1
- package/site/dist/assets/preload-helper-CRzLMJPS.js +0 -1
- package/site/dist/assets/privy-context-81KmsBw2-DiR6-Yq8.js +0 -2
- package/site/dist/assets/property-DuZ9DLP4.js +0 -3
- package/site/dist/assets/react-BfYA1q0G.js +0 -1
- package/site/dist/assets/react-CK2HRI1t.js +0 -1
- package/site/dist/assets/react-DxGeY8B8.js +0 -1
- package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
- package/site/dist/assets/react-dom-BI2Koe4o.js +0 -1
- package/site/dist/assets/reservoir-x-nGuZkT-BguVFH3D.js +0 -1
- package/site/dist/assets/rolldown-runtime-Bjp52BOI.js +0 -1
- package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
- package/site/dist/assets/secp256k1-BeYL8aZl.js +0 -1
- package/site/dist/assets/secp256k1-Bh4MSkhz.js +0 -1
- package/site/dist/assets/secp256k1-Ci4cqHl0.js +0 -1
- package/site/dist/assets/secp256k1-DsxXqbXv.js +0 -1
- package/site/dist/assets/secp256k1-N9RUmafO.js +0 -1
- package/site/dist/assets/serializeAuthorizationList-DGMik9Wx.js +0 -1
- package/site/dist/assets/serializeSignature-CWKpxoab.js +0 -2
- package/site/dist/assets/sha2-C6s7zZmb.js +0 -1
- package/site/dist/assets/shared-DRZUmnL5-BewE5wK8.js +0 -193
- package/site/dist/assets/shared-FM0rljBt-CxLACri_.js +0 -21
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BkAKsRNi.js +0 -1
- package/site/dist/assets/simulateTransaction-CfuOrae4-PjA_tGjX.js +0 -1
- package/site/dist/assets/smartphone-3p8nFSTE.js +0 -1
- package/site/dist/assets/src-BBjkfi-Q.js +0 -1
- package/site/dist/assets/storage-Ufl800Ne-1qCR2JMO.js +0 -8
- package/site/dist/assets/stringify-DqUj9VzO.js +0 -20
- package/site/dist/assets/stripe.esm-DEi2D291.js +0 -2
- package/site/dist/assets/styles-BsotlekN-Aq27nZGc.js +0 -9
- package/site/dist/assets/styles-Cm9H63WO-p_ic_dxE.js +0 -92
- package/site/dist/assets/styles-DVyDvTdj-CjsWqAld.js +0 -74
- package/site/dist/assets/styles-hslXy6xG-D5QHIrJU.js +0 -164
- package/site/dist/assets/telegram-B-JqnkqZ-Dqkm4rDn.js +0 -1
- package/site/dist/assets/to-ui-error-DWe-qwS3-Bi1HUQI9.js +0 -4
- package/site/dist/assets/toViemAccount-B5iDGq1b-CdFZwv07.js +0 -2
- package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
- package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
- package/site/dist/assets/triangle-alert-BG3Qw-Hq.js +0 -1
- package/site/dist/assets/twitch-5IOe4sIQ-CaXHFlAu.js +0 -1
- package/site/dist/assets/types-4CuOD23i.js +0 -1
- package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-CQqeyhGY.js +0 -1
- package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-C0xQN_Lp.js +0 -1
- package/site/dist/assets/useActiveWallet-B-yZYGc6-BkK6rFBm.js +0 -123
- package/site/dist/assets/useGetSolPrice-x7gfUIHJ-CLVXqv6I.js +0 -1
- package/site/dist/assets/useGetTokenPrice-CaoUkb79-DAXXs3rs.js +0 -1
- package/site/dist/assets/useI18n-C-5n_noF-CGpkgIVy.js +0 -1
- package/site/dist/assets/useWalletBalance-CZnP5f_f-Cfd64m4n.js +0 -1
- package/site/dist/assets/useWallets-X1ndvJ_U-CAYqg7Za.js +0 -1
- package/site/dist/assets/utils-BkR7EQjg.js +0 -1
- package/site/dist/assets/utils-DJhBNOZA.js +0 -1
- package/site/dist/assets/utils-DdckVfQr.js +0 -1
- package/site/dist/assets/w3m-modal-B4H_uiPH.js +0 -1174
- package/site/dist/assets/wallet-BNI12IYk.js +0 -1
- package/site/dist/assets/wallet-CITSaPlB.js +0 -2
- package/site/dist/assets/wallet-connect-BvJtUsDt-CVlSNIXP.js +0 -1
- package/site/dist/assets/walletconnect-registry-xxG8XLCb-BpzpBq5s.js +0 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CzZ6oybX.js +0 -12
- package/site/dist/assets/wui-ux-by-reown-XyHdtV1O.js +0 -1147
- package/site/dist/assets/x-gjDrY2rk.js +0 -1
- package/src/discover/explore.js +0 -291
- package/src/market/settle.js +0 -165
- package/src/market/terms.js +0 -200
- package/src/publish/cli.js +0 -336
- package/src/publish/envelope.js +0 -185
- package/src/publish/manifest.js +0 -142
package/src/agent/mcp.js
CHANGED
|
@@ -1,289 +1,136 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Fangorn for agents: every publisher's records, found through the explorer.
|
|
3
3
|
//
|
|
4
|
-
// fangorn-mcp
|
|
4
|
+
// fangorn-mcp
|
|
5
5
|
// claude mcp add fangorn -- fangorn-mcp
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// and
|
|
7
|
+
// The explorer (FANGORN_EXPLORER, default https://explorer.fangorn.network) is the Fangorn
|
|
8
|
+
// app that indexes every publisher. This server downloads the explorer's view and searches
|
|
9
|
+
// it here, so no query ever leaves the machine; it never reads the chain to find anything:
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// search records across every publisher, or one (`publisher`)
|
|
12
|
+
// publishers who publishes what: each publisher's repos, with row counts
|
|
13
|
+
// get one record in full
|
|
14
|
+
// buy a record a publisher sells, quoted at the explorer, paid over x402
|
|
15
|
+
// wallet the person's address and balances
|
|
16
|
+
// publish records into the person's own repo, on their yes
|
|
14
17
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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.
|
|
18
|
+
// Buying needs FANGORN_MCP_WALLET_KEY and pays at most FANGORN_MCP_MAX_PRICE a record
|
|
19
|
+
// (default 0.10). Publishing signs with that key, else the fangorn CLI's.
|
|
22
20
|
|
|
23
21
|
import net from "node:net";
|
|
24
|
-
import {
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
25
23
|
import { homedir } from "node:os";
|
|
26
24
|
import { createInterface } from "node:readline";
|
|
27
|
-
import { APP_EXTENSION, listApps, pluginOf, skillOf, toApp } from "../discover/apps.js";
|
|
28
25
|
|
|
29
26
|
const log = (...a) => console.error("[fangorn-mcp]", ...a); // stdout is the protocol
|
|
30
|
-
// FANGORN_MCP_TRACE=1: step timings on stderr, for finding where an answer waits.
|
|
31
|
-
const T0 = performance.now();
|
|
32
|
-
const trace = process.env.FANGORN_MCP_TRACE ? (what) => log(`${Math.round(performance.now() - T0)}ms ${what}`) : () => {};
|
|
33
27
|
|
|
34
28
|
// FANGORN_NETWORK: the SDK's network name ("baseSepolia"); unset, its default.
|
|
35
29
|
const network = async () => (await import("../publish/view.js")).network();
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
async function fangorn() {
|
|
39
|
-
if (!client) {
|
|
40
|
-
const [{ Fangorn }, { generatePrivateKey }] = await Promise.all([
|
|
41
|
-
import("@fangorn-network/sdk"), import("viem/accounts")]);
|
|
42
|
-
client = Fangorn.create({ privateKey: generatePrivateKey(), config: await network() }); // reads only
|
|
43
|
-
// list-apps verifies every card and open-app verifies one again: once a minute is enough.
|
|
44
|
-
const verify = client.discoverApp.bind(client), seen = new Map();
|
|
45
|
-
client.discoverApp = (url) => {
|
|
46
|
-
const hit = seen.get(url);
|
|
47
|
-
if (hit && Date.now() - hit.at < 60_000) return hit.p;
|
|
48
|
-
const p = verify(url).catch((e) => { seen.delete(url); throw e; });
|
|
49
|
-
seen.set(url, { at: Date.now(), p });
|
|
50
|
-
return p;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return client;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** A card URL → its app's MCP endpoint (the card's `mcp`), once the card verifies on chain. */
|
|
57
|
-
async function resolveCard(cardUrl) {
|
|
58
|
-
const card = await (await fetch(cardUrl)).json();
|
|
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 };
|
|
62
|
-
}
|
|
63
|
-
|
|
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;
|
|
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 };
|
|
101
|
-
}
|
|
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
|
|
107
|
-
function cachingFetch(real = globalThis.fetch) {
|
|
108
|
-
const memo = new Map();
|
|
109
|
-
return async (input, init) => {
|
|
110
|
-
const url = String(input?.url ?? input);
|
|
111
|
-
if ((init?.method ?? input?.method ?? "GET") !== "GET" || !/^https?:/.test(url)) return real(input, init);
|
|
112
|
-
const path = new URL(url).pathname;
|
|
113
|
-
// A card, catalog or manifest is small: 8s without one means the host is
|
|
114
|
-
// down, and a dead card in the directory must not stall a listing for a
|
|
115
|
-
// TCP timeout.
|
|
116
|
-
init = { ...init, signal: init?.signal ?? AbortSignal.timeout(8000) };
|
|
117
|
-
if (/\/cdn\/catalog$|\/manifest$|\/agent-card\.json$/.test(path)) {
|
|
118
|
-
const hit = memo.get(url);
|
|
119
|
-
if (hit && Date.now() - hit.at < 60_000) return new Response(hit.body, { headers: hit.headers });
|
|
120
|
-
const r = await real(input, init);
|
|
121
|
-
if (!r.ok) return r;
|
|
122
|
-
const body = new Uint8Array(await r.arrayBuffer());
|
|
123
|
-
memo.set(url, { at: Date.now(), body, headers: r.headers });
|
|
124
|
-
return new Response(body, { headers: r.headers });
|
|
125
|
-
}
|
|
126
|
-
return real(input, init);
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
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
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** JSON-RPC over newline-delimited stdio. Returns `notify` for server → client messages. */
|
|
152
|
-
export function serve(handlers, { input = process.stdin, output = process.stdout } = {}) {
|
|
153
|
-
const send = (m) => output.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
|
|
154
|
-
createInterface({ input }).on("line", async (line) => {
|
|
155
|
-
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
156
|
-
if (msg.id === undefined) return; // notifications
|
|
157
|
-
const h = handlers[msg.method];
|
|
158
|
-
if (!h) return send({ id: msg.id, error: { code: -32601, message: `unknown method ${msg.method}` } });
|
|
159
|
-
try { send({ id: msg.id, result: await h(msg.params ?? {}) }); }
|
|
160
|
-
catch (e) { send({ id: msg.id, error: { code: -32000, message: e?.message ?? String(e) } }); }
|
|
161
|
-
});
|
|
162
|
-
return (method, params = {}) => send({ method, params });
|
|
163
|
-
}
|
|
164
|
-
|
|
31
|
+
// ── the server ──────────────────────────────────────────────────────────────
|
|
165
32
|
const text = (o) => ({ content: [{ type: "text", text: typeof o === "string" ? o : JSON.stringify(o, null, 1) }] });
|
|
166
|
-
|
|
167
|
-
const slug = (name) => String(name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "").slice(0, 24) || "app";
|
|
33
|
+
const ADDRESS = { type: "string", description: "A publisher's address (0x…)" };
|
|
168
34
|
|
|
169
|
-
const
|
|
170
|
-
{ name: "
|
|
171
|
-
description: "
|
|
172
|
-
inputSchema: { type: "object",
|
|
173
|
-
{ name: "find-apps",
|
|
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.",
|
|
175
|
-
inputSchema: { type: "object", properties: {
|
|
35
|
+
const TOOLS = [
|
|
36
|
+
{ name: "search",
|
|
37
|
+
description: "Search every publisher's records on Fangorn by meaning. Each hit names its `publisher`, and carries `sale` ({publisher, repo, id, price} in USDC base units) when it is sold. Pass `publisher` to search one publisher's records.",
|
|
38
|
+
inputSchema: { type: "object", required: ["query"], properties: {
|
|
176
39
|
query: { type: "string", description: "What the person is looking for, in their words" },
|
|
177
|
-
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 } } } },
|
|
178
|
-
{ name: "
|
|
179
|
-
description: "
|
|
180
|
-
inputSchema: { type: "object", properties: {
|
|
181
|
-
{ name: "
|
|
182
|
-
description: "
|
|
183
|
-
inputSchema: { type: "object",
|
|
40
|
+
publisher: ADDRESS, limit: { type: "integer", minimum: 1, maximum: 50, default: 10 } } } },
|
|
41
|
+
{ name: "publishers",
|
|
42
|
+
description: "Who publishes on Fangorn: each publisher's repos, with what they hold and how many records. Filter with `query` (matches a repo's name or description, or the address). The busiest first.",
|
|
43
|
+
inputSchema: { type: "object", properties: { query: { type: "string" } } } },
|
|
44
|
+
{ name: "get",
|
|
45
|
+
description: "One record in full, by the id a search hit gave.",
|
|
46
|
+
inputSchema: { type: "object", required: ["id"], properties: { id: { type: "string" } } } },
|
|
47
|
+
{ name: "buy",
|
|
48
|
+
description: "Buy one record a publisher sells (a search hit with `sale`) and return its sealed fields opened. Pays the price in USDC from the person's wallet (FANGORN_MCP_WALLET_KEY), never above FANGORN_MCP_MAX_PRICE. It checks the sealed data can be read before paying. Tell the person the price first.",
|
|
49
|
+
inputSchema: { type: "object", required: ["publisher", "repo", "id"], properties: {
|
|
50
|
+
publisher: ADDRESS, repo: { type: "string" }, id: { type: "string", description: "From the hit's `sale`" } } } },
|
|
51
|
+
{ name: "wallet",
|
|
52
|
+
description: "The person's Fangorn wallet: its address and its USDC and ETH balances.",
|
|
53
|
+
inputSchema: { type: "object", properties: {} } },
|
|
54
|
+
{ name: "publish",
|
|
55
|
+
description: "Publish records to the person's own repo on Fangorn, from their wallet, where any agent can find them. Public and permanent: first show the person exactly what will be published and call this only on their yes to these records. Propose it when something you made for them (research, a list, a dataset) would be worth to others.",
|
|
56
|
+
inputSchema: { type: "object", required: ["repo", "records"], properties: {
|
|
57
|
+
repo: { type: "string", description: "Repo name, lowercase, e.g. \"notes\" or \"rust-crates\"; created if new" },
|
|
58
|
+
records: { type: "array", items: { type: "object", required: ["id", "payload"], properties: {
|
|
59
|
+
id: { type: "string", description: "Stable: the same thing keeps the same id" },
|
|
60
|
+
type: { type: "string", description: "Record type, e.g. \"note.v1\"" },
|
|
61
|
+
payload: { type: "object", description: "The record. Include a title and text that say what it is, in words" } } } } } } },
|
|
184
62
|
];
|
|
185
63
|
|
|
186
64
|
/**
|
|
187
65
|
* The server. `deps` is the outside world, so the self-check can fake it:
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* remoteTools(url) → { tools, call(name, args) } (the app's own MCP server)
|
|
193
|
-
* notify(method) set after serve()
|
|
66
|
+
* explorer() → { call(name, args) } (view-tools over the explorer's view, run here)
|
|
67
|
+
* quote(p, r, id) → the explorer's x402 URL for one record
|
|
68
|
+
* buy(quoteUrl) → { fields, price, tx } (src/agent/x402.js, from the person's wallet)
|
|
69
|
+
* wallet(), publish({ repo, records })
|
|
194
70
|
*/
|
|
195
71
|
export function handlers(deps) {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const hit = findOpen(app);
|
|
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);
|
|
211
|
-
}
|
|
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 ext = card.capabilities?.extensions?.find((e) => e.uri === APP_EXTENSION);
|
|
218
|
-
const access = ext?.params?.access ?? null;
|
|
219
|
-
const o = { slug: s, name: card.name, card: cardUrl, appId, skill: skillOf(ext), plugin: pluginOf(ext), ...await deps.remoteTools(mcp, { access }) };
|
|
220
|
-
trace(`open ${app}: ready`);
|
|
221
|
-
open.set(o.slug, o);
|
|
222
|
-
deps.notify?.("notifications/tools/list_changed");
|
|
223
|
-
return o;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function call(app, tool, args) {
|
|
227
|
-
const o = findOpen(app);
|
|
228
|
-
if (!o) throw new Error(`${app} is not open; call open-app first`);
|
|
229
|
-
if (!o.tools.some((t) => t.name === tool)) throw new Error(`${o.slug} has no tool ${tool}`);
|
|
230
|
-
return o.call(tool, args);
|
|
231
|
-
}
|
|
232
|
-
|
|
72
|
+
const ask = async (name, args) => {
|
|
73
|
+
const r = await (await deps.explorer()).call(name, args);
|
|
74
|
+
if (r.isError) throw new Error(`explorer: ${r.content?.[0]?.text ?? "error"}`);
|
|
75
|
+
return r;
|
|
76
|
+
};
|
|
233
77
|
const run = {
|
|
234
|
-
|
|
235
|
-
|
|
78
|
+
search: ({ query, publisher, limit = 10 }) =>
|
|
79
|
+
ask("search", { query, limit: Math.min(50, Math.max(1, limit)), ...(publisher ? { where: { owner: publisher } } : {}) }),
|
|
80
|
+
publishers: async ({ query }) => {
|
|
81
|
+
const { views = [] } = JSON.parse((await ask("describe", {})).content[0].text);
|
|
82
|
+
const by = new Map();
|
|
83
|
+
for (const r of views.flatMap((v) => v.repos ?? [])) {
|
|
84
|
+
const p = by.get(r.publisher.toLowerCase()) ?? { publisher: r.publisher, rows: 0, repos: [] };
|
|
85
|
+
p.rows += r.rows;
|
|
86
|
+
p.repos.push({ repo: r.repo, rows: r.rows, description: r.description });
|
|
87
|
+
by.set(r.publisher.toLowerCase(), p);
|
|
88
|
+
}
|
|
236
89
|
const q = String(query ?? "").toLowerCase();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
app: slug(a.name), name: a.name, description: a.desc, card: a.card, appId: a.appId,
|
|
240
|
-
open: open.has(slug(a.name)),
|
|
241
|
-
})),
|
|
242
|
-
unverified: rejected.length,
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
"find-apps": async ({ query, limit = 10 }) => {
|
|
246
|
-
const { apps, ranked_by } = await deps.exploreApps(query);
|
|
247
|
-
return text({ query: query ?? null, ranked_by, apps: apps.slice(0, Math.min(50, Math.max(1, limit)))
|
|
248
|
-
.map((a) => ({ ...a, open: open.has(a.app) })) });
|
|
90
|
+
const hit = (p) => !q || p.publisher.toLowerCase().includes(q) || p.repos.some((r) => `${r.repo} ${r.description}`.toLowerCase().includes(q));
|
|
91
|
+
return text({ publishers: [...by.values()].filter(hit).sort((a, b) => b.rows - a.rows) });
|
|
249
92
|
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
...(o.skill ? { skill: { ...o.skill, adopt: "The app ships an agent skill. Offer it to the user (what it would make you do, from its description); adopt it only on their yes, checking sha256, and ask again if the hash changes." } } : {}),
|
|
257
|
-
// Its Claude Code plugin: hooks run code on every matching event, so the same rule, louder.
|
|
258
|
-
...(o.plugin ? { plugin: { ...o.plugin, install: `claude plugin install ${o.plugin.name} --marketplace ${o.plugin.marketplace}`,
|
|
259
|
-
adopt: "The app ships a Claude Code plugin, whose hooks run code on this machine without being called. Offer it to the user once, saying what its hooks trigger on; install only on their yes. It takes effect next session." } } : {}) });
|
|
93
|
+
get: ({ id }) => ask("get", { id }),
|
|
94
|
+
buy: async ({ publisher, repo, id }) => text({ publisher, repo, id, ...await deps.buy(deps.quote(publisher, repo, id)) }),
|
|
95
|
+
wallet: async () => text(await deps.wallet()),
|
|
96
|
+
publish: async ({ repo, records }) => {
|
|
97
|
+
if (!records?.length) throw new Error("no records to publish");
|
|
98
|
+
return text(await deps.publish({ repo, records: records.map(({ id, type = "record.v1", payload }) => ({ id: String(id), type, payload })) }));
|
|
260
99
|
},
|
|
261
|
-
"call-app-tool": async ({ app, tool, arguments: args }) => call(app, tool, args),
|
|
262
100
|
};
|
|
263
101
|
|
|
264
102
|
return {
|
|
265
103
|
initialize: async (p) => ({
|
|
266
104
|
protocolVersion: p.protocolVersion ?? "2025-06-18",
|
|
267
|
-
capabilities: { tools: { listChanged:
|
|
268
|
-
serverInfo: { name: "fangorn-mcp", version: "0.
|
|
269
|
-
instructions: "Fangorn
|
|
105
|
+
capabilities: { tools: { listChanged: false } },
|
|
106
|
+
serverInfo: { name: "fangorn-mcp", version: "0.2.0" },
|
|
107
|
+
instructions: "Fangorn is publishers' records, anchored on chain and indexed by the Fangorn explorer. search finds records across every publisher; publishers says who publishes what; get reads one; buy pays for a sold one (say the price first).",
|
|
270
108
|
}),
|
|
271
109
|
ping: async () => ({}),
|
|
272
|
-
"tools/list": async () => ({ tools:
|
|
273
|
-
...META,
|
|
274
|
-
...[...open.values()].flatMap((o) => o.tools.map((t) => ({ ...t, name: `${o.slug}__${t.name}`, description: `[${o.name}] ${t.description ?? ""}` }))),
|
|
275
|
-
] }),
|
|
110
|
+
"tools/list": async () => ({ tools: TOOLS }),
|
|
276
111
|
"tools/call": async ({ name, arguments: args = {} }) => {
|
|
277
112
|
try {
|
|
278
|
-
if (run[name])
|
|
279
|
-
|
|
280
|
-
if (i > 0) return await call(name.slice(0, i), name.slice(i + 2), args);
|
|
281
|
-
throw new Error(`no tool ${name}`);
|
|
113
|
+
if (!run[name]) throw new Error(`no tool ${name}`);
|
|
114
|
+
return await run[name](args);
|
|
282
115
|
} catch (e) { return { isError: true, content: [{ type: "text", text: e?.message ?? String(e) }] }; }
|
|
283
116
|
},
|
|
284
117
|
};
|
|
285
118
|
}
|
|
286
119
|
|
|
120
|
+
/** JSON-RPC over newline-delimited stdio. Returns `notify` for server → client messages. */
|
|
121
|
+
export function serve(handlers, { input = process.stdin, output = process.stdout } = {}) {
|
|
122
|
+
const send = (m) => output.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
|
|
123
|
+
createInterface({ input }).on("line", async (line) => {
|
|
124
|
+
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
125
|
+
if (msg.id === undefined) return; // notifications
|
|
126
|
+
const h = handlers[msg.method];
|
|
127
|
+
if (!h) return send({ id: msg.id, error: { code: -32601, message: `unknown method ${msg.method}` } });
|
|
128
|
+
try { send({ id: msg.id, result: await h(msg.params ?? {}) }); }
|
|
129
|
+
catch (e) { send({ id: msg.id, error: { code: -32000, message: e?.message ?? String(e) } }); }
|
|
130
|
+
});
|
|
131
|
+
return (method, params = {}) => send({ method, params });
|
|
132
|
+
}
|
|
133
|
+
|
|
287
134
|
// ── main / self-check ───────────────────────────────────────────────────────
|
|
288
135
|
// A command, not a module: installed as a bin, argv[1] is the symlink, so there is no is-main check.
|
|
289
136
|
if (process.argv[2] === "--selfcheck") {
|
|
@@ -291,154 +138,104 @@ if (process.argv[2] === "--selfcheck") {
|
|
|
291
138
|
const input = new PassThrough(), output = new PassThrough();
|
|
292
139
|
const replies = [];
|
|
293
140
|
createInterface({ input: output }).on("line", (l) => replies.push(JSON.parse(l)));
|
|
294
|
-
const
|
|
295
|
-
// The
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
params: { skill: { url: "https://k.test/skill/kings-foil/SKILL.md", sha256: "b".repeat(64) }, plugin: { marketplace: "k/kf", name: "kings-foil" } } }] } }, mcp: "https://k.test/mcp" }),
|
|
310
|
-
remoteTools: async () => (opened++, remote),
|
|
311
|
-
};
|
|
312
|
-
deps.notify = serve(handlers(deps), { input, output });
|
|
141
|
+
const P = "0xAbC0000000000000000000000000000000000001", Q = "0x0000000000000000000000000000000000000002";
|
|
142
|
+
// The explorer's view tools, as view-tools.js answers them.
|
|
143
|
+
const views = [{ view: "view", rows: 6, repos: [
|
|
144
|
+
{ publisher: P, repo: "weather", rows: 4, description: "hourly readings" },
|
|
145
|
+
{ publisher: Q, repo: "notes", rows: 1, description: "" },
|
|
146
|
+
{ publisher: P, repo: "tides", rows: 1, description: "" }] }];
|
|
147
|
+
const explorer = { call: async (name, args) => (name === "describe" ? text({ views }) : text({ via: name, ...args })) };
|
|
148
|
+
const published = [];
|
|
149
|
+
serve(handlers({
|
|
150
|
+
explorer: async () => explorer,
|
|
151
|
+
quote: (p, r, id) => `https://explorer.test/paid/${p}/${r}/${encodeURIComponent(id)}`,
|
|
152
|
+
buy: async (quoted) => ({ fields: { quoted }, price: "10000" }),
|
|
153
|
+
wallet: async () => ({ address: "0xme" }),
|
|
154
|
+
publish: async (a) => (published.push(a), { repo: a.repo, records: a.records.length }),
|
|
155
|
+
}), { input, output });
|
|
313
156
|
const rpc = (id, method, params) => input.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
314
|
-
const wait = () => new Promise((r) => setTimeout(r, 20));
|
|
315
157
|
const call = (id, name, args) => rpc(id, "tools/call", { name, arguments: args });
|
|
158
|
+
const wait = () => new Promise((r) => setTimeout(r, 20));
|
|
316
159
|
const res = (id) => replies.find((r) => r.id === id);
|
|
317
160
|
const body = (id) => JSON.parse(res(id).result.content[0].text);
|
|
318
161
|
const assert = (c, m) => { if (!c) throw new Error(m); };
|
|
319
162
|
|
|
320
|
-
rpc(1, "initialize", {}); rpc(2, "tools/list"); call(3, "
|
|
321
|
-
|
|
322
|
-
assert(res(2).result.tools.
|
|
323
|
-
assert(body(3).
|
|
324
|
-
assert(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
assert(
|
|
328
|
-
|
|
329
|
-
call(
|
|
330
|
-
|
|
331
|
-
assert(body(
|
|
332
|
-
assert(
|
|
333
|
-
|
|
334
|
-
assert(
|
|
335
|
-
|
|
336
|
-
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();
|
|
337
|
-
assert(res(7).result.tools.some((t) => t.name === "kings-foil__search"), "opened tools are listed under the app");
|
|
338
|
-
assert(body(8).via === "remote" && body(8).arguments.query === "x" && body(9).name === "search", "both call paths reach the app's server");
|
|
339
|
-
assert(posts.join() === "initialize,tools/list,tools/call,tools/call", `initialize, list, then calls: ${posts}`);
|
|
340
|
-
assert(res(10).result.isError && res(11).result.isError, "failures are tool errors");
|
|
341
|
-
assert(res(13).result.isError && /no MCP server/.test(res(13).result.content[0].text), "an app without `mcp` cannot open");
|
|
342
|
-
assert(res(12).error.code === -32601, "unknown method");
|
|
343
|
-
// A private app: a paid tool answers with PaymentRequired; with a wallet this server pays the
|
|
344
|
-
// call (under FANGORN_MCP_MAX_PRICE), or buys a pass when the card sells one under FANGORN_MCP_MAX_PASS.
|
|
345
|
-
const { generatePrivateKey } = await import("viem/accounts");
|
|
346
|
-
const req = { scheme: "exact", network: "eip155:84532", amount: "10000", asset: `0x${"aa".repeat(20)}`, payTo: `0x${"5e".repeat(20)}`,
|
|
347
|
-
maxTimeoutSeconds: 120, extra: { name: "USDC", version: "2" } };
|
|
348
|
-
const asked = { x402Version: 2, error: "paid", resource: { url: "https://p.test/mcp" }, accepts: [req] };
|
|
349
|
-
const seen = [];
|
|
350
|
-
const paidServer = async (url, init) => {
|
|
351
|
-
if (url === "https://p.test/pass") {
|
|
352
|
-
if (!init.headers?.["PAYMENT-SIGNATURE"]) return new Response("{}", { status: 402, headers: { "PAYMENT-REQUIRED": btoa(JSON.stringify({ ...asked, accepts: [{ ...req, amount: "2000000" }] })) } });
|
|
353
|
-
return new Response(JSON.stringify({ token: "pass.tok", expires: 9e9 }));
|
|
354
|
-
}
|
|
355
|
-
const m = JSON.parse(init.body);
|
|
356
|
-
const paid = m.params?._meta?.["x402/payment"], pass = init.headers.authorization === "Bearer pass.tok";
|
|
357
|
-
if (m.method === "tools/call") seen.push(pass ? "pass" : paid ? `paid ${paid.accepted.amount}` : "asked");
|
|
358
|
-
const result = m.method === "tools/list" ? { tools: [{ name: "search" }] } : m.method !== "tools/call" ? {}
|
|
359
|
-
: paid || pass ? text({ hits: [1] }) : { isError: true, structuredContent: asked, content: [{ type: "text", text: JSON.stringify(asked) }] };
|
|
360
|
-
return new Response(JSON.stringify({ jsonrpc: "2.0", id: m.id, result }), { headers: { "content-type": "application/json" } });
|
|
361
|
-
};
|
|
362
|
-
const broke = await remoteTools("https://p.test/mcp", paidServer, { wallet: undefined });
|
|
363
|
-
if (!(await broke.call("search", {})).content.at(-1).text.includes("no wallet")) throw new Error("no wallet: the price goes back to the agent");
|
|
364
|
-
const access = { pass_url: "https://p.test/pass", pass: "2000000", call: "10000", decimals: 6 };
|
|
365
|
-
const perCall = await remoteTools("https://p.test/mcp", paidServer, { access, wallet: generatePrivateKey(), maxCall: "0.05" });
|
|
366
|
-
if (JSON.parse((await perCall.call("search", {})).content[0].text).hits[0] !== 1) throw new Error("a paid call");
|
|
367
|
-
const passer = await remoteTools("https://p.test/mcp", paidServer, { access, wallet: generatePrivateKey(), maxPass: "5" });
|
|
368
|
-
await passer.call("search", {}); await passer.call("search", {});
|
|
369
|
-
if (seen.join() !== "asked,asked,paid 10000,asked,pass,pass") throw new Error(`payments: ${seen}`);
|
|
370
|
-
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");
|
|
163
|
+
rpc(1, "initialize", {}); rpc(2, "tools/list"); call(3, "search", { query: "rain", publisher: P }); call(4, "search", { query: "rain", limit: 500 });
|
|
164
|
+
call(5, "publishers", {}); call(6, "publishers", { query: "HOURLY" }); call(7, "get", { id: "a" }); call(8, "nope"); rpc(9, "nope"); await wait();
|
|
165
|
+
assert(res(2).result.tools.map((t) => t.name).join() === "search,publishers,get,buy,wallet,publish", "the six tools, no apps");
|
|
166
|
+
assert(body(3).via === "search" && body(3).where.owner === P && body(4).where === undefined && body(4).limit === 50, "search goes to the explorer, narrowed to a publisher on request, capped");
|
|
167
|
+
assert(body(5).publishers.map((p) => [p.publisher, p.rows, p.repos.length]).join("|") === `${P},5,2|${Q},1,1`, "publishers groups repos by publisher, busiest first");
|
|
168
|
+
assert(body(6).publishers.length === 1 && body(6).publishers[0].publisher === P, "publishers filters on what a repo holds");
|
|
169
|
+
assert(body(7).via === "get" && body(7).id === "a", "get goes to the explorer");
|
|
170
|
+
assert(res(8).result.isError && res(9).error.code === -32601, "unknown tool and method");
|
|
171
|
+
call(10, "buy", { publisher: P, repo: "weather", id: "a/b" }); call(11, "publish", { repo: "notes", records: [{ id: 1, payload: { title: "t" } }] });
|
|
172
|
+
call(12, "publish", { repo: "notes", records: [] }); call(13, "wallet"); await wait();
|
|
173
|
+
assert(body(10).fields.quoted === `https://explorer.test/paid/${P}/weather/a%2Fb` && body(10).price === "10000", "buy quotes the record at the explorer, the id escaped");
|
|
174
|
+
assert(body(11).records === 1 && published[0].records[0].id === "1" && published[0].records[0].type === "record.v1", "publish commits, ids as strings, a default type");
|
|
175
|
+
assert(res(12).result.isError && body(13).address === "0xme", "publishing nothing is an error; wallet reports");
|
|
176
|
+
const failing = handlers({ explorer: async () => ({ call: async () => ({ isError: true, content: [{ type: "text", text: "down" }] }) }) });
|
|
177
|
+
assert(/explorer: down/.test((await failing["tools/call"]({ name: "search", arguments: { query: "x" } })).content[0].text), "an explorer error says so");
|
|
178
|
+
console.log("mcp.js self-check ok — search, get and publishers ask the explorer and never the chain; publishers groups repos by publisher; buy quotes at the explorer; publish and wallet");
|
|
371
179
|
process.exit(0);
|
|
372
180
|
}
|
|
373
181
|
|
|
374
|
-
const argv = process.argv.slice(2);
|
|
375
|
-
globalThis.fetch = cachingFetch();
|
|
376
182
|
// Node gives each address 250ms before trying the next; on a slow link, or a
|
|
377
183
|
// host with no working IPv6, every attempt times out and fetch fails outright.
|
|
378
184
|
net.setDefaultAutoSelectFamilyAttemptTimeout?.(2000);
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
// The
|
|
382
|
-
const
|
|
383
|
-
let
|
|
185
|
+
const EXPLORER = (process.env.FANGORN_EXPLORER || "https://explorer.fangorn.network").replace(/\/+$/, "");
|
|
186
|
+
let explorer;
|
|
187
|
+
// The person's key: this server's own, else the one the fangorn CLI signs with.
|
|
188
|
+
const FANGORN_DIR = `${homedir()}/.fangorn`;
|
|
189
|
+
let KEY = process.env.FANGORN_MCP_WALLET_KEY || undefined; // the plugin passes "" when unset
|
|
190
|
+
try { KEY ??= JSON.parse(readFileSync(`${FANGORN_DIR}/config.json`, "utf8")).privateKey; } catch { /* no CLI config */ }
|
|
191
|
+
KEY &&= `0x${KEY.trim().replace(/^0x/i, "")}`; // a key file may lack 0x
|
|
192
|
+
const needKey = () => { if (!KEY) throw new Error("no wallet: run `fangorn init`, or set FANGORN_MCP_WALLET_KEY"); return KEY; };
|
|
193
|
+
let signer, pub;
|
|
194
|
+
const signing = async () => (signer ??= (await import("@fangorn-network/sdk")).Fangorn.create({
|
|
195
|
+
privateKey: needKey(), config: await network(), storage: { signedUrl: {} } }));
|
|
384
196
|
const chainClient = async () => {
|
|
385
197
|
if (!pub) {
|
|
386
|
-
const [{ createPublicClient, http },
|
|
387
|
-
pub = createPublicClient({ chain:
|
|
198
|
+
const [{ createPublicClient, http }, n] = await Promise.all([import("viem"), network()]);
|
|
199
|
+
pub = createPublicClient({ chain: n.chain, transport: http(n.rpcUrl) });
|
|
388
200
|
}
|
|
389
201
|
return pub;
|
|
390
202
|
};
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// seconds, so not at startup, where it would hold up list-apps.
|
|
394
|
-
const warmEmbedder = () => (warming ??= vt.warm({ cacheDir: `${CACHE}/models` }).then((ok) => ok ? trace("embedder ready")
|
|
395
|
-
: log(`embedder unavailable, searching by words: ${vt.warmError}`)));
|
|
203
|
+
// Circle's USDC on Base Sepolia. ponytail: one network's; FANGORN_USDC for another.
|
|
204
|
+
const USDC = process.env.FANGORN_USDC || "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
396
205
|
const deps = {
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
206
|
+
// The explorer's view, searched here. Re-read every 5 minutes, so a long session sees new publishers.
|
|
207
|
+
// ponytail: the first search downloads the whole view; route to the best domains (shard.js setDomains)
|
|
208
|
+
// once the network is big enough for that to hurt.
|
|
209
|
+
explorer: async () => {
|
|
210
|
+
if (!explorer || Date.now() - explorer.at > 300_000) {
|
|
211
|
+
const [vt, { resetShard }] = await Promise.all([import("./view-tools.js"), import("../core/shard.js")]);
|
|
212
|
+
resetShard();
|
|
213
|
+
explorer = { at: Date.now(), ...vt.viewTools({ name: "the Fangorn explorer", desc: "every publisher", views: [`${EXPLORER}/view`] }) };
|
|
404
214
|
}
|
|
405
|
-
return
|
|
215
|
+
return explorer;
|
|
406
216
|
},
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (!
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
const { ex, dir } = explored;
|
|
416
|
-
const { EMBED_MODEL } = await import("../core/embed.js");
|
|
417
|
-
const qv = query?.trim() ? await vt.queryVector(query) : null;
|
|
418
|
-
const apps = ex.rankApps(dir.apps, query, { qv, model: EMBED_MODEL }).map(ex.brief);
|
|
419
|
-
return { apps, ranked_by: apps[0]?.ranked_by ?? null };
|
|
217
|
+
quote: (p, r, id) => `${EXPLORER}/paid/${encodeURIComponent(p)}/${encodeURIComponent(r)}/${encodeURIComponent(id)}`,
|
|
218
|
+
// Paying is opted into with this server's own key; the CLI's key alone only publishes.
|
|
219
|
+
buy: async (quote) => {
|
|
220
|
+
if (!process.env.FANGORN_MCP_WALLET_KEY) throw new Error("buying is off: set FANGORN_MCP_WALLET_KEY");
|
|
221
|
+
const { buyRecord } = await import("./x402.js");
|
|
222
|
+
return buyRecord(quote, { privateKey: needKey(), fangorn: await signing(),
|
|
223
|
+
maxPrice: BigInt(Math.round(Number(process.env.FANGORN_MCP_MAX_PRICE || "0.10") * 1e6)) });
|
|
420
224
|
},
|
|
421
|
-
|
|
422
|
-
const [
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
225
|
+
wallet: async () => {
|
|
226
|
+
const [{ privateKeyToAccount }, { parseAbi, formatUnits, formatEther }, c] = await Promise.all([import("viem/accounts"), import("viem"), chainClient()]);
|
|
227
|
+
const address = privateKeyToAccount(needKey()).address;
|
|
228
|
+
const [usdc, eth] = await Promise.all([
|
|
229
|
+
c.readContract({ address: USDC, abi: parseAbi(["function balanceOf(address) view returns (uint256)"]), functionName: "balanceOf", args: [address] }),
|
|
230
|
+
c.getBalance({ address })]);
|
|
231
|
+
return { address, usdc: formatUnits(usdc, 6), eth: formatEther(eth) };
|
|
232
|
+
},
|
|
233
|
+
publish: async ({ repo, records }) => {
|
|
234
|
+
const f = await signing();
|
|
235
|
+
await f.initRepo(repo);
|
|
236
|
+
const { txHash, commitCid } = await f.uploadBatch(repo, records);
|
|
237
|
+
return { publisher: f.getAddress(), repo, records: records.length, commit: commitCid, tx: txHash };
|
|
429
238
|
},
|
|
430
|
-
resolveCard,
|
|
431
|
-
remoteTools: (url, opts) => remoteTools(url, fetch, opts),
|
|
432
|
-
};
|
|
433
|
-
const h = handlers(deps);
|
|
434
|
-
// The client starts this server with the session, long before an agent opens an
|
|
435
|
-
// app: load the SDK then, behind the agent's first thought. After the initialize
|
|
436
|
-
// reply, not before: it blocks the thread while it loads, and the client is
|
|
437
|
-
// waiting on that reply.
|
|
438
|
-
const reply = h.initialize;
|
|
439
|
-
h.initialize = async (p) => {
|
|
440
|
-
setImmediate(() => fangorn().then(() => trace("sdk ready"), () => {}));
|
|
441
|
-
return reply(p);
|
|
442
239
|
};
|
|
443
|
-
|
|
444
|
-
log(`ready:
|
|
240
|
+
serve(handlers(deps));
|
|
241
|
+
log(`ready: explorer ${EXPLORER}`);
|