@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.
Files changed (386) hide show
  1. package/README.md +35 -5
  2. package/docs/app-to-agent.md +113 -620
  3. package/package.json +18 -18
  4. package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
  5. package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
  6. package/site/dist/assets/index-CkP1WhkV.js +8 -0
  7. package/site/dist/assets/roles-BDPDWrwo.js +1 -0
  8. package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  9. package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
  10. package/site/dist/assets/transformers.web-CwR_IYQV.js +40 -0
  11. package/site/dist/index.html +4 -5
  12. package/site/feed.js +16 -1
  13. package/site/no-model.js +4 -0
  14. package/site/worker.js +382 -59
  15. package/src/agent/agent-card.js +51 -37
  16. package/src/agent/mcp.js +173 -500
  17. package/src/agent/tools.js +3 -2
  18. package/src/agent/ui.js +1 -1
  19. package/src/agent/view-tools.js +11 -30
  20. package/src/agent/x402.js +194 -85
  21. package/src/core/embed.js +56 -53
  22. package/src/core/embed.worker.js +1 -1
  23. package/src/core/rank.js +1 -1
  24. package/src/core/roles.js +7 -7
  25. package/src/core/shard.js +12 -8
  26. package/src/discover/apps.js +103 -67
  27. package/src/discover/corpora.js +1 -1
  28. package/src/discover/directory.js +3 -3
  29. package/src/market/cohort.js +1 -1
  30. package/src/market/demand.js +1 -1
  31. package/src/market/reactions.js +24 -18
  32. package/src/publish/devnet.js +146 -0
  33. package/src/publish/graph.js +1 -1
  34. package/src/publish/init.js +74 -0
  35. package/src/publish/lint.js +3 -2
  36. package/src/publish/ship.js +433 -288
  37. package/src/publish/view-cli.js +14 -10
  38. package/src/publish/view.js +485 -255
  39. package/src/taste/steam.js +1 -1
  40. package/src/taste/taste-doc.js +4 -3
  41. package/src/taste/taste.js +3 -3
  42. package/src/taste/vault.js +4 -2
  43. package/template/README.md +48 -0
  44. package/template/app.json +27 -0
  45. package/template/eval/goals.md +7 -0
  46. package/template/eval/golden.jsonl +0 -0
  47. package/template/gitignore +3 -0
  48. package/template/package.json +13 -0
  49. package/template/wrangler.json +12 -0
  50. package/plugin/scripts/fx.mjs +0 -268
  51. package/site/dist/assets/AccountNotFoundScreen-C4njXc69-BOfmiSH1.js +0 -1
  52. package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js +0 -31
  53. package/site/dist/assets/Address-H8kD1d9O-C85sJ79B.js +0 -17
  54. package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js +0 -6
  55. package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js +0 -1
  56. package/site/dist/assets/ApiController-CYdiyu9B.js +0 -5
  57. package/site/dist/assets/ArrowsRightLeftIcon-cp60yobq.js +0 -1
  58. package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js +0 -1
  59. package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js +0 -1
  60. package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js +0 -1
  61. package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js +0 -103
  62. package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js +0 -1
  63. package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
  64. package/site/dist/assets/CaptchaScreen-DbpvkIGx-D8eDcAkc.js +0 -1
  65. package/site/dist/assets/CheckCircleIcon-BM409mGI.js +0 -1
  66. package/site/dist/assets/CheckCircleIcon-NCOQc0Gs.js +0 -1
  67. package/site/dist/assets/Checkbox-D1EDeo41-rHlvsZzP.js +0 -52
  68. package/site/dist/assets/ChevronDownIcon-CW-4MSpE.js +0 -1
  69. package/site/dist/assets/Chip-CZKIKt9K-CutJG010.js +0 -26
  70. package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js +0 -16
  71. package/site/dist/assets/ConnectEmailForm-B-VRE0MN-N9J6T_YL.js +0 -3
  72. package/site/dist/assets/ConnectLedgerScreen-DWHABGot-h0uYIUi6.js +0 -10
  73. package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js +0 -1
  74. package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js +0 -1
  75. package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
  76. package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js +0 -95
  77. package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
  78. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-BeWLchcf.js +0 -7
  79. package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
  80. package/site/dist/assets/ConnectorUtil-Cq4lWnfe.js +0 -21
  81. package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
  82. package/site/dist/assets/CopyToClipboard-i_OQSBJr-DujUHNzW.js +0 -39
  83. package/site/dist/assets/CopyableText-CQapvaMr-DMIdpDaU.js +0 -28
  84. package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-Csju14vF.js +0 -1
  85. package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js +0 -22
  86. package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js +0 -1
  87. package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js +0 -1
  88. package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-B3vY888G.js +0 -187
  89. package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DmdQaFI2.js +0 -8
  90. package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js +0 -1
  91. package/site/dist/assets/EmailInputForm-Cqo1mda8-B2NG3K5V.js +0 -124
  92. package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js +0 -1
  93. package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js +0 -1
  94. package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js +0 -23
  95. package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js +0 -1
  96. package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js +0 -1
  97. package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js +0 -1
  98. package/site/dist/assets/EnrollTotp-Dibew4dp-SA2pvNEc.js +0 -66
  99. package/site/dist/assets/EnvelopeIcon-Dh2vSVj4.js +0 -1
  100. package/site/dist/assets/ErrorBanner-BcpGRt0h-BKOYTyCi.js +0 -21
  101. package/site/dist/assets/ErrorMessage-D8VaAP5m-CrZOnUyg.js +0 -8
  102. package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
  103. package/site/dist/assets/ErrorScreen-DipdTWzE-c8g8En21.js +0 -4
  104. package/site/dist/assets/ExclamationCircleIcon-DUA2wMfO.js +0 -1
  105. package/site/dist/assets/ExclamationTriangleIcon-eZfaqcFq.js +0 -1
  106. package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js +0 -75
  107. package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js +0 -28
  108. package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
  109. package/site/dist/assets/FingerPrintIcon-Dm6kxmg7.js +0 -1
  110. package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js +0 -1
  111. package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js +0 -1
  112. package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js +0 -58
  113. package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js +0 -1
  114. package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js +0 -1
  115. package/site/dist/assets/GlobeAltIcon-vqrVV5ho.js +0 -1
  116. package/site/dist/assets/GooglePay-B53WnudL-BX5_WtCx.js +0 -1
  117. package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js +0 -4
  118. package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js +0 -1
  119. package/site/dist/assets/InfoBanner-Cb3p1z12-DLBl_Ywy.js +0 -22
  120. package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js +0 -1
  121. package/site/dist/assets/InstallWalletScreen-DTA8dwLv-CxreyXoD.js +0 -1
  122. package/site/dist/assets/JsonTree-BHzNC-ic-DcNcbSoM.js +0 -34
  123. package/site/dist/assets/LabelXs-oqZNqbm_-iafrj25N.js +0 -6
  124. package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
  125. package/site/dist/assets/Layouts-BMRfo5hw-CeCOyCLK.js +0 -77
  126. package/site/dist/assets/Link-BdDilT2T-BEDEdHuS.js +0 -40
  127. package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js +0 -77
  128. package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
  129. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js +0 -117
  130. package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
  131. package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-C0DuD14j.js +0 -1
  132. package/site/dist/assets/Loader-UAZqxsnw-Cy7hEE2f.js +0 -62
  133. package/site/dist/assets/LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js +0 -17
  134. package/site/dist/assets/LockClosedIcon-BgY_5v_Z.js +0 -1
  135. package/site/dist/assets/LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js +0 -1
  136. package/site/dist/assets/ManualTransferScreen-CHqcuqbs-tXiZDHmN.js +0 -1
  137. package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js +0 -1
  138. package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js +0 -1
  139. package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js +0 -1
  140. package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js +0 -1
  141. package/site/dist/assets/ModalFooter-BCuEK_nz-c-khvG4Y.js +0 -403
  142. package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js +0 -16
  143. package/site/dist/assets/OAuthStatusScreen-ufbOa-da-CeiGcSia.js +0 -1
  144. package/site/dist/assets/OpenLink-CUpJ1mOr-DfAvJRyK.js +0 -39
  145. package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js +0 -1
  146. package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js +0 -6
  147. package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js +0 -20
  148. package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
  149. package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
  150. package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
  151. package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
  152. package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
  153. package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
  154. package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
  155. package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
  156. package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
  157. package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
  158. package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
  159. package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
  160. package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
  161. package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
  162. package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
  163. package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
  164. package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
  165. package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
  166. package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
  167. package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
  168. package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
  169. package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
  170. package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
  171. package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
  172. package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
  173. package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
  174. package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
  175. package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
  176. package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
  177. package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
  178. package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
  179. package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
  180. package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
  181. package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
  182. package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
  183. package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
  184. package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
  185. package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
  186. package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
  187. package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
  188. package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
  189. package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
  190. package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
  191. package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
  192. package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
  193. package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
  194. package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
  195. package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
  196. package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
  197. package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
  198. package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
  199. package/site/dist/assets/PhoneIcon-CvIiPwYQ.js +0 -1
  200. package/site/dist/assets/PinInput-DbZ0b1i1-CDXZBdQx.js +0 -247
  201. package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CtQWBiV2.js +0 -1
  202. package/site/dist/assets/QrCode-DbnZwlkH-h6Bhoe5U.js +0 -22
  203. package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js +0 -1
  204. package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js +0 -1
  205. package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js +0 -7
  206. package/site/dist/assets/SIWXUtil-TUCxa7aA.js +0 -1
  207. package/site/dist/assets/Screen-D30meWsY-CCeVNuWs.js +0 -175
  208. package/site/dist/assets/ScreenHeader-CHmc4-Lu-DjyrH7IK.js +0 -31
  209. package/site/dist/assets/ScreenLayout-OwaQQcxH-CrQM6kvA.js +0 -1
  210. package/site/dist/assets/SelectSourceAsset-CWFog6gm-D7yTw87h.js +0 -140
  211. package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js +0 -14
  212. package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js +0 -11
  213. package/site/dist/assets/ShieldCheckIcon-o6RHYev1.js +0 -1
  214. package/site/dist/assets/SignRequestScreen-BHKv32KR-DDpMYyJ1.js +0 -26
  215. package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js +0 -1
  216. package/site/dist/assets/StackedContainer-B2vaEl56-eQCM2cmv.js +0 -24
  217. package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js +0 -20
  218. package/site/dist/assets/Subtitle-CV-2yKE4-zxWw2m1Q.js +0 -13
  219. package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js +0 -1
  220. package/site/dist/assets/Title-BnzYV3Is-B_JpTatT.js +0 -7
  221. package/site/dist/assets/TodoList-DnyULl18-C8Eyy28X.js +0 -36
  222. package/site/dist/assets/TransactionDetails-e9nesl30-C4MpIHR-.js +0 -109
  223. package/site/dist/assets/TransactionErrorView-BDBNqA_f-p1T9WVMx.js +0 -178
  224. package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js +0 -31
  225. package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js +0 -6
  226. package/site/dist/assets/TurnstileWrapper-B7sNddys-Jwy1tJtm.js +0 -1
  227. package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js +0 -1
  228. package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js +0 -1
  229. package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-D7m70C4b.js +0 -1
  230. package/site/dist/assets/Value-DTgR824E-BRuIGiec.js +0 -27
  231. package/site/dist/assets/WalletCards-DH1rqayz-Bgp9Fsgs.js +0 -1
  232. package/site/dist/assets/WalletIcon-gKf0gTw6.js +0 -1
  233. package/site/dist/assets/WalletInfoCard-sq_2wgFg-DpNSAGte.js +0 -29
  234. package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js +0 -1
  235. package/site/dist/assets/WalletLink-DK5LB5Ha-DHaL350n.js +0 -20
  236. package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
  237. package/site/dist/assets/WarningBanner-ZZqCEtZK-2eU4Qo74.js +0 -21
  238. package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
  239. package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
  240. package/site/dist/assets/_esm-BdeM6pxJ.js +0 -1
  241. package/site/dist/assets/accounts-D96UKWkC.js +0 -1
  242. package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
  243. package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
  244. package/site/dist/assets/baseSepolia-DX-qGsik.js +0 -1
  245. package/site/dist/assets/basic-oIwwSyu4.js +0 -1575
  246. package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
  247. package/site/dist/assets/ccip-65sIfSPR.js +0 -1
  248. package/site/dist/assets/ccip-BZvRTL6g.js +0 -7
  249. package/site/dist/assets/ccip-DA_zG341.js +0 -1
  250. package/site/dist/assets/chain-DM3oH6Px.js +0 -8
  251. package/site/dist/assets/check-BPgcwEPP.js +0 -1
  252. package/site/dist/assets/chevron-down-BSsbXkK-.js +0 -1
  253. package/site/dist/assets/circle-alert-DWaeLZNj.js +0 -1
  254. package/site/dist/assets/circle-check-big-Chb1ivw9.js +0 -1
  255. package/site/dist/assets/circle-x-AV9Cet0R.js +0 -1
  256. package/site/dist/assets/client-CZG0ndth.js +0 -2
  257. package/site/dist/assets/client-Dsa7_3V3.js +0 -9
  258. package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
  259. package/site/dist/assets/copy-Ckr0AkL6.js +0 -1
  260. package/site/dist/assets/core-snws5xwv.js +0 -12
  261. package/site/dist/assets/createLucideIcon-IPW50ckY.js +0 -1
  262. package/site/dist/assets/defineChain-CiuCXUZW.js +0 -1
  263. package/site/dist/assets/dijkstra-Drfl93V4.js +0 -1
  264. package/site/dist/assets/dist-Cm7pNAtL.js +0 -1
  265. package/site/dist/assets/dist-CoS6E-3x.js +0 -1
  266. package/site/dist/assets/dist-Dl07uGq1.js +0 -1
  267. package/site/dist/assets/dist-DnGANz51.js +0 -16
  268. package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
  269. package/site/dist/assets/errors-BQ3N-RVt-BLqjTPNR.js +0 -1
  270. package/site/dist/assets/esm-B5cbIQaz.js +0 -37
  271. package/site/dist/assets/esm-ByALO6mA.js +0 -1
  272. package/site/dist/assets/esm-C3DslVJx.js +0 -1
  273. package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
  274. package/site/dist/assets/esm-n2lWo7kz.js +0 -1
  275. package/site/dist/assets/ethers-zTjbodk5-wHhL_BQ1.js +0 -1
  276. package/site/dist/assets/eventemitter3-CL76jPsW.js +0 -1
  277. package/site/dist/assets/external-link-D-uehwM2.js +0 -1
  278. package/site/dist/assets/farcaster-DPlSjvF5-BqTvDXBR.js +0 -1
  279. package/site/dist/assets/features-DQ0UYy6K.js +0 -2
  280. package/site/dist/assets/fingerprint-pattern-D8ytA16Q.js +0 -1
  281. package/site/dist/assets/floating-ui.react-dom-DKWOL9UG.js +0 -1
  282. package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
  283. package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
  284. package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
  285. package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js +0 -1
  286. package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
  287. package/site/dist/assets/getErc20Balance-jXNpFf0N-Bb9WM31M.js +0 -1
  288. package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-KPrdn04K.js +0 -1
  289. package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
  290. package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
  291. package/site/dist/assets/guards-BIW-GiMJ-CHfXo8XI.js +0 -1
  292. package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
  293. package/site/dist/assets/hourglass-CPG1HZmf.js +0 -1
  294. package/site/dist/assets/id-UqEwL2BI-BkKm533U.js +0 -1
  295. package/site/dist/assets/index-1HYFYA-W.js +0 -8
  296. package/site/dist/assets/index-BdEw5wyl-ChSJvPZk.js +0 -1
  297. package/site/dist/assets/index-CWARkn2w-Cezl_uBr.js +0 -61
  298. package/site/dist/assets/index-DvMddJcd-BatvgC-S.js +0 -12
  299. package/site/dist/assets/index-YiCw0Y_x-CX5i3Z87.js +0 -437
  300. package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
  301. package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
  302. package/site/dist/assets/index.es-BmqyoBEM.js +0 -2
  303. package/site/dist/assets/index.es-C1v1or--.js +0 -3
  304. package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js +0 -1
  305. package/site/dist/assets/jsx-runtime-BbDfbRii.js +0 -1
  306. package/site/dist/assets/landmark-CH6LV6F0.js +0 -1
  307. package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
  308. package/site/dist/assets/localBatchGatewayRequest-3kLrX_c7.js +0 -32
  309. package/site/dist/assets/lock-DcskI4Bb.js +0 -1
  310. package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
  311. package/site/dist/assets/mail-Bs0r5yOi.js +0 -1
  312. package/site/dist/assets/modal-context-CwmSeFcv-DruX9J1E.js +0 -1
  313. package/site/dist/assets/native-BKj27wfc.js +0 -1
  314. package/site/dist/assets/ofetch.CWycOUEr-CHp-crOQ.js +0 -1
  315. package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
  316. package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
  317. package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
  318. package/site/dist/assets/pkce-DXF0yKjP-ChQhE0st.js +0 -1
  319. package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
  320. package/site/dist/assets/privy-context-81KmsBw2-CjWGzyg7.js +0 -2
  321. package/site/dist/assets/property-DuZ9DLP4.js +0 -3
  322. package/site/dist/assets/react-DCXkXSrv.js +0 -1
  323. package/site/dist/assets/react-DQnMl2Xv.js +0 -1
  324. package/site/dist/assets/react-DrJ0MmMO.js +0 -1
  325. package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
  326. package/site/dist/assets/react-dom-5dW7aKoP.js +0 -1
  327. package/site/dist/assets/reservoir-x-nGuZkT-BMX127i6.js +0 -1
  328. package/site/dist/assets/roles-32M1l9jI.js +0 -1
  329. package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
  330. package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
  331. package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
  332. package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
  333. package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
  334. package/site/dist/assets/secp256k1-_MleGpqL.js +0 -1
  335. package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
  336. package/site/dist/assets/serializeAuthorizationList-CX8hGL56.js +0 -1
  337. package/site/dist/assets/serializeSignature-DfxkPnu-.js +0 -2
  338. package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
  339. package/site/dist/assets/shared-DRZUmnL5-xqAUARWo.js +0 -193
  340. package/site/dist/assets/shared-FM0rljBt--Avg6q2R.js +0 -21
  341. package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
  342. package/site/dist/assets/simulateTransaction-CfuOrae4-64lWYJAL.js +0 -1
  343. package/site/dist/assets/smartphone-CcEZ-XAy.js +0 -1
  344. package/site/dist/assets/src-BBjkfi-Q.js +0 -1
  345. package/site/dist/assets/storage-Ufl800Ne-D0bZBw-H.js +0 -9
  346. package/site/dist/assets/stringify-BlDm7tuc.js +0 -21
  347. package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
  348. package/site/dist/assets/styles-BsotlekN-CXEB4Ud9.js +0 -9
  349. package/site/dist/assets/styles-Cm9H63WO-Br4QtzUL.js +0 -92
  350. package/site/dist/assets/styles-DVyDvTdj-BTABn2K8.js +0 -74
  351. package/site/dist/assets/styles-hslXy6xG-D-49b1rk.js +0 -164
  352. package/site/dist/assets/telegram-B-JqnkqZ-RBoY3PLx.js +0 -1
  353. package/site/dist/assets/to-ui-error-DWe-qwS3-Cx2bk1kZ.js +0 -4
  354. package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
  355. package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
  356. package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
  357. package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
  358. package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
  359. package/site/dist/assets/triangle-alert-CWjYH75H.js +0 -1
  360. package/site/dist/assets/twitch-5IOe4sIQ-lDtgk0os.js +0 -1
  361. package/site/dist/assets/types-4CuOD23i.js +0 -1
  362. package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js +0 -1
  363. package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-CC2pq27z.js +0 -1
  364. package/site/dist/assets/useActiveWallet-B-yZYGc6-D7Ik-LxJ.js +0 -123
  365. package/site/dist/assets/useGetSolPrice-x7gfUIHJ-D1hmQfpA.js +0 -1
  366. package/site/dist/assets/useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js +0 -1
  367. package/site/dist/assets/useI18n-C-5n_noF-BcqC76yj.js +0 -1
  368. package/site/dist/assets/useWalletBalance-CZnP5f_f-Dw2GIJ4g.js +0 -1
  369. package/site/dist/assets/useWallets-X1ndvJ_U-DRjz7uYD.js +0 -1
  370. package/site/dist/assets/utils-BkR7EQjg.js +0 -1
  371. package/site/dist/assets/utils-DJhBNOZA.js +0 -1
  372. package/site/dist/assets/utils-DdckVfQr.js +0 -1
  373. package/site/dist/assets/w3m-modal-BeVrBOIf.js +0 -1174
  374. package/site/dist/assets/wallet-Bn85Sg2W.js +0 -1
  375. package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
  376. package/site/dist/assets/wallet-connect-BvJtUsDt-Y03hhqFN.js +0 -1
  377. package/site/dist/assets/walletconnect-registry-xxG8XLCb-D1SSFvMp.js +0 -1
  378. package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
  379. package/site/dist/assets/wui-ux-by-reown-D34ILNLS.js +0 -1147
  380. package/site/dist/assets/x-rlSp9U7W.js +0 -1
  381. package/src/discover/explore.js +0 -271
  382. package/src/market/settle.js +0 -165
  383. package/src/market/terms.js +0 -200
  384. package/src/publish/cli.js +0 -336
  385. package/src/publish/envelope.js +0 -185
  386. package/src/publish/manifest.js +0 -142
package/src/agent/mcp.js CHANGED
@@ -1,425 +1,136 @@
1
1
  #!/usr/bin/env node
2
- // Every Fangorn app's WebMCP tools, behind one stdio MCP server.
2
+ // Fangorn for agents: every publisher's records, found through the explorer.
3
3
  //
4
- // fangorn-mcp [--cdp <url>] [--headed] [--from-block <n>]
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
- // WebMCP tools live on `document.modelContext` inside a loaded tab. There is no
9
- // endpoint, so an agent outside the browser cannot reach them, and one MCP
10
- // server per app would mean one registration per app. Instead this server is
11
- // the directory and the browser both:
7
+ // 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
- // list-apps apps read off the chain (AppAgentChanged → verified cards)
14
- // open-app verify one app's card, open its page in a tab, and add its
15
- // tools to this server as `<app>__<tool>` (tools/list_changed)
16
- // call-app-tool the same call by name, for clients that ignore list_changed
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
- // One browser, one tab per opened app: a WebMCP browser at --cdp, or else a
19
- // local Chrome with WebMCP on, headless unless --headed. Calls
20
- // are relayed to the tab's own `modelContext.executeTool`, so the tools, their
21
- // schemas and their answers are the page's, and queries still run in the tab.
22
- // A local Chrome's profile persists (~/.cache/westmarch-mcp), so a page's
23
- // downloads, such as an embedding model, are fetched once. A tab or browser that
24
- // dies is reopened on the next call.
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
- let client;
43
- async function fangorn() {
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
- // MCP tool names: [a-zA-Z0-9_-]{1,64}. `__` separates app from tool.
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 META = [
284
- { name: "list-apps",
285
- description: "List the Fangorn apps registered on chain whose agent cards verify: name, description, card URL, and the WebMCP tools each page offers. Filter with `query`. Call open-app on one to use its tools.",
286
- inputSchema: { type: "object", properties: { query: { type: "string", description: "Case-insensitive match on name or description" } } } },
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: "open-app",
293
- description: "Verify an app's card against the chain and add its tools to this server as `<app>__<tool>`. An app that publishes views gets fast data tools (describe, search, get, similar, count, browse) with no browser. `page: true` also opens the app's page in a browser for its own WebMCP tools (slower). `app` is a name from list-apps, an appId, or a card URL.",
294
- inputSchema: { type: "object", properties: { app: { type: "string" }, page: { type: "boolean", description: "Also load the page's own WebMCP tools in a browser" } }, required: ["app"] } },
295
- { name: "call-app-tool",
296
- description: "Call one tool of an opened app. Same as calling `<app>__<tool>` directly.",
297
- inputSchema: { type: "object", properties: { app: { type: "string" }, tool: { type: "string" }, arguments: { type: "object" } }, required: ["app", "tool"] } },
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
- * findApps() → { apps: [{ name, desc, card, appId }], rejected }
303
- * exploreApps(query) → { apps: ranked explore.js entries, ranked_by }
304
- * findByName(name) → { card, appId } | null (optional: a direct registry read)
305
- * resolveCard(url) → { card, page, verified, views }
306
- * viewTools(app) → { tools, call(name, args) } | null (no browser)
307
- * openTab(page) → ev
308
- * tabTools(ev), tabCall(ev, name, args)
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 open = new Map(); // slug → { name, card, verified, page, views?, pages?, tools }
313
-
314
- const findOpen = (app) => open.get(slug(app)) ?? [...open.values()].find((o) => o.card === app || o.appId === app);
315
- const retool = (o) => {
316
- const taken = new Set(o.views?.tools.map((t) => t.name));
317
- // A page tool that shares a data tool's name is kept, under `page-<name>`.
318
- o.pageNames = new Map((o.pageTools ?? []).map((t) => [taken.has(t.name) ? `page-${t.name}` : t.name, t.name]));
319
- o.tools = [...(o.views?.tools ?? []), ...(o.pageTools ?? []).map((t) => ({ ...t, name: taken.has(t.name) ? `page-${t.name}` : t.name }))];
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
- "list-apps": async ({ query }) => {
376
- const { apps, rejected } = await deps.findApps();
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
- return text({
379
- apps: apps.filter((a) => !q || `${a.name} ${a.desc}`.toLowerCase().includes(q)).map((a) => ({
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
- "find-apps": async ({ query, limit = 10 }) => {
387
- const { apps, ranked_by } = await deps.exploreApps(query);
388
- return text({ query: query ?? null, ranked_by, apps: apps.slice(0, Math.min(50, Math.max(1, limit)))
389
- .map((a) => ({ ...a, open: open.has(a.app) })) });
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: true } },
404
- serverInfo: { name: "fangorn-mcp", version: "0.1.0" },
405
- instructions: "Fangorn apps are static sites anchored on chain. list-apps to find one, open-app to load it, then call its tools as <app>__<tool>. Start with <app>__describe when it exists.",
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]) return await run[name](args);
415
- const i = name.indexOf("__");
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
- let opened = 0;
431
- const deps = {
432
- findApps: async () => ({ apps: [{ name: "Kings Foil", desc: "trials", card: "https://k.test/card", appId: "0xk", tools: ["greet"] }], rejected: [{}] }),
433
- exploreApps: async (q) => ({ ranked_by: q ? "meaning" : null, apps: [{ app: "kings-foil", name: "Kings Foil", relevant: true }, { app: "b", name: "B", relevant: false }] }),
434
- resolveCard: async () => ({ card: { name: "Kings Foil" }, page: "https://k.test/p", verified: true }),
435
- openTab: async () => (opened++ ? "ev" : "dead"), // the first tab dies
436
- tabTools: async () => [{ name: "greet", description: "Hi.", inputSchema: { type: "object" } }],
437
- tabCall: async (ev, name, args) => (name === "boom" ? Promise.reject(new Error("no tool boom"))
438
- : ev === "dead" ? Promise.reject(new Error("tab closed: x")) : text({ ev, name, args })),
439
- };
440
- 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 });
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, "list-apps", { query: "TRIAL" }); call(4, "kings-foil__greet"); await wait();
449
- assert(res(1).result.capabilities.tools.listChanged, "advertises list_changed");
450
- assert(res(2).result.tools.length === 4, "only the meta tools before anything is open");
451
- assert(body(3).apps[0].app === "kings-foil" && body(3).unverified === 1, "list-apps filters and slugs");
452
- assert(res(4).result.isError && /not open/.test(res(4).result.content[0].text), "an unopened app's tool is an error");
453
-
454
- call(30, "find-apps", { query: "trials", limit: 1 }); await wait();
455
- assert(body(30).ranked_by === "meaning" && body(30).apps.length === 1 && body(30).apps[0].open === false, "find-apps ranks, caps, and says what is open");
456
-
457
- call(5, "open-app", { app: "Kings Foil" }); await wait();
458
- call(6, "open-app", { app: "https://k.test/card" }); await wait();
459
- assert(body(5).tools[0].name === "greet" && opened === 1, "open once, by name or by card URL");
460
- call(50, "kings-foil__greet", { who: "b" }); await wait();
461
- assert(body(50).ev === "ev" && opened === 2, "a dead tab is reopened and the call retried");
462
- assert(replies.some((r) => r.method === "notifications/tools/list_changed"), "opening notifies");
463
- rpc(7, "tools/list"); call(8, "kings-foil__greet", { who: "a" }); call(9, "call-app-tool", { app: "kings-foil", tool: "greet" });
464
- call(10, "call-app-tool", { app: "kings-foil", tool: "boom" }); call(11, "open-app", { app: "nope" }); rpc(12, "nope"); await wait();
465
- assert(res(7).result.tools.some((t) => t.name === "kings-foil__greet"), "opened tools are listed under the app");
466
- assert(body(8).args.who === "a" && body(9).name === "greet", "both call paths reach the tab");
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 vt = await import("./view-tools.js");
502
- const stale = staleFetch(globalThis.fetch);
503
- const fb = argv.indexOf("--from-block");
504
- const fromBlock = fb >= 0 ? BigInt(argv[fb + 1]) : DEFAULT_FROM_BLOCK;
505
- const headed = argv.includes("--headed");
506
- const ci = argv.indexOf("--cdp");
507
- const cdp = ci >= 0 ? argv[ci + 1] : process.env.FANGORN_MCP_CDP || undefined;
508
- let found, foundAt = 0, explored, exploredAt = 0;
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
- // ponytail: cached for a minute. The scan is a few RPC calls plus a fetch per card.
511
- findApps: async () => {
512
- if (!found || Date.now() - foundAt > 60_000) {
513
- const { apps, rejected } = await listApps(await fangorn(), { fromBlock });
514
- found = { rejected, apps: await Promise.all(apps.map(async (a) => {
515
- const card = await (await fetch(a.card)).json().catch(() => ({}));
516
- return { ...a, tools: card.skills?.filter((s) => s.tags?.includes("webmcp")).map((s) => s.id) ?? [] };
517
- })) };
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 found;
215
+ return explorer;
521
216
  },
522
- // ponytail: cached for a minute, like findApps. Two getLogs, a card and catalog per app.
523
- exploreApps: async (query) => {
524
- if (!explored || Date.now() - exploredAt > 60_000) {
525
- const [ex, { createPublicClient, http }, { arbitrumSepolia }] = await Promise.all([import("../discover/explore.js"), import("viem"), import("viem/chains")]);
526
- explored = { ex, dir: await ex.readDirectory(createPublicClient({ chain: arbitrumSepolia, transport: http() })) };
527
- exploredAt = Date.now();
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
- findByName: async (name) => {
536
- const [f, { appId }] = await Promise.all([fangorn(), import("@fangorn-network/sdk")]);
537
- const reg = f.getAppRegistry();
538
- for (const id of /^0x[0-9a-fA-F]{64}$/.test(name) ? [name] : [...new Set([name, name.toLowerCase()])].map(appId)) {
539
- const card = await reg.appAgentUri(id).catch(() => "");
540
- if (card) return { card, appId: id };
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
- resolveCard,
545
- viewTools: (app) => {
546
- const v = vt.viewTools(app, { fetchCatalog: stale });
547
- if (v) vt.warm({ cacheDir: `${CACHE}/models` }); // already started at initialize; this is the fallback
548
- return v;
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
- deps.notify = serve(h);
568
- log(`ready: apps from block ${fromBlock}; browser ${cdp ?? "local Chrome"}, connected on the first open-app`);
240
+ serve(handlers(deps));
241
+ log(`ready: explorer ${EXPLORER}`);