@aibtc/mcp-server 1.58.0 → 1.60.0

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 (265) hide show
  1. package/README.md +41 -0
  2. package/dist/bridge/index.d.ts +31 -0
  3. package/dist/bridge/index.js +285 -0
  4. package/dist/index.js +28 -5
  5. package/dist/services/credentials.js +7 -2
  6. package/dist/services/defi.service.js +23 -11
  7. package/dist/services/hiro-api.d.ts +10 -0
  8. package/dist/services/hiro-api.js +30 -8
  9. package/dist/services/nft.service.js +14 -5
  10. package/dist/services/pillar-api.service.d.ts +8 -0
  11. package/dist/services/pillar-api.service.js +13 -1
  12. package/dist/services/spend-limiter.d.ts +45 -0
  13. package/dist/services/spend-limiter.js +218 -0
  14. package/dist/services/wallet-manager.js +5 -0
  15. package/dist/services/x402.service.js +63 -13
  16. package/dist/tools/bitcoin.tools.js +19 -0
  17. package/dist/tools/endpoint.tools.js +3 -1
  18. package/dist/tools/inbox.tools.js +9 -12
  19. package/dist/tools/news.tools.js +14 -16
  20. package/dist/tools/pillar-direct.tools.js +29 -22
  21. package/dist/tools/sbtc.tools.js +2 -9
  22. package/dist/tools/scaffold.tools.js +9 -14
  23. package/dist/tools/stacking-lottery.tools.js +8 -5
  24. package/dist/tools/yield-dashboard.tools.js +10 -14
  25. package/dist/transactions/builder.js +5 -0
  26. package/dist/utils/encryption.js +2 -0
  27. package/package.json +11 -5
  28. package/skill/SKILL.md +1 -1
  29. package/skill/references/genesis-lifecycle.md +105 -92
  30. package/dist/config/bitcoin-constants.d.ts.map +0 -1
  31. package/dist/config/bitcoin-constants.js.map +0 -1
  32. package/dist/config/caip.d.ts.map +0 -1
  33. package/dist/config/caip.js.map +0 -1
  34. package/dist/config/competition.d.ts.map +0 -1
  35. package/dist/config/competition.js.map +0 -1
  36. package/dist/config/contracts.d.ts.map +0 -1
  37. package/dist/config/contracts.js.map +0 -1
  38. package/dist/config/index.d.ts.map +0 -1
  39. package/dist/config/index.js.map +0 -1
  40. package/dist/config/networks.d.ts.map +0 -1
  41. package/dist/config/networks.js.map +0 -1
  42. package/dist/config/pillar.d.ts.map +0 -1
  43. package/dist/config/pillar.js.map +0 -1
  44. package/dist/config/sponsor.d.ts.map +0 -1
  45. package/dist/config/sponsor.js.map +0 -1
  46. package/dist/endpoints/index.d.ts.map +0 -1
  47. package/dist/endpoints/index.js.map +0 -1
  48. package/dist/endpoints/registry.d.ts.map +0 -1
  49. package/dist/endpoints/registry.js.map +0 -1
  50. package/dist/index.d.ts.map +0 -1
  51. package/dist/index.js.map +0 -1
  52. package/dist/services/bitflow.service.d.ts.map +0 -1
  53. package/dist/services/bitflow.service.js.map +0 -1
  54. package/dist/services/bns.service.d.ts.map +0 -1
  55. package/dist/services/bns.service.js.map +0 -1
  56. package/dist/services/credentials.d.ts.map +0 -1
  57. package/dist/services/credentials.js.map +0 -1
  58. package/dist/services/defi.service.d.ts.map +0 -1
  59. package/dist/services/defi.service.js.map +0 -1
  60. package/dist/services/erc8004.service.d.ts.map +0 -1
  61. package/dist/services/erc8004.service.js.map +0 -1
  62. package/dist/services/hiro-api.d.ts.map +0 -1
  63. package/dist/services/hiro-api.js.map +0 -1
  64. package/dist/services/inscription-parser.d.ts.map +0 -1
  65. package/dist/services/inscription-parser.js.map +0 -1
  66. package/dist/services/lightning/provider.d.ts.map +0 -1
  67. package/dist/services/lightning/provider.js.map +0 -1
  68. package/dist/services/lightning/spark-provider.d.ts.map +0 -1
  69. package/dist/services/lightning/spark-provider.js.map +0 -1
  70. package/dist/services/lightning-manager.d.ts.map +0 -1
  71. package/dist/services/lightning-manager.js.map +0 -1
  72. package/dist/services/mempool-api.d.ts.map +0 -1
  73. package/dist/services/mempool-api.js.map +0 -1
  74. package/dist/services/nft.service.d.ts.map +0 -1
  75. package/dist/services/nft.service.js.map +0 -1
  76. package/dist/services/nonce-tracker.d.ts.map +0 -1
  77. package/dist/services/nonce-tracker.js.map +0 -1
  78. package/dist/services/pillar-api.service.d.ts.map +0 -1
  79. package/dist/services/pillar-api.service.js.map +0 -1
  80. package/dist/services/sbtc-deposit.service.d.ts.map +0 -1
  81. package/dist/services/sbtc-deposit.service.js.map +0 -1
  82. package/dist/services/sbtc.service.d.ts.map +0 -1
  83. package/dist/services/sbtc.service.js.map +0 -1
  84. package/dist/services/scaffold.service.d.ts.map +0 -1
  85. package/dist/services/scaffold.service.js.map +0 -1
  86. package/dist/services/signing-key.service.d.ts.map +0 -1
  87. package/dist/services/signing-key.service.js.map +0 -1
  88. package/dist/services/stacking.service.d.ts.map +0 -1
  89. package/dist/services/stacking.service.js.map +0 -1
  90. package/dist/services/tenero-api.d.ts.map +0 -1
  91. package/dist/services/tenero-api.js.map +0 -1
  92. package/dist/services/tokens.service.d.ts.map +0 -1
  93. package/dist/services/tokens.service.js.map +0 -1
  94. package/dist/services/unisat-indexer.d.ts.map +0 -1
  95. package/dist/services/unisat-indexer.js.map +0 -1
  96. package/dist/services/wallet-manager.d.ts.map +0 -1
  97. package/dist/services/wallet-manager.js.map +0 -1
  98. package/dist/services/x402.service.d.ts.map +0 -1
  99. package/dist/services/x402.service.js.map +0 -1
  100. package/dist/tools/arxiv-research.tools.d.ts.map +0 -1
  101. package/dist/tools/arxiv-research.tools.js.map +0 -1
  102. package/dist/tools/bitcoin.tools.d.ts.map +0 -1
  103. package/dist/tools/bitcoin.tools.js.map +0 -1
  104. package/dist/tools/bitflow.tools.d.ts.map +0 -1
  105. package/dist/tools/bitflow.tools.js.map +0 -1
  106. package/dist/tools/bns.tools.d.ts.map +0 -1
  107. package/dist/tools/bns.tools.js.map +0 -1
  108. package/dist/tools/bounty-scanner.tools.d.ts.map +0 -1
  109. package/dist/tools/bounty-scanner.tools.js.map +0 -1
  110. package/dist/tools/child-inscription.tools.d.ts.map +0 -1
  111. package/dist/tools/child-inscription.tools.js.map +0 -1
  112. package/dist/tools/competition.tools.d.ts.map +0 -1
  113. package/dist/tools/competition.tools.js.map +0 -1
  114. package/dist/tools/contract.tools.d.ts.map +0 -1
  115. package/dist/tools/contract.tools.js.map +0 -1
  116. package/dist/tools/credentials.tools.d.ts.map +0 -1
  117. package/dist/tools/credentials.tools.js.map +0 -1
  118. package/dist/tools/defi.tools.d.ts.map +0 -1
  119. package/dist/tools/defi.tools.js.map +0 -1
  120. package/dist/tools/dual-stacking.tools.d.ts.map +0 -1
  121. package/dist/tools/dual-stacking.tools.js.map +0 -1
  122. package/dist/tools/earning.tools.d.ts.map +0 -1
  123. package/dist/tools/earning.tools.js.map +0 -1
  124. package/dist/tools/endpoint.tools.d.ts.map +0 -1
  125. package/dist/tools/endpoint.tools.js.map +0 -1
  126. package/dist/tools/erc8004.tools.d.ts.map +0 -1
  127. package/dist/tools/erc8004.tools.js.map +0 -1
  128. package/dist/tools/identity.tools.d.ts.map +0 -1
  129. package/dist/tools/identity.tools.js.map +0 -1
  130. package/dist/tools/inbox-x402.tools.d.ts.map +0 -1
  131. package/dist/tools/inbox-x402.tools.js.map +0 -1
  132. package/dist/tools/inbox.tools.d.ts.map +0 -1
  133. package/dist/tools/inbox.tools.js.map +0 -1
  134. package/dist/tools/index.d.ts.map +0 -1
  135. package/dist/tools/index.js.map +0 -1
  136. package/dist/tools/jingswap.tools.d.ts.map +0 -1
  137. package/dist/tools/jingswap.tools.js.map +0 -1
  138. package/dist/tools/lightning.tools.d.ts.map +0 -1
  139. package/dist/tools/lightning.tools.js.map +0 -1
  140. package/dist/tools/mempool.tools.d.ts.map +0 -1
  141. package/dist/tools/mempool.tools.js.map +0 -1
  142. package/dist/tools/news.tools.d.ts.map +0 -1
  143. package/dist/tools/news.tools.js.map +0 -1
  144. package/dist/tools/nft.tools.d.ts.map +0 -1
  145. package/dist/tools/nft.tools.js.map +0 -1
  146. package/dist/tools/nonce.tools.d.ts.map +0 -1
  147. package/dist/tools/nonce.tools.js.map +0 -1
  148. package/dist/tools/nostr.tools.d.ts.map +0 -1
  149. package/dist/tools/nostr.tools.js.map +0 -1
  150. package/dist/tools/openrouter.tools.d.ts.map +0 -1
  151. package/dist/tools/openrouter.tools.js.map +0 -1
  152. package/dist/tools/ordinals-marketplace.tools.d.ts.map +0 -1
  153. package/dist/tools/ordinals-marketplace.tools.js.map +0 -1
  154. package/dist/tools/ordinals-p2p.tools.d.ts.map +0 -1
  155. package/dist/tools/ordinals-p2p.tools.js.map +0 -1
  156. package/dist/tools/ordinals.tools.d.ts.map +0 -1
  157. package/dist/tools/ordinals.tools.js.map +0 -1
  158. package/dist/tools/pillar-direct.tools.d.ts.map +0 -1
  159. package/dist/tools/pillar-direct.tools.js.map +0 -1
  160. package/dist/tools/pillar.tools.d.ts.map +0 -1
  161. package/dist/tools/pillar.tools.js.map +0 -1
  162. package/dist/tools/psbt.helpers.d.ts.map +0 -1
  163. package/dist/tools/psbt.helpers.js.map +0 -1
  164. package/dist/tools/psbt.tools.d.ts.map +0 -1
  165. package/dist/tools/psbt.tools.js.map +0 -1
  166. package/dist/tools/query.tools.d.ts.map +0 -1
  167. package/dist/tools/query.tools.js.map +0 -1
  168. package/dist/tools/relay-diagnostic.tools.d.ts.map +0 -1
  169. package/dist/tools/relay-diagnostic.tools.js.map +0 -1
  170. package/dist/tools/reputation.tools.d.ts.map +0 -1
  171. package/dist/tools/reputation.tools.js.map +0 -1
  172. package/dist/tools/runes.tools.d.ts.map +0 -1
  173. package/dist/tools/runes.tools.js.map +0 -1
  174. package/dist/tools/sbtc.tools.d.ts.map +0 -1
  175. package/dist/tools/sbtc.tools.js.map +0 -1
  176. package/dist/tools/scaffold.tools.d.ts.map +0 -1
  177. package/dist/tools/scaffold.tools.js.map +0 -1
  178. package/dist/tools/schemas.d.ts.map +0 -1
  179. package/dist/tools/schemas.js.map +0 -1
  180. package/dist/tools/settings.tools.d.ts.map +0 -1
  181. package/dist/tools/settings.tools.js.map +0 -1
  182. package/dist/tools/signing.tools.d.ts.map +0 -1
  183. package/dist/tools/signing.tools.js.map +0 -1
  184. package/dist/tools/skill-mappings.d.ts.map +0 -1
  185. package/dist/tools/skill-mappings.js.map +0 -1
  186. package/dist/tools/souldinals.tools.d.ts.map +0 -1
  187. package/dist/tools/souldinals.tools.js.map +0 -1
  188. package/dist/tools/stacking-lottery.tools.d.ts.map +0 -1
  189. package/dist/tools/stacking-lottery.tools.js.map +0 -1
  190. package/dist/tools/stacking.tools.d.ts.map +0 -1
  191. package/dist/tools/stacking.tools.js.map +0 -1
  192. package/dist/tools/stacks-market.tools.d.ts.map +0 -1
  193. package/dist/tools/stacks-market.tools.js.map +0 -1
  194. package/dist/tools/styx.tools.d.ts.map +0 -1
  195. package/dist/tools/styx.tools.js.map +0 -1
  196. package/dist/tools/taproot-multisig.tools.d.ts.map +0 -1
  197. package/dist/tools/taproot-multisig.tools.js.map +0 -1
  198. package/dist/tools/tenero.tools.d.ts.map +0 -1
  199. package/dist/tools/tenero.tools.js.map +0 -1
  200. package/dist/tools/tokens.tools.d.ts.map +0 -1
  201. package/dist/tools/tokens.tools.js.map +0 -1
  202. package/dist/tools/transfer.tools.d.ts.map +0 -1
  203. package/dist/tools/transfer.tools.js.map +0 -1
  204. package/dist/tools/wallet-management.tools.d.ts.map +0 -1
  205. package/dist/tools/wallet-management.tools.js.map +0 -1
  206. package/dist/tools/wallet.tools.d.ts.map +0 -1
  207. package/dist/tools/wallet.tools.js.map +0 -1
  208. package/dist/tools/yield-dashboard.tools.d.ts.map +0 -1
  209. package/dist/tools/yield-dashboard.tools.js.map +0 -1
  210. package/dist/tools/yield-hunter.tools.d.ts.map +0 -1
  211. package/dist/tools/yield-hunter.tools.js.map +0 -1
  212. package/dist/transactions/bitcoin-builder.d.ts.map +0 -1
  213. package/dist/transactions/bitcoin-builder.js.map +0 -1
  214. package/dist/transactions/builder.d.ts.map +0 -1
  215. package/dist/transactions/builder.js.map +0 -1
  216. package/dist/transactions/child-inscription-builder.d.ts.map +0 -1
  217. package/dist/transactions/child-inscription-builder.js.map +0 -1
  218. package/dist/transactions/clarity-values.d.ts.map +0 -1
  219. package/dist/transactions/clarity-values.js.map +0 -1
  220. package/dist/transactions/inscription-builder.d.ts.map +0 -1
  221. package/dist/transactions/inscription-builder.js.map +0 -1
  222. package/dist/transactions/post-conditions.d.ts.map +0 -1
  223. package/dist/transactions/post-conditions.js.map +0 -1
  224. package/dist/transactions/rune-transfer-builder.d.ts.map +0 -1
  225. package/dist/transactions/rune-transfer-builder.js.map +0 -1
  226. package/dist/transactions/runestone-builder.d.ts.map +0 -1
  227. package/dist/transactions/runestone-builder.js.map +0 -1
  228. package/dist/transactions/sponsor-builder.d.ts.map +0 -1
  229. package/dist/transactions/sponsor-builder.js.map +0 -1
  230. package/dist/utils/bip322.d.ts.map +0 -1
  231. package/dist/utils/bip322.js.map +0 -1
  232. package/dist/utils/bitcoin.d.ts.map +0 -1
  233. package/dist/utils/bitcoin.js.map +0 -1
  234. package/dist/utils/encryption.d.ts.map +0 -1
  235. package/dist/utils/encryption.js.map +0 -1
  236. package/dist/utils/erc8004-helpers.d.ts.map +0 -1
  237. package/dist/utils/erc8004-helpers.js.map +0 -1
  238. package/dist/utils/errors.d.ts.map +0 -1
  239. package/dist/utils/errors.js.map +0 -1
  240. package/dist/utils/fee.d.ts.map +0 -1
  241. package/dist/utils/fee.js.map +0 -1
  242. package/dist/utils/formatting.d.ts.map +0 -1
  243. package/dist/utils/formatting.js.map +0 -1
  244. package/dist/utils/index.d.ts.map +0 -1
  245. package/dist/utils/index.js.map +0 -1
  246. package/dist/utils/l402-protocol.d.ts.map +0 -1
  247. package/dist/utils/l402-protocol.js.map +0 -1
  248. package/dist/utils/redact.d.ts.map +0 -1
  249. package/dist/utils/redact.js.map +0 -1
  250. package/dist/utils/relay-health.d.ts.map +0 -1
  251. package/dist/utils/relay-health.js.map +0 -1
  252. package/dist/utils/storage.d.ts.map +0 -1
  253. package/dist/utils/storage.js.map +0 -1
  254. package/dist/utils/tokens.d.ts.map +0 -1
  255. package/dist/utils/tokens.js.map +0 -1
  256. package/dist/utils/x402-payment-logging.d.ts.map +0 -1
  257. package/dist/utils/x402-payment-logging.js.map +0 -1
  258. package/dist/utils/x402-payment-state.d.ts.map +0 -1
  259. package/dist/utils/x402-payment-state.js.map +0 -1
  260. package/dist/utils/x402-protocol.d.ts.map +0 -1
  261. package/dist/utils/x402-protocol.js.map +0 -1
  262. package/dist/utils/x402-recovery.d.ts.map +0 -1
  263. package/dist/utils/x402-recovery.js.map +0 -1
  264. package/dist/yield-hunter/index.d.ts.map +0 -1
  265. package/dist/yield-hunter/index.js.map +0 -1
package/README.md CHANGED
@@ -69,6 +69,38 @@ npx @aibtc/mcp-server@latest --install --vscode # VS Code (writes ./.vscode/
69
69
 
70
70
  Each installer merges into the existing config — it won't clobber other servers or settings. Restart the client afterward.
71
71
 
72
+ ### OpenRouter (any model)
73
+
74
+ The clients above are MCP hosts — they connect to this server for you. To drive the tools with an [OpenRouter](https://openrouter.ai) model instead, the server ships a built-in bridge: it spawns itself in server mode, exposes the tools to the model as function tools, and runs the tool-call loop. This is the client-side pattern from [OpenRouter's MCP cookbook](https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers), packaged into the binary.
75
+
76
+ ```bash
77
+ export OPENROUTER_API_KEY=sk-or-...
78
+ npx @aibtc/mcp-server@latest bridge "what's the STX balance of SP3...?"
79
+ ```
80
+
81
+ Safety flags (this server moves real funds, so the bridge defaults to nothing extra and lets you constrain it):
82
+
83
+ | Flag | Effect |
84
+ |------|--------|
85
+ | `--read-only` | Expose only read-only tools (no transfer/swap/deploy/etc.) |
86
+ | `--allow a,b` | Force-allow specific tool names (added to the set) |
87
+ | `--block a,b` | Force-remove specific tool names (wins over `--allow`) |
88
+ | `--max-spend-ustx <n>` / `--max-spend-sats <n>` | Cap spend via the server's spend-limit rail (enforced before signing) |
89
+ | `--list-tools` | Print the exposed tool set and exit (no API key needed) |
90
+ | `--model <id>` | OpenRouter model (default `anthropic/claude-3.5-haiku`) |
91
+ | `--network <net>` | `mainnet` or `testnet` (default `mainnet`) |
92
+ | `--max-turns <n>` | Tool-call loop cap (default 10) |
93
+
94
+ ```bash
95
+ # Preview what a read-only session would expose, no key needed:
96
+ npx @aibtc/mcp-server@latest bridge --read-only --list-tools
97
+
98
+ # Read-only chat, plus one explicitly allowed write tool:
99
+ npx @aibtc/mcp-server@latest bridge --read-only --allow transfer_stx "send 1 STX to SP3..."
100
+ ```
101
+
102
+ The allowlist is re-enforced at execution time, so a model can never call a tool outside the exposed set. Any MCP-capable agent framework (`@openrouter/agent`, OpenAI Agents SDK, Claude Agent SDK) can also point at this server directly — the bridge is for driving it through OpenRouter's raw API without adopting a framework.
103
+
72
104
  ### Testnet Mode
73
105
 
74
106
  Add `--testnet` to any install command:
@@ -512,9 +544,14 @@ Or use any SIP-010 token by contract ID: `SP2X...::token-name`
512
544
  | `API_URL` | Default x402 API base URL | `https://x402.biwas.xyz` |
513
545
  | `CLIENT_MNEMONIC` | (Optional) Pre-configured mnemonic | - |
514
546
  | `HIRO_API_KEY` | (Optional) Hiro API key for higher rate limits | - |
547
+ | `SPEND_LIMIT_ENABLED` | Set `false` to disable the wallet spending limit | `true` |
548
+ | `SPEND_LIMIT_DAILY_USTX` / `SPEND_LIMIT_SESSION_USTX` | STX spend cap per day / per unlock (micro-STX) | `10000000` (10 STX) |
549
+ | `SPEND_LIMIT_DAILY_SATS` / `SPEND_LIMIT_SESSION_SATS` | BTC spend cap per day / per unlock (sats) | `50000` |
515
550
 
516
551
  **Note on `NETWORK`:** The `--install` command writes `NETWORK=mainnet` by default (pass `--testnet` to use testnet). If you omit `NETWORK` from your config entirely, the runtime fallback is `testnet`. Most users should set this explicitly.
517
552
 
553
+ **Note on spending limits:** A default-on safety rail meters every outbound spend (`transfer_stx`, `transfer_btc`, x402/L402 auto-payments) against a cumulative per-session and per-day cap, so a single bad instruction or a malicious endpoint can't drain the wallet. A spend over the cap is blocked and reports the remaining budget. Raise the caps via the env vars above, or disable with `SPEND_LIMIT_ENABLED=false`. See [SECURITY.md](SECURITY.md#limit-blast-radius).
554
+
518
555
  **Note:** `CLIENT_MNEMONIC` is optional. The recommended approach is to let Claude create its own wallet. `HIRO_API_KEY` is optional but recommended for production use — without it, you may hit Hiro's public rate limits (429 responses). Get a key at [platform.hiro.so](https://platform.hiro.so).
519
556
 
520
557
  ## Architecture
@@ -538,8 +575,12 @@ You ←→ Claude ←→ aibtc-mcp-server
538
575
  - Mnemonics shown only once at creation
539
576
  - Auto-lock after 15 minutes (configurable)
540
577
  - Transactions signed locally before broadcast
578
+ - **Spending limit (default-on):** outbound spends are capped per session and per day so a single bad instruction can't drain the wallet — see [Configuration](#configuration)
579
+ - **Secret-scanning pre-commit hook:** contributors get a hook (auto-installed via `npm install`) that blocks committing a seed phrase or private key
541
580
  - For mainnet: Fund with small amounts first
542
581
 
582
+ See [SECURITY.md](SECURITY.md) for the full wallet-key protection model and key-leak recovery steps.
583
+
543
584
  ## Advanced: Pre-configured Mnemonic
544
585
 
545
586
  For automated setups where Claude needs immediate wallet access, add the `CLIENT_MNEMONIC` environment variable to your MCP server config (in `~/.claude.json` for Claude Code, or `claude_desktop_config.json` for Claude Desktop):
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OpenRouter <-> MCP bridge (`aibtc-mcp-server bridge`).
3
+ *
4
+ * Lets any OpenRouter model call this server's tools. This is the canonical
5
+ * integration described in OpenRouter's own cookbook
6
+ * (https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers): the CLIENT
7
+ * connects to the MCP server and routes LLM calls through OpenRouter — there is
8
+ * no OpenRouter-side MCP hosting. This is a Node port of that pattern with two
9
+ * upgrades: it handles every tool_call (not just the first) and loops to
10
+ * completion (not a single round).
11
+ *
12
+ * Flow:
13
+ * 1. Spawn this same binary in server mode over stdio, connect, list tools.
14
+ * 2. Apply safety filters, then convert each MCP tool -> OpenAI function tool.
15
+ * 3. POST conversation + tools to OpenRouter /chat/completions.
16
+ * 4. On tool_calls, execute via MCP and feed results back as role:"tool".
17
+ * 5. Loop until the model answers in plain text (capped by --max-turns).
18
+ *
19
+ * Safety flags:
20
+ * --read-only expose only read-only tools (heuristic allowlist)
21
+ * --allow a,b,c force-allow these tool names (added to the set)
22
+ * --block a,b,c force-remove these tool names (applied last, wins)
23
+ * --max-spend-ustx <n> forward to the server's SPEND_LIMIT_SESSION_USTX rail
24
+ * --max-spend-sats <n> forward to the server's SPEND_LIMIT_SESSION_SATS rail
25
+ * --list-tools print the exposed tool set and exit (no API key needed)
26
+ * --model <id> OpenRouter model (default anthropic/claude-3.5-haiku)
27
+ * --network <net> mainnet|testnet for the spawned server (default mainnet)
28
+ * --max-turns <n> tool-call loop cap (default 10)
29
+ */
30
+ export declare function runBridge(argv: string[]): Promise<void>;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,285 @@
1
+ /**
2
+ * OpenRouter <-> MCP bridge (`aibtc-mcp-server bridge`).
3
+ *
4
+ * Lets any OpenRouter model call this server's tools. This is the canonical
5
+ * integration described in OpenRouter's own cookbook
6
+ * (https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers): the CLIENT
7
+ * connects to the MCP server and routes LLM calls through OpenRouter — there is
8
+ * no OpenRouter-side MCP hosting. This is a Node port of that pattern with two
9
+ * upgrades: it handles every tool_call (not just the first) and loops to
10
+ * completion (not a single round).
11
+ *
12
+ * Flow:
13
+ * 1. Spawn this same binary in server mode over stdio, connect, list tools.
14
+ * 2. Apply safety filters, then convert each MCP tool -> OpenAI function tool.
15
+ * 3. POST conversation + tools to OpenRouter /chat/completions.
16
+ * 4. On tool_calls, execute via MCP and feed results back as role:"tool".
17
+ * 5. Loop until the model answers in plain text (capped by --max-turns).
18
+ *
19
+ * Safety flags:
20
+ * --read-only expose only read-only tools (heuristic allowlist)
21
+ * --allow a,b,c force-allow these tool names (added to the set)
22
+ * --block a,b,c force-remove these tool names (applied last, wins)
23
+ * --max-spend-ustx <n> forward to the server's SPEND_LIMIT_SESSION_USTX rail
24
+ * --max-spend-sats <n> forward to the server's SPEND_LIMIT_SESSION_SATS rail
25
+ * --list-tools print the exposed tool set and exit (no API key needed)
26
+ * --model <id> OpenRouter model (default anthropic/claude-3.5-haiku)
27
+ * --network <net> mainnet|testnet for the spawned server (default mainnet)
28
+ * --max-turns <n> tool-call loop cap (default 10)
29
+ */
30
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
31
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
32
+ const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
33
+ const DEFAULT_MODEL = "anthropic/claude-3.5-haiku";
34
+ /**
35
+ * Read-only heuristic. A tool counts as read-only only if it matches a known
36
+ * read pattern AND contains no mutating verb — deliberately conservative so a
37
+ * spend/write tool is never auto-included. Use --allow/--block for precision.
38
+ */
39
+ const READ_PATTERNS = [
40
+ /^get_/, /^list_/, /^check_/, /^lookup_/, /^reverse_/, /^probe_/, /^read_/,
41
+ /_status$/, /_info$/, /_quote$/, /_quote_/, /_models$/, /_guide$/, /_list$/,
42
+ /_balance$/, /_fees$/, /_utxos$/, /_history$/, /_holdings$/, /_overview$/,
43
+ /_summary$/, /_health$/, /_price$/, /_prices$/, /_state$/, /_positions$/,
44
+ /_breakdown$/, /_dashboard_/, /_stats$/, /_metadata$/, /_owner$/, /_holders$/,
45
+ /_search$/, /_feed$/, /_profile$/, /_pubkey$/, /_availability$/, /_count$/,
46
+ ];
47
+ const WRITE_VERB = /(transfer|send|swap|deploy|broadcast|buy|sell|pay|supply|withdraw|borrow|repay|create|delete|register|claim|deposit|stack|boost|unwind|invite|set_|sign|submit|enroll|opt_|file_|preorder|mint|inscribe|approve|revoke|connect|disconnect|rotate|export|import|unlock|lock|start|stop|join|settle|redeem|fund|accept|cancel|counter|append|give|add_|deactivate|record|compile|configure|heal|fill_gap|recover|rebalance|hunter|enable)/;
48
+ function isReadOnly(name) {
49
+ if (WRITE_VERB.test(name))
50
+ return false;
51
+ return READ_PATTERNS.some((p) => p.test(name));
52
+ }
53
+ /** MCP tool -> OpenRouter (OpenAI) function-tool schema (verbatim JSON Schema). */
54
+ function toOpenAITool(tool) {
55
+ // Clone so we never mutate the tool descriptor handed back by the MCP SDK.
56
+ const raw = tool.inputSchema ?? {};
57
+ const schema = { type: "object", properties: {}, ...raw };
58
+ return {
59
+ type: "function",
60
+ function: {
61
+ name: tool.name,
62
+ description: tool.description ?? "",
63
+ parameters: schema,
64
+ },
65
+ };
66
+ }
67
+ function parseArgs(argv) {
68
+ const opts = {
69
+ prompt: "",
70
+ model: process.env.OPENROUTER_MODEL || DEFAULT_MODEL,
71
+ network: process.env.NETWORK || "mainnet",
72
+ readOnly: false,
73
+ allow: new Set(),
74
+ block: new Set(),
75
+ maxTurns: 10,
76
+ listTools: false,
77
+ };
78
+ const promptWords = [];
79
+ const csv = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
80
+ for (let i = 0; i < argv.length; i++) {
81
+ const a = argv[i];
82
+ // Value-taking flags must be followed by a value — otherwise the next loop
83
+ // iteration would silently consume the following flag (or leave the option
84
+ // unset, which for a spend cap means the rail is quietly disabled).
85
+ const value = (flag) => {
86
+ const v = argv[++i];
87
+ if (v === undefined || v.startsWith("--")) {
88
+ console.error(`Missing value for ${flag}`);
89
+ process.exit(1);
90
+ }
91
+ return v;
92
+ };
93
+ switch (a) {
94
+ case "--read-only":
95
+ opts.readOnly = true;
96
+ break;
97
+ case "--list-tools":
98
+ opts.listTools = true;
99
+ break;
100
+ case "--model":
101
+ opts.model = value(a);
102
+ break;
103
+ case "--network":
104
+ opts.network = value(a);
105
+ break;
106
+ case "--allow":
107
+ csv(value(a)).forEach((t) => opts.allow.add(t));
108
+ break;
109
+ case "--block":
110
+ csv(value(a)).forEach((t) => opts.block.add(t));
111
+ break;
112
+ case "--max-spend-ustx":
113
+ opts.maxSpendUstx = value(a);
114
+ break;
115
+ case "--max-spend-sats":
116
+ opts.maxSpendSats = value(a);
117
+ break;
118
+ case "--max-turns": {
119
+ const raw = value(a);
120
+ const n = parseInt(raw, 10);
121
+ if (!Number.isInteger(n) || n < 1) {
122
+ console.error(`--max-turns must be a positive integer (got "${raw}")`);
123
+ process.exit(1);
124
+ }
125
+ opts.maxTurns = n;
126
+ break;
127
+ }
128
+ default:
129
+ if (a.startsWith("--")) {
130
+ console.error(`Unknown flag: ${a}`);
131
+ process.exit(1);
132
+ }
133
+ promptWords.push(a);
134
+ }
135
+ }
136
+ opts.prompt = promptWords.join(" ").trim();
137
+ return opts;
138
+ }
139
+ /** Apply read-only + allow + block to the raw MCP tool list. */
140
+ function selectTools(tools, opts) {
141
+ return tools.filter((t) => {
142
+ if (opts.block.has(t.name))
143
+ return false;
144
+ if (opts.allow.has(t.name))
145
+ return true;
146
+ if (opts.readOnly)
147
+ return isReadOnly(t.name);
148
+ return true;
149
+ });
150
+ }
151
+ async function callOpenRouter(apiKey, model, messages, tools) {
152
+ const res = await fetch(OPENROUTER_URL, {
153
+ method: "POST",
154
+ headers: {
155
+ Authorization: `Bearer ${apiKey}`,
156
+ "Content-Type": "application/json",
157
+ "HTTP-Referer": "https://github.com/aibtcdev/aibtc-mcp",
158
+ "X-Title": "aibtc-mcp bridge",
159
+ },
160
+ body: JSON.stringify({ model, messages, tools, tool_choice: "auto" }),
161
+ });
162
+ if (!res.ok) {
163
+ throw new Error(`OpenRouter ${res.status}: ${await res.text()}`);
164
+ }
165
+ // OpenRouter can return HTTP 200 with an error payload (rate limit, moderation,
166
+ // provider failure) and no choices — surface it instead of crashing on [0].
167
+ const data = (await res.json());
168
+ const message = data.choices?.[0]?.message;
169
+ if (!message) {
170
+ const detail = data.error?.message ?? JSON.stringify(data);
171
+ throw new Error(`OpenRouter returned no message: ${detail}`);
172
+ }
173
+ return message;
174
+ }
175
+ export async function runBridge(argv) {
176
+ const opts = parseArgs(argv);
177
+ // Spawn this same binary in plain server mode (no extra args -> MCP server).
178
+ const serverEnv = {
179
+ ...process.env,
180
+ NETWORK: opts.network,
181
+ };
182
+ if (opts.maxSpendUstx)
183
+ serverEnv.SPEND_LIMIT_SESSION_USTX = opts.maxSpendUstx;
184
+ if (opts.maxSpendSats)
185
+ serverEnv.SPEND_LIMIT_SESSION_SATS = opts.maxSpendSats;
186
+ const transport = new StdioClientTransport({
187
+ command: process.execPath,
188
+ args: [process.argv[1]],
189
+ env: serverEnv,
190
+ });
191
+ const client = new Client({ name: "aibtc-openrouter-bridge", version: "1.0.0" });
192
+ await client.connect(transport);
193
+ const { tools: allTools } = await client.listTools();
194
+ const exposed = selectTools(allTools, opts);
195
+ if (opts.listTools) {
196
+ console.log(`Exposed ${exposed.length}/${allTools.length} tools` +
197
+ (opts.readOnly ? " (read-only)" : "") + ":");
198
+ for (const t of exposed)
199
+ console.log(` ${t.name}`);
200
+ await client.close();
201
+ return;
202
+ }
203
+ const apiKey = process.env.OPENROUTER_API_KEY;
204
+ if (!apiKey) {
205
+ console.error("Set OPENROUTER_API_KEY (or use --list-tools to preview without a model).");
206
+ await client.close();
207
+ process.exit(1);
208
+ }
209
+ if (!opts.prompt) {
210
+ console.error('Provide a prompt, e.g. aibtc-mcp-server bridge "what is the STX balance of SP3..."');
211
+ await client.close();
212
+ process.exit(1);
213
+ }
214
+ const writeCount = exposed.filter((t) => !isReadOnly(t.name)).length;
215
+ console.error(`Bridge: ${exposed.length} tools -> ${opts.model}` +
216
+ (opts.readOnly ? " [read-only]" : ` [${writeCount} can write/spend]`) +
217
+ (opts.maxSpendUstx || opts.maxSpendSats
218
+ ? ` (cap ${opts.maxSpendUstx ?? "-"} uSTX / ${opts.maxSpendSats ?? "-"} sats)`
219
+ : ""));
220
+ const openaiTools = exposed.map(toOpenAITool);
221
+ const messages = [
222
+ {
223
+ role: "system",
224
+ content: "You are an agent with access to aibtc-mcp tools for Stacks/Bitcoin and " +
225
+ "x402 actions. Use tools when needed. Be concise.",
226
+ },
227
+ { role: "user", content: opts.prompt },
228
+ ];
229
+ try {
230
+ let completed = false;
231
+ for (let turn = 0; turn < opts.maxTurns; turn++) {
232
+ const reply = await callOpenRouter(apiKey, opts.model, messages, openaiTools);
233
+ messages.push(reply);
234
+ const toolCalls = reply.tool_calls ?? [];
235
+ if (toolCalls.length === 0) {
236
+ console.log("\n" + (reply.content ?? ""));
237
+ completed = true;
238
+ break;
239
+ }
240
+ for (const call of toolCalls) {
241
+ const name = call.function.name;
242
+ // Enforce the allowlist at execution time too — never trust the model
243
+ // to stay inside the exposed set.
244
+ if (!exposed.some((t) => t.name === name)) {
245
+ messages.push({
246
+ role: "tool",
247
+ tool_call_id: call.id,
248
+ name,
249
+ content: `Tool ${name} is not permitted by the bridge.`,
250
+ });
251
+ continue;
252
+ }
253
+ let args = {};
254
+ try {
255
+ args = JSON.parse(call.function.arguments || "{}");
256
+ }
257
+ catch {
258
+ /* malformed JSON -> empty args */
259
+ }
260
+ console.error(`-> ${name}(${call.function.arguments || "{}"})`);
261
+ let content;
262
+ try {
263
+ const result = await client.callTool({ name, arguments: args });
264
+ content = (result.content ?? [])
265
+ .map((c) => (c?.type === "text" ? c.text : JSON.stringify(c)))
266
+ .join("\n");
267
+ }
268
+ catch (err) {
269
+ content = `Tool error: ${err.message}`;
270
+ }
271
+ messages.push({ role: "tool", tool_call_id: call.id, name, content });
272
+ }
273
+ }
274
+ // Don't let the cap swallow the session silently — the model was still
275
+ // calling tools when we ran out of turns.
276
+ if (!completed) {
277
+ console.error(`Bridge: reached --max-turns limit (${opts.maxTurns}) without a final response. ` +
278
+ "Re-run with a higher --max-turns to let it finish.");
279
+ }
280
+ }
281
+ finally {
282
+ await client.close();
283
+ }
284
+ }
285
+ //# sourceMappingURL=index.js.map
package/dist/index.js CHANGED
@@ -36,13 +36,24 @@ function getClaudeDesktopConfigPath() {
36
36
  }
37
37
  }
38
38
  async function readJsonConfig(filePath) {
39
+ let content;
40
+ try {
41
+ content = await fs.readFile(filePath, "utf8");
42
+ }
43
+ catch (error) {
44
+ if (error.code === "ENOENT") {
45
+ return {}; // No config yet - fresh install
46
+ }
47
+ throw error;
48
+ }
39
49
  try {
40
- const content = await fs.readFile(filePath, "utf8");
41
50
  return JSON.parse(content);
42
51
  }
43
52
  catch {
44
- // File doesn't exist or isn't valid JSON, start fresh
45
- return {};
53
+ // Never silently replace an unparseable config: it may hold the user's
54
+ // other MCP servers, and "merging" into {} would discard them on write.
55
+ throw new Error(`Existing config at ${filePath} is not valid JSON. ` +
56
+ `Fix or remove it, then re-run the install.`);
46
57
  }
47
58
  }
48
59
  async function writeJsonConfig(filePath, config) {
@@ -99,8 +110,10 @@ async function writeCodexToml(configPath, network) {
99
110
  try {
100
111
  existing = await fs.readFile(configPath, "utf8");
101
112
  }
102
- catch {
103
- existing = "";
113
+ catch (error) {
114
+ if (error.code !== "ENOENT") {
115
+ throw error; // Unreadable existing config must not be clobbered
116
+ }
104
117
  }
105
118
  const preserved = stripCodexSection(existing).replace(/\s+$/, "");
106
119
  const block = [
@@ -216,6 +229,16 @@ if (process.argv[2] === "yield-hunter") {
216
229
  process.exit(1);
217
230
  });
218
231
  }
232
+ // Check for bridge command (OpenRouter <-> MCP)
233
+ else if (process.argv[2] === "bridge") {
234
+ import("./bridge/index.js")
235
+ .then(({ runBridge }) => runBridge(process.argv.slice(3)))
236
+ .then(() => process.exit(0))
237
+ .catch((error) => {
238
+ console.error("❌ Bridge error:", redactSensitive(String(error)));
239
+ process.exit(1);
240
+ });
241
+ }
219
242
  // Check for --install flag
220
243
  else if (process.argv.includes("--install") || process.argv.includes("install")) {
221
244
  runInstall()
@@ -39,6 +39,7 @@ async function encrypt(data, password) {
39
39
  const iv = crypto.randomBytes(12);
40
40
  const key = await deriveKey(password, salt);
41
41
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
42
+ key.fill(0); // cipher holds its own copy
42
43
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
43
44
  const authTag = cipher.getAuthTag();
44
45
  return {
@@ -53,6 +54,7 @@ async function encrypt(data, password) {
53
54
  async function decrypt(encrypted, password) {
54
55
  const key = await deriveKey(password, Buffer.from(encrypted.salt, "base64"));
55
56
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(encrypted.iv, "base64"));
57
+ key.fill(0); // decipher holds its own copy
56
58
  decipher.setAuthTag(Buffer.from(encrypted.authTag, "base64"));
57
59
  const decrypted = Buffer.concat([
58
60
  decipher.update(Buffer.from(encrypted.ciphertext, "base64")),
@@ -67,12 +69,15 @@ function emptyStore() {
67
69
  async function save() {
68
70
  if (!_store || !_password)
69
71
  throw new Error("Store not unlocked");
70
- await fs.mkdir(getStoreDir(), { recursive: true });
72
+ await fs.mkdir(getStoreDir(), { recursive: true, mode: 0o700 });
71
73
  const file = {
72
74
  version: VERSION,
73
75
  encrypted: await encrypt(JSON.stringify(_store), _password),
74
76
  };
75
- await fs.writeFile(getStoreFile(), JSON.stringify(file, null, 2));
77
+ // Owner-only perms + atomic rename, same pattern as the wallet keystores
78
+ const tempFile = getStoreFile() + ".tmp";
79
+ await fs.writeFile(tempFile, JSON.stringify(file, null, 2), { mode: 0o600 });
80
+ await fs.rename(tempFile, getStoreFile());
76
81
  }
77
82
  async function load(password) {
78
83
  const raw = await fs.readFile(getStoreFile(), "utf-8");
@@ -1,6 +1,6 @@
1
1
  import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, noneCV, someCV, listCV, bufferCV, } from "@stacks/transactions";
2
2
  import { AlexSDK, Currency } from "alex-sdk";
3
- import { getHiroApi } from "./hiro-api.js";
3
+ import { getHiroApi, isNotFoundError } from "./hiro-api.js";
4
4
  import { getAlexContracts, getZestContracts, parseContractId, ZEST_ASSETS, ZEST_V2_MARKET, ZEST_V2_MARKET_VAULT, } from "../config/index.js";
5
5
  import { callContract } from "../transactions/builder.js";
6
6
  // ============================================================================
@@ -129,8 +129,10 @@ export class AlexDexService {
129
129
  }
130
130
  return null;
131
131
  }
132
- catch {
133
- return null;
132
+ catch (error) {
133
+ if (isNotFoundError(error))
134
+ return null;
135
+ throw error;
134
136
  }
135
137
  }
136
138
  /**
@@ -360,8 +362,10 @@ export class ZestProtocolService {
360
362
  healthFactor: position["health-factor"]?.value,
361
363
  };
362
364
  }
363
- catch {
364
- return null;
365
+ catch (error) {
366
+ if (isNotFoundError(error))
367
+ return null;
368
+ throw error;
365
369
  }
366
370
  }
367
371
  /**
@@ -377,8 +381,10 @@ export class ZestProtocolService {
377
381
  }
378
382
  return cvToJSON(hexToCV(result.result));
379
383
  }
380
- catch {
381
- return null;
384
+ catch (error) {
385
+ if (isNotFoundError(error))
386
+ return null;
387
+ throw error;
382
388
  }
383
389
  }
384
390
  /**
@@ -411,8 +417,12 @@ export class ZestProtocolService {
411
417
  }
412
418
  }
413
419
  }
414
- catch {
415
- // Fall back to shares as lower bound estimate
420
+ catch (error) {
421
+ // Only fall back to the shares lower-bound estimate when the vault
422
+ // function is missing — network/server failures must surface, or the
423
+ // post-conditions built from this value would be silently wrong.
424
+ if (!isNotFoundError(error))
425
+ throw error;
416
426
  }
417
427
  return shares;
418
428
  }
@@ -431,8 +441,10 @@ export class ZestProtocolService {
431
441
  }
432
442
  }
433
443
  }
434
- catch {
435
- // Fall back to amount as upper bound
444
+ catch (error) {
445
+ // Same rationale as getUnderlyingFromShares: estimate only on 404.
446
+ if (!isNotFoundError(error))
447
+ throw error;
436
448
  }
437
449
  return amount;
438
450
  }
@@ -7,6 +7,16 @@ export declare class HiroApiRateLimitError extends Error {
7
7
  readonly retryAfterSeconds: number;
8
8
  constructor(message: string, retryAfterSeconds: number);
9
9
  }
10
+ /**
11
+ * HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
12
+ * callers can treat 404 as "not found" without swallowing other failures.
13
+ */
14
+ export declare class HiroApiError extends Error {
15
+ readonly status: number;
16
+ constructor(message: string, status: number);
17
+ }
18
+ /** True when the error is an expected "resource does not exist" response. */
19
+ export declare function isNotFoundError(error: unknown): boolean;
10
20
  export interface AccountInfo {
11
21
  balance: string;
12
22
  locked: string;
@@ -16,6 +16,22 @@ export class HiroApiRateLimitError extends Error {
16
16
  this.name = "HiroApiRateLimitError";
17
17
  }
18
18
  }
19
+ /**
20
+ * HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
21
+ * callers can treat 404 as "not found" without swallowing other failures.
22
+ */
23
+ export class HiroApiError extends Error {
24
+ status;
25
+ constructor(message, status) {
26
+ super(message);
27
+ this.status = status;
28
+ this.name = "HiroApiError";
29
+ }
30
+ }
31
+ /** True when the error is an expected "resource does not exist" response. */
32
+ export function isNotFoundError(error) {
33
+ return error instanceof HiroApiError && error.status === 404;
34
+ }
19
35
  // ============================================================================
20
36
  // Hiro API Service
21
37
  // ============================================================================
@@ -51,7 +67,7 @@ export class HiroApiService {
51
67
  throw new HiroApiRateLimitError(`Hiro API rate limit exceeded. Retry after ${retryAfterSeconds}s`, retryAfterSeconds);
52
68
  }
53
69
  const errorText = await response.text();
54
- throw new Error(`Hiro API error (${response.status}): ${errorText}`);
70
+ throw new HiroApiError(`Hiro API error (${response.status}): ${errorText}`, response.status);
55
71
  }
56
72
  return response.json();
57
73
  }
@@ -123,8 +139,10 @@ export class HiroApiService {
123
139
  try {
124
140
  return await this.fetch(`/metadata/v1/ft/${contractId}`);
125
141
  }
126
- catch {
127
- return null;
142
+ catch (error) {
143
+ if (isNotFoundError(error))
144
+ return null;
145
+ throw error;
128
146
  }
129
147
  }
130
148
  async getTokenHolders(contractId, options) {
@@ -329,8 +347,10 @@ export class HiroApiService {
329
347
  const info = await this.getBnsNameInfo(name);
330
348
  return info.address;
331
349
  }
332
- catch {
333
- return null;
350
+ catch (error) {
351
+ if (isNotFoundError(error))
352
+ return null;
353
+ throw error;
334
354
  }
335
355
  }
336
356
  // ==========================================================================
@@ -385,7 +405,7 @@ export class BnsV2ApiService {
385
405
  });
386
406
  if (!response.ok) {
387
407
  const errorText = await response.text();
388
- throw new Error(`BNS V2 API error (${response.status}): ${errorText}`);
408
+ throw new HiroApiError(`BNS V2 API error (${response.status}): ${errorText}`, response.status);
389
409
  }
390
410
  return response.json();
391
411
  }
@@ -431,8 +451,10 @@ export class BnsV2ApiService {
431
451
  }
432
452
  return null;
433
453
  }
434
- catch {
435
- return null;
454
+ catch (error) {
455
+ if (isNotFoundError(error))
456
+ return null;
457
+ throw error;
436
458
  }
437
459
  }
438
460
  }