@fangorn-network/westmarch 0.0.20-dev → 0.1.1
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 +35 -5
- package/docs/app-to-agent.md +113 -620
- package/package.json +18 -18
- 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-CkP1WhkV.js +8 -0
- package/site/dist/assets/roles-BDPDWrwo.js +1 -0
- package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
- package/site/dist/assets/transformers.web-CwR_IYQV.js +40 -0
- package/site/dist/index.html +4 -5
- package/site/feed.js +16 -1
- package/site/no-model.js +4 -0
- package/site/worker.js +382 -59
- package/src/agent/agent-card.js +51 -37
- package/src/agent/mcp.js +173 -500
- package/src/agent/tools.js +3 -2
- package/src/agent/ui.js +1 -1
- package/src/agent/view-tools.js +11 -30
- package/src/agent/x402.js +194 -85
- package/src/core/embed.js +56 -53
- package/src/core/embed.worker.js +1 -1
- package/src/core/rank.js +1 -1
- package/src/core/roles.js +7 -7
- package/src/core/shard.js +12 -8
- package/src/discover/apps.js +103 -67
- package/src/discover/corpora.js +1 -1
- package/src/discover/directory.js +3 -3
- package/src/market/cohort.js +1 -1
- package/src/market/demand.js +1 -1
- package/src/market/reactions.js +24 -18
- package/src/publish/devnet.js +146 -0
- package/src/publish/graph.js +1 -1
- package/src/publish/init.js +74 -0
- package/src/publish/lint.js +3 -2
- package/src/publish/ship.js +433 -288
- package/src/publish/view-cli.js +14 -10
- package/src/publish/view.js +485 -255
- package/src/taste/steam.js +1 -1
- package/src/taste/taste-doc.js +4 -3
- package/src/taste/taste.js +3 -3
- package/src/taste/vault.js +4 -2
- package/template/README.md +48 -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/plugin/scripts/fx.mjs +0 -268
- package/site/dist/assets/AccountNotFoundScreen-C4njXc69-BOfmiSH1.js +0 -1
- package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js +0 -31
- package/site/dist/assets/Address-H8kD1d9O-C85sJ79B.js +0 -17
- package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js +0 -6
- package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js +0 -1
- package/site/dist/assets/ApiController-CYdiyu9B.js +0 -5
- package/site/dist/assets/ArrowsRightLeftIcon-cp60yobq.js +0 -1
- package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js +0 -1
- package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js +0 -1
- package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js +0 -1
- package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js +0 -103
- package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js +0 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
- package/site/dist/assets/CaptchaScreen-DbpvkIGx-D8eDcAkc.js +0 -1
- package/site/dist/assets/CheckCircleIcon-BM409mGI.js +0 -1
- package/site/dist/assets/CheckCircleIcon-NCOQc0Gs.js +0 -1
- package/site/dist/assets/Checkbox-D1EDeo41-rHlvsZzP.js +0 -52
- package/site/dist/assets/ChevronDownIcon-CW-4MSpE.js +0 -1
- package/site/dist/assets/Chip-CZKIKt9K-CutJG010.js +0 -26
- package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js +0 -16
- package/site/dist/assets/ConnectEmailForm-B-VRE0MN-N9J6T_YL.js +0 -3
- package/site/dist/assets/ConnectLedgerScreen-DWHABGot-h0uYIUi6.js +0 -10
- package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js +0 -1
- package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js +0 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
- package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js +0 -95
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-BeWLchcf.js +0 -7
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
- package/site/dist/assets/ConnectorUtil-Cq4lWnfe.js +0 -21
- package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
- package/site/dist/assets/CopyToClipboard-i_OQSBJr-DujUHNzW.js +0 -39
- package/site/dist/assets/CopyableText-CQapvaMr-DMIdpDaU.js +0 -28
- package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-Csju14vF.js +0 -1
- package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js +0 -22
- package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js +0 -1
- package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js +0 -1
- package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-B3vY888G.js +0 -187
- package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DmdQaFI2.js +0 -8
- package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js +0 -1
- package/site/dist/assets/EmailInputForm-Cqo1mda8-B2NG3K5V.js +0 -124
- package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js +0 -1
- package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js +0 -1
- package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js +0 -23
- package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js +0 -1
- package/site/dist/assets/EnrollTotp-Dibew4dp-SA2pvNEc.js +0 -66
- package/site/dist/assets/EnvelopeIcon-Dh2vSVj4.js +0 -1
- package/site/dist/assets/ErrorBanner-BcpGRt0h-BKOYTyCi.js +0 -21
- package/site/dist/assets/ErrorMessage-D8VaAP5m-CrZOnUyg.js +0 -8
- package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-c8g8En21.js +0 -4
- package/site/dist/assets/ExclamationCircleIcon-DUA2wMfO.js +0 -1
- package/site/dist/assets/ExclamationTriangleIcon-eZfaqcFq.js +0 -1
- package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js +0 -75
- package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js +0 -28
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
- package/site/dist/assets/FingerPrintIcon-Dm6kxmg7.js +0 -1
- package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js +0 -1
- package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js +0 -1
- package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js +0 -58
- package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js +0 -1
- package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js +0 -1
- package/site/dist/assets/GlobeAltIcon-vqrVV5ho.js +0 -1
- package/site/dist/assets/GooglePay-B53WnudL-BX5_WtCx.js +0 -1
- package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js +0 -4
- package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js +0 -1
- package/site/dist/assets/InfoBanner-Cb3p1z12-DLBl_Ywy.js +0 -22
- package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js +0 -1
- package/site/dist/assets/InstallWalletScreen-DTA8dwLv-CxreyXoD.js +0 -1
- package/site/dist/assets/JsonTree-BHzNC-ic-DcNcbSoM.js +0 -34
- package/site/dist/assets/LabelXs-oqZNqbm_-iafrj25N.js +0 -6
- package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
- package/site/dist/assets/Layouts-BMRfo5hw-CeCOyCLK.js +0 -77
- package/site/dist/assets/Link-BdDilT2T-BEDEdHuS.js +0 -40
- package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js +0 -77
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js +0 -117
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
- package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-C0DuD14j.js +0 -1
- package/site/dist/assets/Loader-UAZqxsnw-Cy7hEE2f.js +0 -62
- package/site/dist/assets/LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js +0 -17
- package/site/dist/assets/LockClosedIcon-BgY_5v_Z.js +0 -1
- package/site/dist/assets/LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js +0 -1
- package/site/dist/assets/ManualTransferScreen-CHqcuqbs-tXiZDHmN.js +0 -1
- package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js +0 -1
- package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js +0 -1
- package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js +0 -1
- package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js +0 -1
- package/site/dist/assets/ModalFooter-BCuEK_nz-c-khvG4Y.js +0 -403
- package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js +0 -16
- package/site/dist/assets/OAuthStatusScreen-ufbOa-da-CeiGcSia.js +0 -1
- package/site/dist/assets/OpenLink-CUpJ1mOr-DfAvJRyK.js +0 -39
- package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js +0 -1
- package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js +0 -6
- package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-B9F67IYV.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-CvIiPwYQ.js +0 -1
- package/site/dist/assets/PinInput-DbZ0b1i1-CDXZBdQx.js +0 -247
- package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CtQWBiV2.js +0 -1
- package/site/dist/assets/QrCode-DbnZwlkH-h6Bhoe5U.js +0 -22
- package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js +0 -1
- package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js +0 -1
- package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js +0 -7
- package/site/dist/assets/SIWXUtil-TUCxa7aA.js +0 -1
- package/site/dist/assets/Screen-D30meWsY-CCeVNuWs.js +0 -175
- package/site/dist/assets/ScreenHeader-CHmc4-Lu-DjyrH7IK.js +0 -31
- package/site/dist/assets/ScreenLayout-OwaQQcxH-CrQM6kvA.js +0 -1
- package/site/dist/assets/SelectSourceAsset-CWFog6gm-D7yTw87h.js +0 -140
- package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js +0 -14
- package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js +0 -11
- package/site/dist/assets/ShieldCheckIcon-o6RHYev1.js +0 -1
- package/site/dist/assets/SignRequestScreen-BHKv32KR-DDpMYyJ1.js +0 -26
- package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js +0 -1
- package/site/dist/assets/StackedContainer-B2vaEl56-eQCM2cmv.js +0 -24
- package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js +0 -20
- package/site/dist/assets/Subtitle-CV-2yKE4-zxWw2m1Q.js +0 -13
- package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js +0 -1
- package/site/dist/assets/Title-BnzYV3Is-B_JpTatT.js +0 -7
- package/site/dist/assets/TodoList-DnyULl18-C8Eyy28X.js +0 -36
- package/site/dist/assets/TransactionDetails-e9nesl30-C4MpIHR-.js +0 -109
- package/site/dist/assets/TransactionErrorView-BDBNqA_f-p1T9WVMx.js +0 -178
- package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js +0 -31
- package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js +0 -6
- package/site/dist/assets/TurnstileWrapper-B7sNddys-Jwy1tJtm.js +0 -1
- package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js +0 -1
- package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js +0 -1
- package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-D7m70C4b.js +0 -1
- package/site/dist/assets/Value-DTgR824E-BRuIGiec.js +0 -27
- package/site/dist/assets/WalletCards-DH1rqayz-Bgp9Fsgs.js +0 -1
- package/site/dist/assets/WalletIcon-gKf0gTw6.js +0 -1
- package/site/dist/assets/WalletInfoCard-sq_2wgFg-DpNSAGte.js +0 -29
- package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js +0 -1
- package/site/dist/assets/WalletLink-DK5LB5Ha-DHaL350n.js +0 -20
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
- package/site/dist/assets/WarningBanner-ZZqCEtZK-2eU4Qo74.js +0 -21
- package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
- package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
- package/site/dist/assets/_esm-BdeM6pxJ.js +0 -1
- package/site/dist/assets/accounts-D96UKWkC.js +0 -1
- package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
- package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
- package/site/dist/assets/baseSepolia-DX-qGsik.js +0 -1
- package/site/dist/assets/basic-oIwwSyu4.js +0 -1575
- package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
- package/site/dist/assets/ccip-65sIfSPR.js +0 -1
- package/site/dist/assets/ccip-BZvRTL6g.js +0 -7
- package/site/dist/assets/ccip-DA_zG341.js +0 -1
- package/site/dist/assets/chain-DM3oH6Px.js +0 -8
- package/site/dist/assets/check-BPgcwEPP.js +0 -1
- package/site/dist/assets/chevron-down-BSsbXkK-.js +0 -1
- package/site/dist/assets/circle-alert-DWaeLZNj.js +0 -1
- package/site/dist/assets/circle-check-big-Chb1ivw9.js +0 -1
- package/site/dist/assets/circle-x-AV9Cet0R.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/copy-Ckr0AkL6.js +0 -1
- package/site/dist/assets/core-snws5xwv.js +0 -12
- package/site/dist/assets/createLucideIcon-IPW50ckY.js +0 -1
- package/site/dist/assets/defineChain-CiuCXUZW.js +0 -1
- package/site/dist/assets/dijkstra-Drfl93V4.js +0 -1
- package/site/dist/assets/dist-Cm7pNAtL.js +0 -1
- package/site/dist/assets/dist-CoS6E-3x.js +0 -1
- package/site/dist/assets/dist-Dl07uGq1.js +0 -1
- package/site/dist/assets/dist-DnGANz51.js +0 -16
- package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
- package/site/dist/assets/errors-BQ3N-RVt-BLqjTPNR.js +0 -1
- package/site/dist/assets/esm-B5cbIQaz.js +0 -37
- package/site/dist/assets/esm-ByALO6mA.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-n2lWo7kz.js +0 -1
- package/site/dist/assets/ethers-zTjbodk5-wHhL_BQ1.js +0 -1
- package/site/dist/assets/eventemitter3-CL76jPsW.js +0 -1
- package/site/dist/assets/external-link-D-uehwM2.js +0 -1
- package/site/dist/assets/farcaster-DPlSjvF5-BqTvDXBR.js +0 -1
- package/site/dist/assets/features-DQ0UYy6K.js +0 -2
- package/site/dist/assets/fingerprint-pattern-D8ytA16Q.js +0 -1
- package/site/dist/assets/floating-ui.react-dom-DKWOL9UG.js +0 -1
- package/site/dist/assets/floating-ui.react-xRkl9O3u.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-Dukf7_kJ.js +0 -1
- package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
- package/site/dist/assets/getErc20Balance-jXNpFf0N-Bb9WM31M.js +0 -1
- package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-KPrdn04K.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-CHfXo8XI.js +0 -1
- package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
- package/site/dist/assets/hourglass-CPG1HZmf.js +0 -1
- package/site/dist/assets/id-UqEwL2BI-BkKm533U.js +0 -1
- package/site/dist/assets/index-1HYFYA-W.js +0 -8
- package/site/dist/assets/index-BdEw5wyl-ChSJvPZk.js +0 -1
- package/site/dist/assets/index-CWARkn2w-Cezl_uBr.js +0 -61
- package/site/dist/assets/index-DvMddJcd-BatvgC-S.js +0 -12
- package/site/dist/assets/index-YiCw0Y_x-CX5i3Z87.js +0 -437
- 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-BmqyoBEM.js +0 -2
- package/site/dist/assets/index.es-C1v1or--.js +0 -3
- package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js +0 -1
- package/site/dist/assets/jsx-runtime-BbDfbRii.js +0 -1
- package/site/dist/assets/landmark-CH6LV6F0.js +0 -1
- package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
- package/site/dist/assets/localBatchGatewayRequest-3kLrX_c7.js +0 -32
- package/site/dist/assets/lock-DcskI4Bb.js +0 -1
- package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
- package/site/dist/assets/mail-Bs0r5yOi.js +0 -1
- package/site/dist/assets/modal-context-CwmSeFcv-DruX9J1E.js +0 -1
- package/site/dist/assets/native-BKj27wfc.js +0 -1
- package/site/dist/assets/ofetch.CWycOUEr-CHp-crOQ.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-VjhVo3dd.js +0 -1
- package/site/dist/assets/pkce-DXF0yKjP-ChQhE0st.js +0 -1
- package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
- package/site/dist/assets/privy-context-81KmsBw2-CjWGzyg7.js +0 -2
- package/site/dist/assets/property-DuZ9DLP4.js +0 -3
- package/site/dist/assets/react-DCXkXSrv.js +0 -1
- package/site/dist/assets/react-DQnMl2Xv.js +0 -1
- package/site/dist/assets/react-DrJ0MmMO.js +0 -1
- package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
- package/site/dist/assets/react-dom-5dW7aKoP.js +0 -1
- package/site/dist/assets/reservoir-x-nGuZkT-BMX127i6.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/safe-url-D7SRPu33-CkMedGro.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-_MleGpqL.js +0 -1
- package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
- package/site/dist/assets/serializeAuthorizationList-CX8hGL56.js +0 -1
- package/site/dist/assets/serializeSignature-DfxkPnu-.js +0 -2
- package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
- package/site/dist/assets/shared-DRZUmnL5-xqAUARWo.js +0 -193
- package/site/dist/assets/shared-FM0rljBt--Avg6q2R.js +0 -21
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
- package/site/dist/assets/simulateTransaction-CfuOrae4-64lWYJAL.js +0 -1
- package/site/dist/assets/smartphone-CcEZ-XAy.js +0 -1
- package/site/dist/assets/src-BBjkfi-Q.js +0 -1
- package/site/dist/assets/storage-Ufl800Ne-D0bZBw-H.js +0 -9
- package/site/dist/assets/stringify-BlDm7tuc.js +0 -21
- package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
- package/site/dist/assets/styles-BsotlekN-CXEB4Ud9.js +0 -9
- package/site/dist/assets/styles-Cm9H63WO-Br4QtzUL.js +0 -92
- package/site/dist/assets/styles-DVyDvTdj-BTABn2K8.js +0 -74
- package/site/dist/assets/styles-hslXy6xG-D-49b1rk.js +0 -164
- package/site/dist/assets/telegram-B-JqnkqZ-RBoY3PLx.js +0 -1
- package/site/dist/assets/to-ui-error-DWe-qwS3-Cx2bk1kZ.js +0 -4
- package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.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/transformers.web-CVP3pJOS.js +0 -40
- package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
- package/site/dist/assets/triangle-alert-CWjYH75H.js +0 -1
- package/site/dist/assets/twitch-5IOe4sIQ-lDtgk0os.js +0 -1
- package/site/dist/assets/types-4CuOD23i.js +0 -1
- package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js +0 -1
- package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-CC2pq27z.js +0 -1
- package/site/dist/assets/useActiveWallet-B-yZYGc6-D7Ik-LxJ.js +0 -123
- package/site/dist/assets/useGetSolPrice-x7gfUIHJ-D1hmQfpA.js +0 -1
- package/site/dist/assets/useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js +0 -1
- package/site/dist/assets/useI18n-C-5n_noF-BcqC76yj.js +0 -1
- package/site/dist/assets/useWalletBalance-CZnP5f_f-Dw2GIJ4g.js +0 -1
- package/site/dist/assets/useWallets-X1ndvJ_U-DRjz7uYD.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-BeVrBOIf.js +0 -1174
- package/site/dist/assets/wallet-Bn85Sg2W.js +0 -1
- package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
- package/site/dist/assets/wallet-connect-BvJtUsDt-Y03hhqFN.js +0 -1
- package/site/dist/assets/walletconnect-registry-xxG8XLCb-D1SSFvMp.js +0 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
- package/site/dist/assets/wui-ux-by-reown-D34ILNLS.js +0 -1147
- package/site/dist/assets/x-rlSp9U7W.js +0 -1
- package/src/discover/explore.js +0 -271
- 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,425 +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
|
-
// claude mcp add fangorn -e FANGORN_MCP_CDP=ws://browser:9222 -- fangorn-mcp
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the directory and the browser both:
|
|
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:
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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
|
|
17
17
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// are relayed to the tab's own `modelContext.executeTool`, so the tools, their
|
|
21
|
-
// schemas and their answers are the page's, and queries still run in the tab.
|
|
22
|
-
// A local Chrome's profile persists (~/.cache/westmarch-mcp), so a page's
|
|
23
|
-
// downloads, such as an embedding model, are fetched once. A tab or browser that
|
|
24
|
-
// dies is reopened on the next call.
|
|
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.
|
|
25
20
|
|
|
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
21
|
import net from "node:net";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
30
23
|
import { homedir } from "node:os";
|
|
31
24
|
import { createInterface } from "node:readline";
|
|
32
|
-
import { APP_EXTENSION, listApps, toApp } from "../discover/apps.js";
|
|
33
25
|
|
|
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
26
|
const log = (...a) => console.error("[fangorn-mcp]", ...a); // stdout is the protocol
|
|
38
|
-
// FANGORN_MCP_TRACE=1: step timings on stderr, for finding where an answer waits.
|
|
39
|
-
const T0 = performance.now();
|
|
40
|
-
const trace = process.env.FANGORN_MCP_TRACE ? (what) => log(`${Math.round(performance.now() - T0)}ms ${what}`) : () => {};
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
async
|
|
44
|
-
if (!client) {
|
|
45
|
-
const [{ Fangorn, FangornConfig }, { generatePrivateKey }] = await Promise.all([
|
|
46
|
-
import("@fangorn-network/sdk"), import("viem/accounts")]);
|
|
47
|
-
client = Fangorn.create({ privateKey: generatePrivateKey(), config: FangornConfig }); // reads only
|
|
48
|
-
// list-apps verifies every card and open-app verifies one again: once a minute is enough.
|
|
49
|
-
const verify = client.discoverApp.bind(client), seen = new Map();
|
|
50
|
-
client.discoverApp = (url) => {
|
|
51
|
-
const hit = seen.get(url);
|
|
52
|
-
if (hit && Date.now() - hit.at < 60_000) return hit.p;
|
|
53
|
-
const p = verify(url).catch((e) => { seen.delete(url); throw e; });
|
|
54
|
-
seen.set(url, { at: Date.now(), p });
|
|
55
|
-
return p;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
return client;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A card URL → the verified card and the page to open. A plain A2A card (no
|
|
62
|
-
* Fangorn extension) opens unverified: the caller asked for it by URL. */
|
|
63
|
-
async function resolveCard(cardUrl) {
|
|
64
|
-
const card = await (await fetch(cardUrl)).json();
|
|
65
|
-
const fangornApp = card.capabilities?.extensions?.some((e) => e.uri === APP_EXTENSION);
|
|
66
|
-
// Views only from a verified card, and only http(s) ones (toApp filters).
|
|
67
|
-
const app = fangornApp ? toApp(await (await fangorn()).discoverApp(cardUrl), cardUrl) : null;
|
|
68
|
-
const page = new URL(card.url);
|
|
69
|
-
if (page.protocol !== "https:" && page.protocol !== "http:") throw new Error(`card.url is not http(s): ${card.url}`);
|
|
70
|
-
return { card, page: page.toString(), verified: fangornApp, views: app?.views ?? [], paid: app?.paid ?? null };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── the network ─────────────────────────────────────────────────────────────
|
|
74
|
-
// Every byte an answer waits on, cached where it is safe to:
|
|
75
|
-
// - a content-addressed file (`…-<12 hex>.<ext>`, every shard) never changes,
|
|
76
|
-
// so it is kept on disk forever. shard.js still checks it against the
|
|
77
|
-
// manifest's sha256, so a bad cache entry fails loudly, not silently.
|
|
78
|
-
// - catalogs, manifests and cards change on a rebake: held for a minute, so
|
|
79
|
-
// one search (or resolving then verifying a card) does not fetch each twice.
|
|
80
|
-
const CACHE = `${homedir()}/.cache/fangorn-mcp`;
|
|
81
|
-
/** Stale-while-revalidate from disk: the last copy at once, a fresh one fetched
|
|
82
|
-
* behind it for next time. Only for reads where stale is harmless. */
|
|
83
|
-
function staleFetch(fetcher) {
|
|
84
|
-
mkdirSync(`${CACHE}/stale`, { recursive: true });
|
|
85
|
-
return async (url) => {
|
|
86
|
-
const f = `${CACHE}/stale/${createHash("sha256").update(url).digest("hex")}`;
|
|
87
|
-
const refresh = fetcher(url).then(async (r) => {
|
|
88
|
-
if (!r.ok) return r;
|
|
89
|
-
const b = new Uint8Array(await r.arrayBuffer());
|
|
90
|
-
writeFileSync(`${f}.tmp`, b); renameSync(`${f}.tmp`, f);
|
|
91
|
-
return new Response(b);
|
|
92
|
-
});
|
|
93
|
-
if (existsSync(f)) { refresh.catch(() => {}); return new Response(readFileSync(f)); }
|
|
94
|
-
return refresh;
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function cachingFetch(real = globalThis.fetch) {
|
|
99
|
-
mkdirSync(`${CACHE}/blobs`, { recursive: true });
|
|
100
|
-
const memo = new Map();
|
|
101
|
-
return async (input, init) => {
|
|
102
|
-
const url = String(input?.url ?? input);
|
|
103
|
-
if ((init?.method ?? input?.method ?? "GET") !== "GET" || !/^https?:/.test(url)) return real(input, init);
|
|
104
|
-
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
|
-
// A card, catalog or manifest is small: 8s without one means the host is
|
|
115
|
-
// down, and a dead card in the directory must not stall a listing for a
|
|
116
|
-
// TCP timeout. Shards get no deadline; a big file on a slow link is legitimate.
|
|
117
|
-
init = { ...init, signal: init?.signal ?? AbortSignal.timeout(8000) };
|
|
118
|
-
if (/\/cdn\/catalog$|\/manifest$|\/agent-card\.json$/.test(path)) {
|
|
119
|
-
const hit = memo.get(url);
|
|
120
|
-
if (hit && Date.now() - hit.at < 60_000) return new Response(hit.body, { headers: hit.headers });
|
|
121
|
-
const r = await real(input, init);
|
|
122
|
-
if (!r.ok) return r;
|
|
123
|
-
const body = new Uint8Array(await r.arrayBuffer());
|
|
124
|
-
memo.set(url, { at: Date.now(), body, headers: r.headers });
|
|
125
|
-
return new Response(body, { headers: r.headers });
|
|
126
|
-
}
|
|
127
|
-
return real(input, init);
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── the browser ─────────────────────────────────────────────────────────────
|
|
132
|
-
// Anything that speaks the Chrome DevTools Protocol and has WebMCP: a headless
|
|
133
|
-
// Chromium service, a hosted browser, or the local Chrome. `--cdp` (or
|
|
134
|
-
// FANGORN_MCP_CDP) names it as http(s)://host:port or a ws(s):// browser
|
|
135
|
-
// endpoint. Without it, a local Chrome is reused if one is already serving
|
|
136
|
-
// this profile (another session's), else launched.
|
|
137
|
-
//
|
|
138
|
-
// Everything goes over the one browser-level WebSocket (Target.* plus flat
|
|
139
|
-
// sessions). That is the endpoint every CDP host exposes, where the per-tab
|
|
140
|
-
// /json HTTP routes are not.
|
|
141
|
-
|
|
142
|
-
const PROFILE = `${homedir()}/.cache/westmarch-mcp`;
|
|
143
|
-
|
|
144
|
-
/** An http(s) DevTools address → its browser WebSocket, reached via the host we were given. */
|
|
145
|
-
async function browserWs(http) {
|
|
146
|
-
const v = await (await fetch(new URL("/json/version", http), { signal: AbortSignal.timeout(3000) })).json();
|
|
147
|
-
const ws = new URL(v.webSocketDebuggerUrl), base = new URL(http);
|
|
148
|
-
ws.host = base.host; // the service reports its own view of itself
|
|
149
|
-
if (base.protocol === "https:") ws.protocol = "wss:";
|
|
150
|
-
return ws.toString();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function localChrome({ headed }) {
|
|
154
|
-
mkdirSync(PROFILE, { recursive: true });
|
|
155
|
-
const portFile = `${PROFILE}/DevToolsActivePort`;
|
|
156
|
-
if (existsSync(portFile)) {
|
|
157
|
-
const ws = await browserWs(`http://127.0.0.1:${readFileSync(portFile, "utf-8").split("\n")[0]}`).catch(() => null);
|
|
158
|
-
if (ws) { log("reusing the Chrome already serving this profile"); return { ws }; }
|
|
159
|
-
rmSync(portFile, { force: true }); // stale: that Chrome is gone
|
|
160
|
-
}
|
|
161
|
-
const bin = process.env.CHROME ?? "google-chrome";
|
|
162
|
-
const proc = spawn(bin, [
|
|
163
|
-
...(headed ? [] : ["--headless=new"]),
|
|
164
|
-
"--enable-features=WebMCP", "--remote-debugging-port=0", "--no-first-run",
|
|
165
|
-
"--no-default-browser-check", `--user-data-dir=${PROFILE}`, "about:blank",
|
|
166
|
-
], { stdio: "ignore" });
|
|
167
|
-
const failed = new Promise((_, j) => proc.on("error", (e) => j(new Error(
|
|
168
|
-
`no browser: cannot launch ${bin} (${e.code}). Point --cdp / FANGORN_MCP_CDP at a WebMCP browser, or set CHROME.`))));
|
|
169
|
-
// 30s: a cold start on a loaded machine or CI box has taken 16s.
|
|
170
|
-
for (let i = 0; i < 300; i++) {
|
|
171
|
-
await Promise.race([new Promise((r) => setTimeout(r, 100)), failed]);
|
|
172
|
-
if (existsSync(portFile)) return { ws: await browserWs(`http://127.0.0.1:${readFileSync(portFile, "utf-8").split("\n")[0]}`), proc };
|
|
173
|
-
}
|
|
174
|
-
proc.kill();
|
|
175
|
-
throw new Error(`${bin} did not start`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let conn;
|
|
179
|
-
/** The browser connection, opened once and reopened after it drops. */
|
|
180
|
-
function browser(opts) {
|
|
181
|
-
return conn ??= (async () => {
|
|
182
|
-
const { cdp } = opts;
|
|
183
|
-
const { ws: url, proc } = !cdp ? await localChrome(opts)
|
|
184
|
-
: /^wss?:/.test(cdp) ? { ws: cdp } : { ws: await browserWs(cdp) };
|
|
185
|
-
const ws = new WebSocket(url);
|
|
186
|
-
await new Promise((r, j) => { ws.onopen = r; ws.onerror = () => j(new Error(`cannot connect to the browser at ${url}`)); });
|
|
187
|
-
let id = 0; const pending = new Map(), dead = new Set();
|
|
188
|
-
ws.onmessage = (m) => {
|
|
189
|
-
const d = JSON.parse(m.data);
|
|
190
|
-
if (d.method === "Target.detachedFromTarget") dead.add(d.params.sessionId);
|
|
191
|
-
if (d.id !== undefined) { pending.get(d.id)?.(d); pending.delete(d.id); }
|
|
192
|
-
};
|
|
193
|
-
ws.onclose = () => { conn = null; for (const r of pending.values()) r({ error: { message: "browser connection closed" } }); };
|
|
194
|
-
const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
|
|
195
|
-
if (ws.readyState !== WebSocket.OPEN) return rej(new Error("browser connection closed"));
|
|
196
|
-
pending.set(++id, (d) => (d.error ? rej(new Error(d.error.message)) : res(d.result)));
|
|
197
|
-
ws.send(JSON.stringify({ id, method, params, ...(sessionId && { sessionId }) }));
|
|
198
|
-
});
|
|
199
|
-
const tabs = new Set();
|
|
200
|
-
// Close what we opened. A shared or remote browser outlives this process.
|
|
201
|
-
const bye = async () => {
|
|
202
|
-
await Promise.race([Promise.all([...tabs].map((t) => send("Target.closeTarget", { targetId: t }).catch(() => {}))),
|
|
203
|
-
new Promise((r) => setTimeout(r, 500))]);
|
|
204
|
-
// A local Chrome may be serving another session: leave it running
|
|
205
|
-
// while anyone else has a tab open; the last one out closes it.
|
|
206
|
-
const others = await Promise.race([send("Target.getTargets").then((r) => r.targetInfos.filter((t) => t.type === "page" && t.url !== "about:blank").length).catch(() => 0),
|
|
207
|
-
new Promise((r) => setTimeout(() => r(0), 500))]);
|
|
208
|
-
// Never a --cdp browser: that one is somebody's service.
|
|
209
|
-
if (!cdp && !others) await Promise.race([send("Browser.close").catch(() => {}), new Promise((r) => setTimeout(r, 500))]);
|
|
210
|
-
proc?.kill();
|
|
211
|
-
process.exit(0);
|
|
212
|
-
};
|
|
213
|
-
process.once("SIGINT", bye); process.once("SIGTERM", bye); process.stdin.once("end", bye);
|
|
214
|
-
log(`browser: ${cdp ?? (proc ? "launched local Chrome" : "local Chrome")}`);
|
|
215
|
-
return { send, dead, tabs };
|
|
216
|
-
})().catch((e) => { conn = null; throw e; });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** A new tab on `page`; returns `ev(expr)`, which evaluates in it. */
|
|
220
|
-
async function openTab(page, opts) {
|
|
221
|
-
const { send, dead, tabs } = await browser(opts);
|
|
222
|
-
const { targetId } = await send("Target.createTarget", { url: page });
|
|
223
|
-
tabs.add(targetId);
|
|
224
|
-
const { sessionId } = await send("Target.attachToTarget", { targetId, flatten: true });
|
|
225
|
-
return async (expression) => {
|
|
226
|
-
if (dead.has(sessionId)) throw new Error(`tab closed: ${page}`);
|
|
227
|
-
const r = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }, sessionId)
|
|
228
|
-
.catch((e) => { throw new Error(`tab closed: ${page} (${e.message})`); });
|
|
229
|
-
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text);
|
|
230
|
-
return r.result.value;
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
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
|
-
/** JSON-RPC over newline-delimited stdio. Returns `notify` for server → client messages. */
|
|
266
|
-
export function serve(handlers, { input = process.stdin, output = process.stdout } = {}) {
|
|
267
|
-
const send = (m) => output.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
|
|
268
|
-
createInterface({ input }).on("line", async (line) => {
|
|
269
|
-
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
270
|
-
if (msg.id === undefined) return; // notifications
|
|
271
|
-
const h = handlers[msg.method];
|
|
272
|
-
if (!h) return send({ id: msg.id, error: { code: -32601, message: `unknown method ${msg.method}` } });
|
|
273
|
-
try { send({ id: msg.id, result: await h(msg.params ?? {}) }); }
|
|
274
|
-
catch (e) { send({ id: msg.id, error: { code: -32000, message: e?.message ?? String(e) } }); }
|
|
275
|
-
});
|
|
276
|
-
return (method, params = {}) => send({ method, params });
|
|
277
|
-
}
|
|
28
|
+
// FANGORN_NETWORK: the SDK's network name ("baseSepolia"); unset, its default.
|
|
29
|
+
const network = async () => (await import("../publish/view.js")).network();
|
|
278
30
|
|
|
31
|
+
// ── the server ──────────────────────────────────────────────────────────────
|
|
279
32
|
const text = (o) => ({ content: [{ type: "text", text: typeof o === "string" ? o : JSON.stringify(o, null, 1) }] });
|
|
280
|
-
|
|
281
|
-
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…)" };
|
|
282
34
|
|
|
283
|
-
const
|
|
284
|
-
{ name: "
|
|
285
|
-
description: "
|
|
286
|
-
inputSchema: { type: "object",
|
|
287
|
-
{ name: "find-apps",
|
|
288
|
-
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.",
|
|
289
|
-
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: {
|
|
290
39
|
query: { type: "string", description: "What the person is looking for, in their words" },
|
|
291
|
-
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 } } } },
|
|
292
|
-
{ name: "
|
|
293
|
-
description: "
|
|
294
|
-
inputSchema: { type: "object", properties: {
|
|
295
|
-
{ name: "
|
|
296
|
-
description: "
|
|
297
|
-
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" } } } } } } },
|
|
298
62
|
];
|
|
299
63
|
|
|
300
64
|
/**
|
|
301
65
|
* The server. `deps` is the outside world, so the self-check can fake it:
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* viewTools(app) → { tools, call(name, args) } | null (no browser)
|
|
307
|
-
* openTab(page) → ev
|
|
308
|
-
* tabTools(ev), tabCall(ev, name, args)
|
|
309
|
-
* 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 })
|
|
310
70
|
*/
|
|
311
71
|
export function handlers(deps) {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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 }))];
|
|
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;
|
|
320
76
|
};
|
|
321
|
-
async function openPage(o) {
|
|
322
|
-
o.ev = await deps.openTab(o.page);
|
|
323
|
-
o.pageTools = await deps.tabTools(o.ev);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function openApp(app, { page = false } = {}) {
|
|
327
|
-
const hit = findOpen(app);
|
|
328
|
-
if (hit && (hit.ev || !page)) return hit;
|
|
329
|
-
let o = hit;
|
|
330
|
-
if (!o) {
|
|
331
|
-
let cardUrl = app, appId;
|
|
332
|
-
if (!/^https?:\/\//.test(app)) {
|
|
333
|
-
// One registry read when the name is the app's own; the scan only when it is not.
|
|
334
|
-
trace(`open ${app}: lookup`);
|
|
335
|
-
const a = await deps.findByName?.(app)
|
|
336
|
-
?? (await deps.findApps()).apps.find((x) => x.appId === app || slug(x.name) === slug(app));
|
|
337
|
-
if (!a) throw new Error(`no verified app named ${JSON.stringify(app)}; see list-apps`);
|
|
338
|
-
({ card: cardUrl, appId } = a);
|
|
339
|
-
}
|
|
340
|
-
trace(`open ${app}: resolve ${cardUrl}`);
|
|
341
|
-
const { card, page: url, verified, views, paid } = await deps.resolveCard(cardUrl);
|
|
342
|
-
trace(`open ${app}: verified`);
|
|
343
|
-
let s = slug(card.name ?? app);
|
|
344
|
-
while (open.has(s)) s += "-";
|
|
345
|
-
o = { slug: s, name: card.name, card: cardUrl, appId, verified, page: url,
|
|
346
|
-
views: deps.viewTools?.({ name: card.name, desc: card.description, views, paid }) ?? null };
|
|
347
|
-
}
|
|
348
|
-
// The browser only when asked, or when there is no other way in.
|
|
349
|
-
if (page || !o.views) await openPage(o);
|
|
350
|
-
retool(o);
|
|
351
|
-
trace(`open ${app}: ready`);
|
|
352
|
-
open.set(o.slug, o);
|
|
353
|
-
deps.notify?.("notifications/tools/list_changed");
|
|
354
|
-
return o;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function call(app, tool, args) {
|
|
358
|
-
const o = findOpen(app);
|
|
359
|
-
if (!o) throw new Error(`${app} is not open; call open-app first`);
|
|
360
|
-
if (!o.pageNames?.has(tool)) {
|
|
361
|
-
if (o.views?.tools.some((t) => t.name === tool)) return o.views.call(tool, args);
|
|
362
|
-
throw new Error(`${o.slug} has no tool ${tool}${o.ev ? "" : " (its page tools load with open-app page: true)"}`);
|
|
363
|
-
}
|
|
364
|
-
const name = o.pageNames.get(tool);
|
|
365
|
-
try { return await deps.tabCall(o.ev, name, args); }
|
|
366
|
-
catch (e) {
|
|
367
|
-
if (!/tab closed/.test(e.message)) throw e;
|
|
368
|
-
// The tab or the whole browser went away. Reopen once; the tools keep their names.
|
|
369
|
-
await openPage(o);
|
|
370
|
-
return deps.tabCall(o.ev, name, args);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
77
|
const run = {
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
89
|
const q = String(query ?? "").toLowerCase();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
app: slug(a.name), name: a.name, description: a.desc, card: a.card, appId: a.appId,
|
|
381
|
-
tools: a.tools, open: open.has(slug(a.name)),
|
|
382
|
-
})),
|
|
383
|
-
unverified: rejected.length,
|
|
384
|
-
});
|
|
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) });
|
|
385
92
|
},
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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 })) }));
|
|
390
99
|
},
|
|
391
|
-
"open-app": async ({ app, page }) => {
|
|
392
|
-
const o = await openApp(app, { page });
|
|
393
|
-
return text({ app: o.slug, name: o.name, verified: o.verified,
|
|
394
|
-
data_tools: !!o.views, page_tools: o.ev ? o.page : false,
|
|
395
|
-
call_as: `${o.slug}__<tool>, or call-app-tool`, tools: o.tools });
|
|
396
|
-
},
|
|
397
|
-
"call-app-tool": async ({ app, tool, arguments: args }) => call(app, tool, args),
|
|
398
100
|
};
|
|
399
101
|
|
|
400
102
|
return {
|
|
401
103
|
initialize: async (p) => ({
|
|
402
104
|
protocolVersion: p.protocolVersion ?? "2025-06-18",
|
|
403
|
-
capabilities: { tools: { listChanged:
|
|
404
|
-
serverInfo: { name: "fangorn-mcp", version: "0.
|
|
405
|
-
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).",
|
|
406
108
|
}),
|
|
407
109
|
ping: async () => ({}),
|
|
408
|
-
"tools/list": async () => ({ tools:
|
|
409
|
-
...META,
|
|
410
|
-
...[...open.values()].flatMap((o) => o.tools.map((t) => ({ ...t, name: `${o.slug}__${t.name}`, description: `[${o.name}] ${t.description ?? ""}` }))),
|
|
411
|
-
] }),
|
|
110
|
+
"tools/list": async () => ({ tools: TOOLS }),
|
|
412
111
|
"tools/call": async ({ name, arguments: args = {} }) => {
|
|
413
112
|
try {
|
|
414
|
-
if (run[name])
|
|
415
|
-
|
|
416
|
-
if (i > 0) return await call(name.slice(0, i), name.slice(i + 2), args);
|
|
417
|
-
throw new Error(`no tool ${name}`);
|
|
113
|
+
if (!run[name]) throw new Error(`no tool ${name}`);
|
|
114
|
+
return await run[name](args);
|
|
418
115
|
} catch (e) { return { isError: true, content: [{ type: "text", text: e?.message ?? String(e) }] }; }
|
|
419
116
|
},
|
|
420
117
|
};
|
|
421
118
|
}
|
|
422
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
|
+
|
|
423
134
|
// ── main / self-check ───────────────────────────────────────────────────────
|
|
424
135
|
// A command, not a module: installed as a bin, argv[1] is the symlink, so there is no is-main check.
|
|
425
136
|
if (process.argv[2] === "--selfcheck") {
|
|
@@ -427,142 +138,104 @@ if (process.argv[2] === "--selfcheck") {
|
|
|
427
138
|
const input = new PassThrough(), output = new PassThrough();
|
|
428
139
|
const replies = [];
|
|
429
140
|
createInterface({ input: output }).on("line", (l) => replies.push(JSON.parse(l)));
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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 });
|
|
441
156
|
const rpc = (id, method, params) => input.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
442
|
-
const wait = () => new Promise((r) => setTimeout(r, 20));
|
|
443
157
|
const call = (id, name, args) => rpc(id, "tools/call", { name, arguments: args });
|
|
158
|
+
const wait = () => new Promise((r) => setTimeout(r, 20));
|
|
444
159
|
const res = (id) => replies.find((r) => r.id === id);
|
|
445
160
|
const body = (id) => JSON.parse(res(id).result.content[0].text);
|
|
446
161
|
const assert = (c, m) => { if (!c) throw new Error(m); };
|
|
447
162
|
|
|
448
|
-
rpc(1, "initialize", {}); rpc(2, "tools/list"); call(3, "
|
|
449
|
-
|
|
450
|
-
assert(res(2).result.tools.
|
|
451
|
-
assert(body(3).
|
|
452
|
-
assert(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
assert(
|
|
456
|
-
|
|
457
|
-
call(
|
|
458
|
-
|
|
459
|
-
assert(body(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
assert(
|
|
463
|
-
|
|
464
|
-
call(10, "call-app-tool", { app: "kings-foil", tool: "boom" }); call(11, "open-app", { app: "nope" }); rpc(12, "nope"); await wait();
|
|
465
|
-
assert(res(7).result.tools.some((t) => t.name === "kings-foil__greet"), "opened tools are listed under the app");
|
|
466
|
-
assert(body(8).args.who === "a" && body(9).name === "greet", "both call paths reach the tab");
|
|
467
|
-
assert(res(10).result.isError && res(11).result.isError, "failures are tool errors");
|
|
468
|
-
assert(res(12).error.code === -32601, "unknown method");
|
|
469
|
-
|
|
470
|
-
// An app with views: data tools, no browser, until page: true.
|
|
471
|
-
{
|
|
472
|
-
const out2 = new PassThrough(), in2 = new PassThrough(), r2 = [];
|
|
473
|
-
createInterface({ input: out2 }).on("line", (l) => r2.push(JSON.parse(l)));
|
|
474
|
-
let tabs = 0;
|
|
475
|
-
const d2 = { ...deps,
|
|
476
|
-
resolveCard: async () => ({ card: { name: "Kings Foil" }, page: "https://k.test/p", verified: true, views: ["https://k.test/v"] }),
|
|
477
|
-
viewTools: (app) => ({ tools: [{ name: "search" }, { name: "greet" }], call: async (n, a) => text({ via: "views", n, a, views: app.views }) }),
|
|
478
|
-
openTab: async () => (tabs++, "ev"),
|
|
479
|
-
};
|
|
480
|
-
d2.notify = serve(handlers(d2), { input: in2, output: out2 });
|
|
481
|
-
const q = (id, name, args) => in2.write(JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } }) + "\n");
|
|
482
|
-
const got = (id) => r2.find((r) => r.id === id)?.result;
|
|
483
|
-
q(1, "open-app", { app: "Kings Foil" }); await wait(); q(2, "kings-foil__search", { query: "x" }); await wait();
|
|
484
|
-
assert(tabs === 0 && JSON.parse(got(1).content[0].text).data_tools, "views mean no browser");
|
|
485
|
-
assert(JSON.parse(got(2).content[0].text).via === "views", "data tools answer from the views");
|
|
486
|
-
q(3, "open-app", { app: "kings-foil", page: true }); await wait();
|
|
487
|
-
const names = JSON.parse(got(3).content[0].text).tools.map((t) => t.name);
|
|
488
|
-
assert(tabs === 1 && names.includes("search") && names.includes("greet") && names.includes("page-greet"), `page: true adds page tools, a clash renamed: ${names}`);
|
|
489
|
-
q(4, "kings-foil__page-greet", { who: "p" }); q(5, "kings-foil__greet", {}); await wait();
|
|
490
|
-
assert(JSON.parse(got(4).content[0].text).ev === "ev" && JSON.parse(got(5).content[0].text).via === "views", "each name reaches its own side");
|
|
491
|
-
}
|
|
492
|
-
console.log("mcp.js self-check ok — one server, apps opened on demand, data tools without a browser, page tools on request");
|
|
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");
|
|
493
179
|
process.exit(0);
|
|
494
180
|
}
|
|
495
181
|
|
|
496
|
-
const argv = process.argv.slice(2);
|
|
497
|
-
globalThis.fetch = cachingFetch();
|
|
498
182
|
// Node gives each address 250ms before trying the next; on a slow link, or a
|
|
499
183
|
// host with no working IPv6, every attempt times out and fetch fails outright.
|
|
500
184
|
net.setDefaultAutoSelectFamilyAttemptTimeout?.(2000);
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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: {} } }));
|
|
196
|
+
const chainClient = async () => {
|
|
197
|
+
if (!pub) {
|
|
198
|
+
const [{ createPublicClient, http }, n] = await Promise.all([import("viem"), network()]);
|
|
199
|
+
pub = createPublicClient({ chain: n.chain, transport: http(n.rpcUrl) });
|
|
200
|
+
}
|
|
201
|
+
return pub;
|
|
202
|
+
};
|
|
203
|
+
// Circle's USDC on Base Sepolia. ponytail: one network's; FANGORN_USDC for another.
|
|
204
|
+
const USDC = process.env.FANGORN_USDC || "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
509
205
|
const deps = {
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
})
|
|
518
|
-
foundAt = Date.now();
|
|
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`] }) };
|
|
519
214
|
}
|
|
520
|
-
return
|
|
215
|
+
return explorer;
|
|
521
216
|
},
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const { ex, dir } = explored;
|
|
530
|
-
const { EMBED_MODEL } = await import("../core/embed.js");
|
|
531
|
-
const qv = query?.trim() ? await vt.queryVector(query) : null;
|
|
532
|
-
const apps = ex.rankApps(dir.apps, query, { qv, model: EMBED_MODEL }).map(ex.brief);
|
|
533
|
-
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)) });
|
|
534
224
|
},
|
|
535
|
-
|
|
536
|
-
const [
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
return null;
|
|
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) };
|
|
543
232
|
},
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return
|
|
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 };
|
|
549
238
|
},
|
|
550
|
-
openTab: (page) => openTab(page, { headed, cdp }),
|
|
551
|
-
tabTools, tabCall,
|
|
552
|
-
};
|
|
553
|
-
const h = handlers(deps);
|
|
554
|
-
// The client starts this server with the session, long before an agent opens an
|
|
555
|
-
// app: load the SDK and the embedding model then, behind the agent's first
|
|
556
|
-
// thought. After the initialize reply, not before: both block the thread while
|
|
557
|
-
// they load, and the client is waiting on that reply.
|
|
558
|
-
const reply = h.initialize;
|
|
559
|
-
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
|
-
});
|
|
565
|
-
return reply(p);
|
|
566
239
|
};
|
|
567
|
-
|
|
568
|
-
log(`ready:
|
|
240
|
+
serve(handlers(deps));
|
|
241
|
+
log(`ready: explorer ${EXPLORER}`);
|