@oobe-protocol-labs/sap-mcp-server 0.9.38 → 0.9.51

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 (178) hide show
  1. package/dist/config/env.d.ts +19 -0
  2. package/dist/config/env.d.ts.map +1 -1
  3. package/dist/config/env.js +9 -2
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/config/mcp-client-injection.d.ts +1 -1
  6. package/dist/config/mcp-client-injection.d.ts.map +1 -1
  7. package/dist/config/mcp-client-injection.js +35 -0
  8. package/dist/config/mcp-client-injection.js.map +1 -1
  9. package/dist/config/wizard.js +2 -2
  10. package/dist/config/wizard.js.map +1 -1
  11. package/dist/core/constants.d.ts +1 -1
  12. package/dist/core/constants.js +1 -1
  13. package/dist/core/logger.js +1 -1
  14. package/dist/payments/monetization-gate.d.ts +5 -0
  15. package/dist/payments/monetization-gate.d.ts.map +1 -1
  16. package/dist/payments/monetization-gate.js +39 -2
  17. package/dist/payments/monetization-gate.js.map +1 -1
  18. package/dist/payments/prepaid-credit-store.d.ts +111 -0
  19. package/dist/payments/prepaid-credit-store.d.ts.map +1 -0
  20. package/dist/payments/prepaid-credit-store.js +230 -0
  21. package/dist/payments/prepaid-credit-store.js.map +1 -0
  22. package/dist/payments/pricing.d.ts.map +1 -1
  23. package/dist/payments/pricing.js +1 -0
  24. package/dist/payments/pricing.js.map +1 -1
  25. package/dist/payments/schema-validation.d.ts +52 -0
  26. package/dist/payments/schema-validation.d.ts.map +1 -0
  27. package/dist/payments/schema-validation.js +112 -0
  28. package/dist/payments/schema-validation.js.map +1 -0
  29. package/dist/payments/x402-paid-call.d.ts +21 -0
  30. package/dist/payments/x402-paid-call.d.ts.map +1 -1
  31. package/dist/payments/x402-paid-call.js +108 -3
  32. package/dist/payments/x402-paid-call.js.map +1 -1
  33. package/dist/perps/adrena/adrena-builder-commodity.d.ts +63 -0
  34. package/dist/perps/adrena/adrena-builder-commodity.d.ts.map +1 -0
  35. package/dist/perps/adrena/adrena-builder-commodity.js +200 -0
  36. package/dist/perps/adrena/adrena-builder-commodity.js.map +1 -0
  37. package/dist/perps/adrena/adrena-builder-core.d.ts +319 -0
  38. package/dist/perps/adrena/adrena-builder-core.d.ts.map +1 -0
  39. package/dist/perps/adrena/adrena-builder-core.js +516 -0
  40. package/dist/perps/adrena/adrena-builder-core.js.map +1 -0
  41. package/dist/perps/adrena/adrena-builder-liquidity.d.ts +50 -0
  42. package/dist/perps/adrena/adrena-builder-liquidity.d.ts.map +1 -0
  43. package/dist/perps/adrena/adrena-builder-liquidity.js +196 -0
  44. package/dist/perps/adrena/adrena-builder-liquidity.js.map +1 -0
  45. package/dist/perps/adrena/adrena-builder-staking.d.ts +57 -0
  46. package/dist/perps/adrena/adrena-builder-staking.d.ts.map +1 -0
  47. package/dist/perps/adrena/adrena-builder-staking.js +368 -0
  48. package/dist/perps/adrena/adrena-builder-staking.js.map +1 -0
  49. package/dist/perps/adrena/adrena-builder-trading.d.ts +160 -0
  50. package/dist/perps/adrena/adrena-builder-trading.d.ts.map +1 -0
  51. package/dist/perps/adrena/adrena-builder-trading.js +835 -0
  52. package/dist/perps/adrena/adrena-builder-trading.js.map +1 -0
  53. package/dist/perps/adrena/adrena-builder.d.ts +14 -293
  54. package/dist/perps/adrena/adrena-builder.d.ts.map +1 -1
  55. package/dist/perps/adrena/adrena-builder.js +14 -1140
  56. package/dist/perps/adrena/adrena-builder.js.map +1 -1
  57. package/dist/perps/adrena/adrena-constants.d.ts +33 -5
  58. package/dist/perps/adrena/adrena-constants.d.ts.map +1 -1
  59. package/dist/perps/adrena/adrena-constants.js +29 -7
  60. package/dist/perps/adrena/adrena-constants.js.map +1 -1
  61. package/dist/perps/adrena/adrena-idl.d.ts +15084 -0
  62. package/dist/perps/adrena/adrena-idl.d.ts.map +1 -0
  63. package/dist/perps/adrena/adrena-idl.js +29902 -0
  64. package/dist/perps/adrena/adrena-idl.js.map +1 -0
  65. package/dist/perps/adrena/adrena-pda.d.ts +5 -4
  66. package/dist/perps/adrena/adrena-pda.d.ts.map +1 -1
  67. package/dist/perps/adrena/adrena-pda.js +6 -10
  68. package/dist/perps/adrena/adrena-pda.js.map +1 -1
  69. package/dist/perps/adrena/index.d.ts +1 -0
  70. package/dist/perps/adrena/index.d.ts.map +1 -1
  71. package/dist/perps/adrena/index.js +1 -0
  72. package/dist/perps/adrena/index.js.map +1 -1
  73. package/dist/perps/chart-indicators.d.ts +27 -0
  74. package/dist/perps/chart-indicators.d.ts.map +1 -0
  75. package/dist/perps/chart-indicators.js +345 -0
  76. package/dist/perps/chart-indicators.js.map +1 -0
  77. package/dist/perps/chart-tools.d.ts +45 -0
  78. package/dist/perps/chart-tools.d.ts.map +1 -0
  79. package/dist/perps/chart-tools.js +341 -0
  80. package/dist/perps/chart-tools.js.map +1 -0
  81. package/dist/perps/perp-analytics-tools.d.ts +20 -0
  82. package/dist/perps/perp-analytics-tools.d.ts.map +1 -0
  83. package/dist/perps/perp-analytics-tools.js +868 -0
  84. package/dist/perps/perp-analytics-tools.js.map +1 -0
  85. package/dist/perps/perp-constants.d.ts +362 -0
  86. package/dist/perps/perp-constants.d.ts.map +1 -0
  87. package/dist/perps/perp-constants.js +210 -0
  88. package/dist/perps/perp-constants.js.map +1 -0
  89. package/dist/perps/perp-decoders.d.ts +47 -0
  90. package/dist/perps/perp-decoders.d.ts.map +1 -0
  91. package/dist/perps/perp-decoders.js +332 -0
  92. package/dist/perps/perp-decoders.js.map +1 -0
  93. package/dist/policy/policy-engine.d.ts +42 -0
  94. package/dist/policy/policy-engine.d.ts.map +1 -1
  95. package/dist/policy/policy-engine.js +43 -0
  96. package/dist/policy/policy-engine.js.map +1 -1
  97. package/dist/premium/activation-manager.d.ts.map +1 -1
  98. package/dist/premium/activation-manager.js +20 -2
  99. package/dist/premium/activation-manager.js.map +1 -1
  100. package/dist/prompts/context/sap-agent-context.prompt.js +15 -1
  101. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  102. package/dist/server/server-metadata.d.ts +13 -0
  103. package/dist/server/server-metadata.d.ts.map +1 -1
  104. package/dist/server/server-metadata.js +15 -2
  105. package/dist/server/server-metadata.js.map +1 -1
  106. package/dist/solana/ata-utils.d.ts +39 -0
  107. package/dist/solana/ata-utils.d.ts.map +1 -0
  108. package/dist/solana/ata-utils.js +49 -0
  109. package/dist/solana/ata-utils.js.map +1 -0
  110. package/dist/strategies/trade-journal.d.ts +78 -0
  111. package/dist/strategies/trade-journal.d.ts.map +1 -0
  112. package/dist/strategies/trade-journal.js +92 -0
  113. package/dist/strategies/trade-journal.js.map +1 -0
  114. package/dist/tools/adrena/adrena-commodity-tools.d.ts +15 -0
  115. package/dist/tools/adrena/adrena-commodity-tools.d.ts.map +1 -0
  116. package/dist/tools/adrena/adrena-commodity-tools.js +137 -0
  117. package/dist/tools/adrena/adrena-commodity-tools.js.map +1 -0
  118. package/dist/tools/adrena/adrena-data-tools.d.ts +23 -0
  119. package/dist/tools/adrena/adrena-data-tools.d.ts.map +1 -0
  120. package/dist/tools/adrena/adrena-data-tools.js +437 -0
  121. package/dist/tools/adrena/adrena-data-tools.js.map +1 -0
  122. package/dist/tools/adrena/adrena-helpers.d.ts +46 -0
  123. package/dist/tools/adrena/adrena-helpers.d.ts.map +1 -0
  124. package/dist/tools/adrena/adrena-helpers.js +41 -0
  125. package/dist/tools/adrena/adrena-helpers.js.map +1 -0
  126. package/dist/tools/adrena/adrena-index.d.ts +20 -0
  127. package/dist/tools/adrena/adrena-index.d.ts.map +1 -0
  128. package/dist/tools/adrena/adrena-index.js +75 -0
  129. package/dist/tools/adrena/adrena-index.js.map +1 -0
  130. package/dist/tools/adrena/adrena-limit-order-tools.d.ts +22 -0
  131. package/dist/tools/adrena/adrena-limit-order-tools.d.ts.map +1 -0
  132. package/dist/tools/adrena/adrena-limit-order-tools.js +91 -0
  133. package/dist/tools/adrena/adrena-limit-order-tools.js.map +1 -0
  134. package/dist/tools/adrena/adrena-liquidity-tools.d.ts +15 -0
  135. package/dist/tools/adrena/adrena-liquidity-tools.d.ts.map +1 -0
  136. package/dist/tools/adrena/adrena-liquidity-tools.js +108 -0
  137. package/dist/tools/adrena/adrena-liquidity-tools.js.map +1 -0
  138. package/dist/tools/adrena/adrena-snapshot-tools.d.ts +42 -0
  139. package/dist/tools/adrena/adrena-snapshot-tools.d.ts.map +1 -0
  140. package/dist/tools/adrena/adrena-snapshot-tools.js +124 -0
  141. package/dist/tools/adrena/adrena-snapshot-tools.js.map +1 -0
  142. package/dist/tools/adrena/adrena-staking-tools.d.ts +15 -0
  143. package/dist/tools/adrena/adrena-staking-tools.d.ts.map +1 -0
  144. package/dist/tools/adrena/adrena-staking-tools.js +139 -0
  145. package/dist/tools/adrena/adrena-staking-tools.js.map +1 -0
  146. package/dist/tools/adrena/adrena-trading-tools.d.ts +78 -0
  147. package/dist/tools/adrena/adrena-trading-tools.d.ts.map +1 -0
  148. package/dist/tools/adrena/adrena-trading-tools.js +556 -0
  149. package/dist/tools/adrena/adrena-trading-tools.js.map +1 -0
  150. package/dist/tools/adrena-tools.d.ts +1 -66
  151. package/dist/tools/adrena-tools.d.ts.map +1 -1
  152. package/dist/tools/adrena-tools.js +1 -1061
  153. package/dist/tools/adrena-tools.js.map +1 -1
  154. package/dist/tools/client-sdk-tools.js +1 -1
  155. package/dist/tools/client-sdk-tools.js.map +1 -1
  156. package/dist/tools/memory-tools.d.ts.map +1 -1
  157. package/dist/tools/memory-tools.js +170 -0
  158. package/dist/tools/memory-tools.js.map +1 -1
  159. package/dist/tools/perp-tools.d.ts +6 -174
  160. package/dist/tools/perp-tools.d.ts.map +1 -1
  161. package/dist/tools/perp-tools.js +6 -1716
  162. package/dist/tools/perp-tools.js.map +1 -1
  163. package/dist/tools/premium-tools.d.ts.map +1 -1
  164. package/dist/tools/premium-tools.js +123 -5
  165. package/dist/tools/premium-tools.js.map +1 -1
  166. package/dist/tools/skills-tools.d.ts.map +1 -1
  167. package/dist/tools/skills-tools.js +7 -4
  168. package/dist/tools/skills-tools.js.map +1 -1
  169. package/dist/tools/transaction-tools.d.ts +21 -1
  170. package/dist/tools/transaction-tools.d.ts.map +1 -1
  171. package/dist/tools/transaction-tools.js +114 -30
  172. package/dist/tools/transaction-tools.js.map +1 -1
  173. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
  174. package/dist/tools/x402-paid-call-tool.js +288 -0
  175. package/dist/tools/x402-paid-call-tool.js.map +1 -1
  176. package/package.json +3 -2
  177. package/server.json +3 -3
  178. package/skills/sap-clawpump-bridge/SKILL.md +206 -0
@@ -1,1722 +1,12 @@
1
1
  /**
2
2
  * @name tools/perp-tools
3
- * @description MCP tools for perpetual futures trading and chart analysis on Solana.
4
- *
5
- * Provides perp analytics plus explicit execution-readiness tooling:
6
- *
7
- * Read-only tools (7):
8
- * - sap_perp_markets — List Adrena markets with funding/accounting, OI, and oracle ids.
9
- * - sap_perp_position_info — Read on-chain perp positions for a wallet.
10
- * - sap_perp_funding_history — Compute funding rate from on-chain custody account.
11
- * - sap_chart_ohlc — OHLC candlestick data for any Solana token.
12
- * - sap_chart_long_term — Long-term price history + protocol TVL.
13
- * - sap_chart_volume_profile — Volume profile analysis (POC, VAH, VAL).
14
- * - sap_perp_liquidation_zones — Compute liquidation zones for open positions.
15
- *
16
- * Professional planning tools:
17
- * - sap_perp_trade_plan — Build a trader-grade risk, route, and execution checklist.
18
- * - sap_perp_builder_status — Report whether a hosted unsigned perps builder is configured.
19
- *
20
- * Adrena reads decode Pool, Custody, and Position accounts directly from the
21
- * official release/39 ABI layout through Solana RPC. Optional provider hooks
22
- * can enrich reads or build unsigned execution transactions, but SAP MCP never
23
- * pretends a perps execution route exists until the builder can return a real
24
- * locally finalizable Solana transaction.
3
+ * @description Barrel re-export for perp tools split into perp-constants, perp-decoders,
4
+ * chart-tools, and perp-analytics-tools under src/perps/.
25
5
  *
26
6
  * @module tools/perp-tools
27
7
  */
28
- import { PublicKey } from '@solana/web3.js';
29
- import bs58 from 'bs58';
30
- import { createTextResponse } from '../adapters/mcp/tool-response.js';
31
- import { registerTool } from '../adapters/mcp/sdk-compat.js';
32
- import { logger } from '../core/logger.js';
33
- /* ═══════════════════════════════════════════════════════════════════
34
- * Constants
35
- * ═══════════════════════════════════════════════════════════════════ */
36
- /** Adrena program ID on Solana mainnet. Can be overridden by hosted env config. */
37
- const DEFAULT_ADRENA_PROGRAM_ID = '13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3SEAwet';
38
- /** DexScreener REST API base URL (free, no API key). */
39
- const DEXSCREENER_API_URL = 'https://api.dexscreener.com';
40
- /** DeFiLlama REST API base URL (free, no API key). */
41
- const DEFILAMA_API_URL = 'https://api.llama.fi';
42
- /** Fetch timeout for external API calls (ms). */
43
- const FETCH_TIMEOUT_MS = 8_000;
44
- /** Default number of volume profile buckets. */
45
- const DEFAULT_VP_BUCKETS = 20;
46
- /** Value area percentage for volume profile (70%). */
47
- const VALUE_AREA_PCT = 0.70;
48
- /* ═══════════════════════════════════════════════════════════════════
49
- * Adrena account discriminators (Anchor 0.31 — sha256("account:<Name>")[0..8])
50
- * ═══════════════════════════════════════════════════════════════════ */
51
- /** Pool account discriminator — sha256("account:Pool")[0..8]. */
52
- const DISC_POOL = Buffer.from([241, 154, 109, 4, 17, 177, 109, 188]);
53
- /** Custody account discriminator — sha256("account:Custody")[0..8]. */
54
- const DISC_CUSTODY = Buffer.from([1, 184, 48, 81, 93, 131, 63, 145]);
55
- /** Position account discriminator — sha256("account:Position")[0..8]. */
56
- const DISC_POSITION = Buffer.from([170, 188, 143, 228, 122, 64, 247, 208]);
57
- /** Adrena release/39 constants from the official ABI. */
58
- const ADRENA_USD_DECIMALS = 6;
59
- const ADRENA_PRICE_DECIMALS = 10;
60
- const ADRENA_BPS_DECIMALS = 4;
61
- /** Absolute offsets include the 8-byte Anchor account discriminator. */
62
- const ADRENA_CUSTODY_OFFSETS = {
63
- allowTrade: 10,
64
- allowSwap: 11,
65
- decimals: 12,
66
- pool: 16,
67
- mint: 48,
68
- tokenAccount: 80,
69
- oracle: 112,
70
- tradeOracle: 144,
71
- maxInitialLeverage: 176,
72
- maxLeverage: 180,
73
- maxPositionLockedUsd: 184,
74
- volumeOpenPositionUsd: 320,
75
- tradeOiLongUsd: 360,
76
- tradeOiShortUsd: 368,
77
- assetsCollateral: 376,
78
- assetsOwned: 384,
79
- assetsLocked: 392,
80
- longPositionsOpenCount: 400,
81
- longPositionsSizeUsd: 408,
82
- longPositionsBorrowSizeUsd: 416,
83
- longPositionsLockedAmount: 424,
84
- longPositionsCollateralUsd: 472,
85
- shortPositionsOpenCount: 600,
86
- shortPositionsSizeUsd: 608,
87
- shortPositionsBorrowSizeUsd: 616,
88
- shortPositionsLockedAmount: 624,
89
- shortPositionsCollateralUsd: 672,
90
- borrowRateCurrent: 800,
91
- borrowRateLastUpdate: 808,
92
- optimalUtilizationBps: 832,
93
- virtualFundingMaxHourlyRate: 840,
94
- virtualFundingMinTotalOiUsd: 848,
95
- virtualFundingImbalanceSensitivityBps: 856,
96
- virtualFundingCurrentLongToShort: 864,
97
- virtualFundingLastUpdate: 872,
98
- virtualFundingCumulativeLongToShort: 880,
99
- virtualFundingCumulativeShortToLong: 896,
100
- isSynthetic: 912,
101
- version: 913,
102
- oracleFeedId: 914,
103
- tradeOracleFeedId: 915,
104
- };
105
- const ADRENA_POOL_OFFSETS = {
106
- allowTrade: 12,
107
- allowSwap: 13,
108
- registeredCustodyCount: 15,
109
- name: 16,
110
- custodies: 48,
111
- lpTokenPriceUsd: 328,
112
- aumUsd: 448,
113
- };
114
- export const ADRENA_POSITION_OWNER_MEMCMP_OFFSET = 16;
115
- const ADRENA_POSITION_OFFSETS = {
116
- side: 9,
117
- owner: ADRENA_POSITION_OWNER_MEMCMP_OFFSET,
118
- pool: 48,
119
- custody: 80,
120
- collateralCustody: 112,
121
- openTime: 144,
122
- updateTime: 152,
123
- price: 160,
124
- sizeUsd: 168,
125
- borrowSizeUsd: 176,
126
- collateralUsd: 184,
127
- unrealizedInterestUsd: 192,
128
- lockedAmount: 216,
129
- collateralAmount: 224,
130
- exitFeeUsd: 232,
131
- liquidationFeeUsd: 240,
132
- id: 248,
133
- takeProfitLimitPrice: 256,
134
- paidInterestUsd: 264,
135
- stopLossLimitPrice: 272,
136
- stopLossClosePositionPrice: 280,
137
- unrealizedFundingPaidUsd: 328,
138
- unrealizedFundingReceivedUsd: 336,
139
- };
140
- const ADRENA_MAINNET_POOLS = {
141
- '4bQRutgDJs6vuh6ZcWaPVXiQaBzbHketjbCDjL4oRN34': { name: 'main-pool', type: 'gmx' },
142
- GN2hyBVHcUitWETeDfAoeXDMqow1x8StqdRFnGaUB2vb: { name: 'commodities-pool', type: 'autonom' },
143
- };
144
- const ADRENA_MAINNET_MARKETS_BY_CUSTODY = {
145
- Dk523LZeDQbZtUwPEBjFXCd2Au1tD7mWZBJJmcgHktNk: { symbol: 'USDC', market: 'USDC', poolName: 'main-pool', kind: 'collateral' },
146
- '8aJuzsgjxBnvRhDcfQBD7z4CUj7QoPEpaNwVd7KqsSk5': { symbol: 'BONK', market: 'BONK-PERP', poolName: 'main-pool', kind: 'perp' },
147
- GZ9XfWwgTRhkma2Y91Q9r1XKotNXYjBnKKabj19rhT71: { symbol: 'JITOSOL', market: 'JITOSOL-PERP', poolName: 'main-pool', kind: 'perp' },
148
- GFu3qS22mo6bAjg4Lr5R7L8pPgHq6GvbjJPKEHkbbs2c: { symbol: 'WBTC', market: 'WBTC-PERP', poolName: 'main-pool', kind: 'perp' },
149
- woVG8fmrUzFJhWa6mRjiYC2qFCY73oAnQeioYK1m1JX: { symbol: 'USDC', market: 'USDC', poolName: 'commodities-pool', kind: 'collateral' },
150
- JB86ouHXGYgF4UbPs8yxYdaHudrdsintf5EbBfMydzYt: { symbol: 'XAU', market: 'XAU-PERP', poolName: 'commodities-pool', kind: 'synthetic-perp' },
151
- De21TFyUPHkvFsWAt6xJLBBXGp636VuL5cKk2DvfbHiR: { symbol: 'WTI', market: 'WTI-PERP', poolName: 'commodities-pool', kind: 'synthetic-perp' },
152
- PexsCkkxpVmY4HNxUjT3U9PEg69kYScc8GukUwn6Q3Q: { symbol: 'XAG', market: 'XAG-PERP', poolName: 'commodities-pool', kind: 'synthetic-perp' },
153
- };
154
- /* ═══════════════════════════════════════════════════════════════════
155
- * Helper: timed fetch
156
- * ═══════════════════════════════════════════════════════════════════ */
157
- /**
158
- * @name timedFetch
159
- * @description Fetch JSON from a URL with an abort timeout.
160
- *
161
- * @param url — The URL to fetch.
162
- * @returns Parsed JSON response, or `null` on error/timeout.
163
- *
164
- * @internal
165
- */
166
- async function timedFetch(url, options = {}) {
167
- try {
168
- const controller = new AbortController();
169
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? FETCH_TIMEOUT_MS);
170
- const response = await fetch(url, {
171
- method: options.method ?? 'GET',
172
- headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },
173
- body: options.body,
174
- signal: controller.signal,
175
- });
176
- clearTimeout(timeout);
177
- if (!response.ok)
178
- return null;
179
- return await response.json();
180
- }
181
- catch {
182
- return null;
183
- }
184
- }
185
- function getPerpsConfig(context) {
186
- return context.config.perps ?? {
187
- adrenaProgramId: DEFAULT_ADRENA_PROGRAM_ID,
188
- apiKeyConfigured: Boolean(process.env.SAP_MCP_PERPS_API_KEY?.trim()),
189
- timeoutMs: FETCH_TIMEOUT_MS,
190
- };
191
- }
192
- function getAdrenaProgramId(context) {
193
- return new PublicKey(getPerpsConfig(context).adrenaProgramId || DEFAULT_ADRENA_PROGRAM_ID);
194
- }
195
- function perpsProviderHeaders() {
196
- const apiKey = process.env.SAP_MCP_PERPS_API_KEY?.trim();
197
- if (!apiKey) {
198
- return {};
199
- }
200
- return {
201
- Authorization: `Bearer ${apiKey}`,
202
- 'X-API-Key': apiKey,
203
- };
204
- }
205
- function appendQuery(baseUrl, params) {
206
- const url = new URL(baseUrl);
207
- for (const [key, value] of Object.entries(params)) {
208
- if (value) {
209
- url.searchParams.set(key, value);
210
- }
211
- }
212
- return url.toString();
213
- }
214
- export function normalizePerpProviderMarkets(payload, marketFilter = '') {
215
- const records = Array.isArray(payload)
216
- ? payload
217
- : payload && typeof payload === 'object' && !Array.isArray(payload)
218
- ? (payload.markets ?? payload.data ?? [])
219
- : [];
220
- const normalized = records
221
- .filter((record) => Boolean(record) && typeof record === 'object' && !Array.isArray(record))
222
- .map((record) => {
223
- const symbol = String(record.symbol ?? record.market ?? '').toUpperCase();
224
- return {
225
- ...record,
226
- symbol,
227
- market: String(record.market ?? symbol),
228
- };
229
- });
230
- if (!marketFilter) {
231
- return normalized;
232
- }
233
- return normalized.filter((record) => record.symbol === marketFilter || String(record.market).toUpperCase() === marketFilter);
234
- }
235
- async function fetchConfiguredPerpMarkets(context, marketFilter) {
236
- const perps = getPerpsConfig(context);
237
- if (!perps.marketsUrl) {
238
- return null;
239
- }
240
- const url = appendQuery(perps.marketsUrl, { market: marketFilter || undefined });
241
- const payload = await timedFetch(url, {
242
- headers: perpsProviderHeaders(),
243
- timeoutMs: perps.timeoutMs,
244
- });
245
- if (!payload) {
246
- return null;
247
- }
248
- return {
249
- markets: normalizePerpProviderMarkets(payload, marketFilter),
250
- source: Array.isArray(payload) ? 'configured-perps-provider' : payload.source ?? payload.venue ?? 'configured-perps-provider',
251
- timestamp: Array.isArray(payload) ? Date.now() : payload.timestamp,
252
- };
253
- }
254
- /**
255
- * @name discToBase58
256
- * @description Convert an 8-byte discriminator Buffer to a base58 string for
257
- * use in `getProgramAccounts` memcmp filters.
258
- *
259
- * @param disc — 8-byte discriminator Buffer.
260
- * @returns Base58-encoded string.
261
- *
262
- * @internal
263
- */
264
- export function discToBase58(disc) {
265
- return bs58.encode(disc);
266
- }
267
- /**
268
- * @name readU64LE
269
- * @description Read a u64 little-endian value from a Buffer and return as a JS number.
270
- *
271
- * @param buf — Source buffer.
272
- * @param offset — Read offset.
273
- * @returns The value as a JavaScript number.
274
- *
275
- * @internal
276
- */
277
- function readU64LE(buf, offset) {
278
- return Number(buf.readBigUInt64LE(offset));
279
- }
280
- /**
281
- * @name readI64LE
282
- * @description Read an i64 little-endian value from a Buffer and return as a JS number.
283
- *
284
- * @param buf — Source buffer.
285
- * @param offset — Read offset.
286
- * @returns The signed value as a JavaScript number.
287
- *
288
- * @internal
289
- */
290
- function readI64LE(buf, offset) {
291
- return Number(buf.readBigInt64LE(offset));
292
- }
293
- function readU128Split(buf, offset) {
294
- const high = buf.readBigUInt64LE(offset);
295
- const low = buf.readBigUInt64LE(offset + 8);
296
- return (high << 64n) + low;
297
- }
298
- function readPublicKey(buf, offset) {
299
- return new PublicKey(buf.subarray(offset, offset + 32)).toBase58();
300
- }
301
- function scaleNumber(raw, decimals) {
302
- return Number(raw) / (10 ** decimals);
303
- }
304
- function readUsd(buf, offset) {
305
- return scaleNumber(buf.readBigUInt64LE(offset), ADRENA_USD_DECIMALS);
306
- }
307
- function readPrice(buf, offset) {
308
- return scaleNumber(buf.readBigUInt64LE(offset), ADRENA_PRICE_DECIMALS);
309
- }
310
- /**
311
- * @name readAdrenaLimitedString
312
- * @description Read Adrena `LimitedString` (31-byte value + 1-byte length)
313
- * from an account data buffer.
314
- *
315
- * @param buf — Source buffer.
316
- * @param offset — Offset to the LimitedString field.
317
- * @returns Decoded symbol string, or empty string on failure.
318
- *
319
- * @internal
320
- */
321
- export function readAdrenaLimitedString(buf, offset) {
322
- try {
323
- const len = buf[offset + 31] ?? 0;
324
- if (len === 0 || len > 31)
325
- return '';
326
- return buf.subarray(offset, offset + len).toString('utf8').replace(/\0/g, '').trim();
327
- }
328
- catch {
329
- return '';
330
- }
331
- }
332
- function inferAdrenaSymbolFromOracle(oracle, fallback) {
333
- const normalized = oracle.toUpperCase().replace(/[^A-Z0-9]/g, '');
334
- for (const candidate of ['JITOSOL', 'WBTC', 'BONK', 'USDC', 'XAU', 'WTI', 'XAG', 'SOL', 'BTC']) {
335
- if (normalized.includes(candidate)) {
336
- return candidate;
337
- }
338
- }
339
- return fallback;
340
- }
341
- function decodeAdrenaPoolAccount(pubkey, data) {
342
- if (data.length < ADRENA_POOL_OFFSETS.custodies + 32) {
343
- return null;
344
- }
345
- if (!data.subarray(0, 8).equals(DISC_POOL)) {
346
- return null;
347
- }
348
- const poolAddress = pubkey.toBase58();
349
- const registeredCustodyCount = data[ADRENA_POOL_OFFSETS.registeredCustodyCount] ?? 0;
350
- const custodies = [];
351
- const custodyCount = Math.min(registeredCustodyCount, 8);
352
- for (let i = 0; i < custodyCount; i++) {
353
- custodies.push(readPublicKey(data, ADRENA_POOL_OFFSETS.custodies + i * 32));
354
- }
355
- const manifestPool = ADRENA_MAINNET_POOLS[poolAddress];
356
- return {
357
- poolAddress,
358
- name: readAdrenaLimitedString(data, ADRENA_POOL_OFFSETS.name) || manifestPool?.name || 'unknown-pool',
359
- type: manifestPool?.type,
360
- allowTrade: data[ADRENA_POOL_OFFSETS.allowTrade] !== 0,
361
- allowSwap: data[ADRENA_POOL_OFFSETS.allowSwap] !== 0,
362
- registeredCustodyCount,
363
- custodies,
364
- lpTokenPriceUsd: readUsd(data, ADRENA_POOL_OFFSETS.lpTokenPriceUsd),
365
- aumUsd: scaleNumber(readU128Split(data, ADRENA_POOL_OFFSETS.aumUsd), ADRENA_USD_DECIMALS),
366
- };
367
- }
368
- export function decodeAdrenaCustodyAccount(pubkey, data, pool) {
369
- if (data.length < ADRENA_CUSTODY_OFFSETS.version + 1) {
370
- return null;
371
- }
372
- if (!data.subarray(0, 8).equals(DISC_CUSTODY)) {
373
- return null;
374
- }
375
- const custodyAddress = pubkey.toBase58();
376
- const manifest = ADRENA_MAINNET_MARKETS_BY_CUSTODY[custodyAddress];
377
- const poolAddress = readPublicKey(data, ADRENA_CUSTODY_OFFSETS.pool);
378
- const custodyMint = readPublicKey(data, ADRENA_CUSTODY_OFFSETS.mint);
379
- const oracle = readAdrenaLimitedString(data, ADRENA_CUSTODY_OFFSETS.oracle);
380
- const tradeOracle = readAdrenaLimitedString(data, ADRENA_CUSTODY_OFFSETS.tradeOracle);
381
- const inferredSymbol = inferAdrenaSymbolFromOracle(tradeOracle || oracle, custodyMint.slice(0, 6).toUpperCase());
382
- const symbol = manifest?.symbol ?? inferredSymbol;
383
- const isSynthetic = data[ADRENA_CUSTODY_OFFSETS.isSynthetic] !== 0;
384
- const kind = manifest?.kind ?? (isSynthetic ? 'synthetic-perp' : 'unknown');
385
- return {
386
- symbol,
387
- market: manifest?.market ?? (kind === 'collateral' ? symbol : `${symbol}-PERP`),
388
- custodyAddress,
389
- poolAddress,
390
- poolName: pool?.name ?? manifest?.poolName ?? ADRENA_MAINNET_POOLS[poolAddress]?.name,
391
- poolType: pool?.type ?? ADRENA_MAINNET_POOLS[poolAddress]?.type,
392
- custodyMint,
393
- tokenAccount: readPublicKey(data, ADRENA_CUSTODY_OFFSETS.tokenAccount),
394
- oracle,
395
- tradeOracle,
396
- oracleFeedId: data[ADRENA_CUSTODY_OFFSETS.oracleFeedId] ?? 0,
397
- tradeOracleFeedId: data[ADRENA_CUSTODY_OFFSETS.tradeOracleFeedId] ?? 0,
398
- decimals: data[ADRENA_CUSTODY_OFFSETS.decimals] ?? 0,
399
- allowTrade: data[ADRENA_CUSTODY_OFFSETS.allowTrade] !== 0,
400
- allowSwap: data[ADRENA_CUSTODY_OFFSETS.allowSwap] !== 0,
401
- isStable: data[13] !== 0,
402
- isSynthetic,
403
- version: data[ADRENA_CUSTODY_OFFSETS.version] ?? 0,
404
- kind,
405
- markPrice: null,
406
- markPriceSource: 'not_in_custody_account',
407
- maxInitialLeverage: data.readUInt32LE(ADRENA_CUSTODY_OFFSETS.maxInitialLeverage) / (10 ** ADRENA_BPS_DECIMALS),
408
- maxLeverage: data.readUInt32LE(ADRENA_CUSTODY_OFFSETS.maxLeverage) / (10 ** ADRENA_BPS_DECIMALS),
409
- maxPositionLockedUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.maxPositionLockedUsd),
410
- openInterestLong: readUsd(data, ADRENA_CUSTODY_OFFSETS.longPositionsSizeUsd),
411
- openInterestShort: readUsd(data, ADRENA_CUSTODY_OFFSETS.shortPositionsSizeUsd),
412
- openPositionsLong: readU64LE(data, ADRENA_CUSTODY_OFFSETS.longPositionsOpenCount),
413
- openPositionsShort: readU64LE(data, ADRENA_CUSTODY_OFFSETS.shortPositionsOpenCount),
414
- borrowSizeLongUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.longPositionsBorrowSizeUsd),
415
- borrowSizeShortUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.shortPositionsBorrowSizeUsd),
416
- collateralLongUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.longPositionsCollateralUsd),
417
- collateralShortUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.shortPositionsCollateralUsd),
418
- lockedAmountLongRaw: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.longPositionsLockedAmount).toString(),
419
- lockedAmountShortRaw: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.shortPositionsLockedAmount).toString(),
420
- tradeOiLongUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.tradeOiLongUsd),
421
- tradeOiShortUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.tradeOiShortUsd),
422
- cumulativeOpenPositionUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.volumeOpenPositionUsd),
423
- assetsRaw: {
424
- collateral: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.assetsCollateral).toString(),
425
- owned: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.assetsOwned).toString(),
426
- locked: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.assetsLocked).toString(),
427
- },
428
- borrowRate: {
429
- currentRateRaw: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.borrowRateCurrent).toString(),
430
- lastUpdate: readI64LE(data, ADRENA_CUSTODY_OFFSETS.borrowRateLastUpdate),
431
- },
432
- funding: {
433
- currentRateLongToShortRaw: data.readBigInt64LE(ADRENA_CUSTODY_OFFSETS.virtualFundingCurrentLongToShort).toString(),
434
- lastUpdate: readI64LE(data, ADRENA_CUSTODY_OFFSETS.virtualFundingLastUpdate),
435
- cumulativeLongToShortRaw: readU128Split(data, ADRENA_CUSTODY_OFFSETS.virtualFundingCumulativeLongToShort).toString(),
436
- cumulativeShortToLongRaw: readU128Split(data, ADRENA_CUSTODY_OFFSETS.virtualFundingCumulativeShortToLong).toString(),
437
- maxHourlyFundingRateRaw: data.readBigUInt64LE(ADRENA_CUSTODY_OFFSETS.virtualFundingMaxHourlyRate).toString(),
438
- minTotalOiUsd: readUsd(data, ADRENA_CUSTODY_OFFSETS.virtualFundingMinTotalOiUsd),
439
- imbalanceSensitivityBps: data.readUInt16LE(ADRENA_CUSTODY_OFFSETS.virtualFundingImbalanceSensitivityBps),
440
- },
441
- };
442
- }
443
- function sideFromAdrenaByte(sideByte) {
444
- if (sideByte === 1)
445
- return 'long';
446
- if (sideByte === 2)
447
- return 'short';
448
- return null;
449
- }
450
- export function decodeAdrenaPositionAccount(pubkey, data, marketsByCustody = new Map()) {
451
- if (data.length < ADRENA_POSITION_OFFSETS.unrealizedFundingReceivedUsd + 8) {
452
- return null;
453
- }
454
- if (!data.subarray(0, 8).equals(DISC_POSITION)) {
455
- return null;
456
- }
457
- const sideRaw = data[ADRENA_POSITION_OFFSETS.side] ?? 0;
458
- const side = sideFromAdrenaByte(sideRaw);
459
- if (!side) {
460
- return null;
461
- }
462
- const custodyAddress = readPublicKey(data, ADRENA_POSITION_OFFSETS.custody);
463
- const market = marketsByCustody.get(custodyAddress)?.market ?? 'unknown';
464
- const entryPrice = readPrice(data, ADRENA_POSITION_OFFSETS.price);
465
- const sizeUsd = readUsd(data, ADRENA_POSITION_OFFSETS.sizeUsd);
466
- const collateralUsd = readUsd(data, ADRENA_POSITION_OFFSETS.collateralUsd);
467
- const leverage = collateralUsd > 0 ? sizeUsd / collateralUsd : 0;
468
- const liquidationPrice = leverage > 0
469
- ? side === 'long'
470
- ? entryPrice * (1 - 1 / leverage)
471
- : entryPrice * (1 + 1 / leverage)
472
- : 0;
473
- const takeProfitLimitPrice = readPrice(data, ADRENA_POSITION_OFFSETS.takeProfitLimitPrice);
474
- const stopLossLimitPrice = readPrice(data, ADRENA_POSITION_OFFSETS.stopLossLimitPrice);
475
- const stopLossClosePositionPrice = readPrice(data, ADRENA_POSITION_OFFSETS.stopLossClosePositionPrice);
476
- return {
477
- positionKey: pubkey.toBase58(),
478
- market,
479
- side,
480
- sideRaw,
481
- owner: readPublicKey(data, ADRENA_POSITION_OFFSETS.owner),
482
- poolAddress: readPublicKey(data, ADRENA_POSITION_OFFSETS.pool),
483
- custodyAddress,
484
- collateralCustodyAddress: readPublicKey(data, ADRENA_POSITION_OFFSETS.collateralCustody),
485
- openTime: readI64LE(data, ADRENA_POSITION_OFFSETS.openTime),
486
- updateTime: readI64LE(data, ADRENA_POSITION_OFFSETS.updateTime),
487
- entryPrice,
488
- entryPriceRaw: data.readBigUInt64LE(ADRENA_POSITION_OFFSETS.price).toString(),
489
- size: sizeUsd,
490
- collateral: collateralUsd,
491
- borrowSizeUsd: readUsd(data, ADRENA_POSITION_OFFSETS.borrowSizeUsd),
492
- leverage,
493
- unrealizedPnl: 0,
494
- unrealizedInterestUsd: readUsd(data, ADRENA_POSITION_OFFSETS.unrealizedInterestUsd),
495
- unrealizedFundingPaidUsd: readUsd(data, ADRENA_POSITION_OFFSETS.unrealizedFundingPaidUsd),
496
- unrealizedFundingReceivedUsd: readUsd(data, ADRENA_POSITION_OFFSETS.unrealizedFundingReceivedUsd),
497
- lockedAmountRaw: data.readBigUInt64LE(ADRENA_POSITION_OFFSETS.lockedAmount).toString(),
498
- collateralAmountRaw: data.readBigUInt64LE(ADRENA_POSITION_OFFSETS.collateralAmount).toString(),
499
- liquidationPrice,
500
- takeProfitLimitPrice: takeProfitLimitPrice > 0 ? takeProfitLimitPrice : null,
501
- stopLossLimitPrice: stopLossLimitPrice > 0 ? stopLossLimitPrice : null,
502
- stopLossClosePositionPrice: stopLossClosePositionPrice > 0 ? stopLossClosePositionPrice : null,
503
- id: data.readBigUInt64LE(ADRENA_POSITION_OFFSETS.id).toString(),
504
- };
505
- }
506
- async function readAdrenaMarketsByCustody(context) {
507
- const adrenaProgramId = getAdrenaProgramId(context);
508
- const [poolAccounts, custodyAccounts] = await Promise.all([
509
- context.connection.getProgramAccounts(adrenaProgramId, {
510
- filters: [{ memcmp: { offset: 0, bytes: discToBase58(DISC_POOL) } }],
511
- commitment: 'confirmed',
512
- }),
513
- context.connection.getProgramAccounts(adrenaProgramId, {
514
- filters: [{ memcmp: { offset: 0, bytes: discToBase58(DISC_CUSTODY) } }],
515
- commitment: 'confirmed',
516
- }),
517
- ]);
518
- const pools = new Map();
519
- for (const { pubkey, account } of poolAccounts) {
520
- const decoded = decodeAdrenaPoolAccount(pubkey, account.data);
521
- if (decoded) {
522
- pools.set(decoded.poolAddress, decoded);
523
- }
524
- }
525
- const markets = new Map();
526
- for (const { pubkey, account } of custodyAccounts) {
527
- const poolAddress = account.data.length >= ADRENA_CUSTODY_OFFSETS.pool + 32
528
- ? readPublicKey(account.data, ADRENA_CUSTODY_OFFSETS.pool)
529
- : '';
530
- const decoded = decodeAdrenaCustodyAccount(pubkey, account.data, pools.get(poolAddress));
531
- if (decoded) {
532
- markets.set(decoded.custodyAddress, decoded);
533
- }
534
- }
535
- return markets;
536
- }
537
- /* ═══════════════════════════════════════════════════════════════════
538
- * Tool 1: sap_perp_markets
539
- * ═══════════════════════════════════════════════════════════════════ */
540
- /**
541
- * @name registerPerpMarketsTool
542
- * @description Register the sap_perp_markets read-only tool.
543
- *
544
- * Reads Pool and Custody accounts directly from Solana RPC using
545
- * `getProgramAccounts` with memcmp discriminator filters. Decodes symbol,
546
- * price, funding rate, and open interest from raw account data.
547
- *
548
- * @param server — MCP server instance.
549
- * @param context — Runtime context with Solana RPC connection.
550
- *
551
- * @internal
552
- */
553
- function registerPerpMarketsTool(server, context) {
554
- const schema = {
555
- type: 'object',
556
- properties: {
557
- market: {
558
- type: 'string',
559
- description: 'Optional market symbol filter (e.g. "SOL", "BTC"). Empty = all markets.',
560
- },
561
- },
562
- additionalProperties: false,
563
- };
564
- registerTool(server, 'sap_perp_markets', {
565
- description: 'List available Adrena perpetual futures markets from Pool/Custody accounts with funding/accounting fields, open interest, leverage caps, and oracle identifiers. Read-only analysis tool. Custody accounts do not expose a ready mark price, so markPrice is null until an oracle/feed decoder or configured Adrena data runtime is available. If markets are empty, treat it as data unavailable, not proof that markets do not exist. SAP MCP exposes execution only when sap_perp_builder_status reports a verified hosted unsigned builder.',
566
- inputSchema: schema,
567
- }, async (args) => {
568
- const marketFilter = typeof args['market'] === 'string' ? args['market'].toUpperCase() : '';
569
- try {
570
- const providerMarkets = await fetchConfiguredPerpMarkets(context, marketFilter);
571
- if (providerMarkets) {
572
- return createTextResponse(JSON.stringify({
573
- source: providerMarkets.source,
574
- markets: providerMarkets.markets,
575
- count: providerMarkets.markets.length,
576
- timestamp: providerMarkets.timestamp ?? Date.now(),
577
- dataAvailability: {
578
- status: providerMarkets.markets.length > 0 ? 'available' : 'empty_from_provider',
579
- reason: providerMarkets.markets.length > 0
580
- ? 'Configured perps market provider returned market data.'
581
- : 'Configured perps market provider responded but returned no matching markets for the requested filter.',
582
- agentAction: providerMarkets.markets.length > 0
583
- ? 'Use these records for professional planning. Before execution, call sap_perp_builder_status to verify whether a hosted unsigned builder is configured.'
584
- : 'Do not infer that the venue has no markets. Try a broader market filter or check provider status before planning execution.',
585
- },
586
- recommendedNextTools: [
587
- 'sap_perp_builder_status',
588
- 'sap_chart_ohlc',
589
- 'sap_chart_volume_profile',
590
- 'sap_perp_trade_plan',
591
- ],
592
- executionStatus: getPerpsConfig(context).builderUrl
593
- ? 'hosted_unsigned_builder_configured'
594
- : 'analysis_only_no_hosted_builder',
595
- note: 'Market data came from the configured hosted perps provider, not heuristic account decoding. Execution still requires a typed unsigned builder and local finalization.',
596
- }, null, 2));
597
- }
598
- const adrenaProgramId = getAdrenaProgramId(context);
599
- // Fetch Custody accounts — these contain per-token price, funding rate, and OI.
600
- // We use memcmp on the first 8 bytes (Anchor account discriminator).
601
- const custodyAccounts = await context.connection.getProgramAccounts(adrenaProgramId, {
602
- filters: [
603
- { memcmp: { offset: 0, bytes: discToBase58(DISC_CUSTODY) } },
604
- ],
605
- commitment: 'confirmed',
606
- });
607
- // Fetch Pool accounts — these contain pool configuration and token info.
608
- const poolAccounts = await context.connection.getProgramAccounts(adrenaProgramId, {
609
- filters: [
610
- { memcmp: { offset: 0, bytes: discToBase58(DISC_POOL) } },
611
- ],
612
- commitment: 'confirmed',
613
- });
614
- const poolInfoByAddress = new Map();
615
- for (const { pubkey, account } of poolAccounts) {
616
- const decoded = decodeAdrenaPoolAccount(pubkey, account.data);
617
- if (decoded) {
618
- poolInfoByAddress.set(decoded.poolAddress, decoded);
619
- }
620
- }
621
- const markets = custodyAccounts
622
- .map(({ pubkey, account }) => {
623
- const poolAddress = account.data.length >= ADRENA_CUSTODY_OFFSETS.pool + 32
624
- ? readPublicKey(account.data, ADRENA_CUSTODY_OFFSETS.pool)
625
- : '';
626
- return decodeAdrenaCustodyAccount(pubkey, account.data, poolInfoByAddress.get(poolAddress));
627
- })
628
- .filter((market) => Boolean(market));
629
- // Apply market filter if provided.
630
- const filtered = marketFilter
631
- ? markets.filter(m => m.symbol.toUpperCase() === marketFilter || m.market.toUpperCase() === marketFilter)
632
- : markets;
633
- const scannedAccounts = custodyAccounts.length + poolAccounts.length;
634
- const dataAvailability = scannedAccounts === 0
635
- ? {
636
- status: 'rpc_scan_empty',
637
- reason: 'No configured perps provider is set and no Adrena Pool/Custody accounts were returned by getProgramAccounts using Anchor account discriminators. On an indexed/full-history RPC this usually means the program ID or account type discriminators are stale, not that perps do not exist.',
638
- agentAction: 'Do not infer that no perp markets exist and do not retry in a loop. Operator should verify ADRENA_PROGRAM_ID and account discriminators against the current deployed program/IDL, or configure SAP_MCP_PERPS_MARKETS_URL with an IDL-backed provider. Use sap_chart_ohlc/sap_chart_volume_profile for market context, then stop before execution until sap_perp_builder_status says a builder is available.',
639
- }
640
- : filtered.length === 0
641
- ? {
642
- status: 'decoded_no_matching_markets',
643
- reason: 'The configured RPC returned Adrena Pool/Custody accounts, but no decoded market matched the requested filter. This is usually a market-filter issue.',
644
- agentAction: 'Retry once with an empty market filter to inspect all decoded symbols. If markets decode but markPrice is null, use oracle/chart tools for live price and do not pretend custody contains a spot mark price.',
645
- }
646
- : {
647
- status: 'available',
648
- reason: 'Adrena account scan returned on-chain accounts from the configured RPC.',
649
- agentAction: 'Use custodyAddress and poolAddress only for analysis. Call sap_perp_builder_status before any execution attempt.',
650
- };
651
- return createTextResponse(JSON.stringify({
652
- source: 'on-chain-rpc',
653
- markets: filtered,
654
- count: filtered.length,
655
- totalCustodies: custodyAccounts.length,
656
- totalPools: poolAccounts.length,
657
- scan: {
658
- programId: adrenaProgramId.toBase58(),
659
- custodyDiscriminator: discToBase58(DISC_CUSTODY),
660
- poolDiscriminator: discToBase58(DISC_POOL),
661
- },
662
- dataAvailability,
663
- recommendedNextTools: [
664
- 'sap_perp_builder_status',
665
- 'sap_chart_ohlc',
666
- 'sap_chart_volume_profile',
667
- 'sap_perp_trade_plan',
668
- ],
669
- executionStatus: getPerpsConfig(context).builderUrl
670
- ? 'hosted_unsigned_builder_configured'
671
- : 'analysis_only_no_hosted_builder',
672
- priceNote: 'Adrena Custody accounts expose market configuration, OI, funding/accounting, and oracle identifiers. They do not expose a ready-to-use mark price. markPrice is null until SAP MCP adds a typed oracle/feed decoder or configured Adrena data runtime.',
673
- note: 'Data read directly from Solana on-chain Pool/Custody accounts using the official Adrena release/39 ABI layout. Use custodyAddress and poolAddress for analysis only. Do not execute perps unless SAP MCP exposes a typed unsigned builder or a local signer tool for that exact action.',
674
- }));
675
- }
676
- catch (err) {
677
- return createTextResponse(JSON.stringify({
678
- error: 'Failed to fetch perp markets from Solana RPC',
679
- message: err instanceof Error ? err.message : 'Unknown error',
680
- }), { isError: true });
681
- }
682
- });
683
- }
684
- /* ═══════════════════════════════════════════════════════════════════
685
- * Tool 2: sap_perp_position_info
686
- * ═══════════════════════════════════════════════════════════════════ */
687
- /**
688
- * @name registerPerpPositionInfoTool
689
- * @description Register the sap_perp_position_info read-only tool.
690
- *
691
- * Reads on-chain Adrena position accounts for a given wallet using
692
- * `getProgramAccounts` with a memcmp filter on the owner field.
693
- *
694
- * @param server — MCP server instance.
695
- * @param context — Runtime context with Solana RPC connection.
696
- *
697
- * @internal
698
- */
699
- function registerPerpPositionInfoTool(server, context) {
700
- const schema = {
701
- type: 'object',
702
- properties: {
703
- wallet: {
704
- type: 'string',
705
- description: 'Wallet public key (base58) to read positions for.',
706
- },
707
- },
708
- required: ['wallet'],
709
- additionalProperties: false,
710
- };
711
- registerTool(server, 'sap_perp_position_info', {
712
- description: 'Read all open perpetual positions on Adrena for a given wallet address. Returns position key, market, side, size, collateral, entry price, leverage, unrealized PnL, and liquidation price from the on-chain Position account. markPrice mirrors entryPrice until SAP MCP has a live oracle/feed price decoder. Read-only — uses Solana RPC.',
713
- inputSchema: schema,
714
- }, async (args) => {
715
- const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
716
- if (!walletStr) {
717
- return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
718
- }
719
- let walletPubkey;
720
- try {
721
- walletPubkey = new PublicKey(walletStr);
722
- }
723
- catch {
724
- return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
725
- }
726
- try {
727
- const accounts = await context.connection.getProgramAccounts(getAdrenaProgramId(context), {
728
- filters: [
729
- { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
730
- { memcmp: { offset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET, bytes: walletPubkey.toBase58() } },
731
- ],
732
- commitment: 'confirmed',
733
- });
734
- const marketsByCustody = await readAdrenaMarketsByCustody(context);
735
- const positions = accounts
736
- .map(({ pubkey, account }) => decodeAdrenaPositionAccount(pubkey, account.data, marketsByCustody))
737
- .filter((position) => Boolean(position))
738
- .map((position) => ({
739
- positionKey: position.positionKey,
740
- market: position.market,
741
- side: position.side,
742
- size: position.size,
743
- collateral: position.collateral,
744
- entryPrice: position.entryPrice,
745
- markPrice: position.entryPrice,
746
- leverage: position.leverage,
747
- unrealizedPnl: position.unrealizedPnl,
748
- liquidationPrice: position.liquidationPrice,
749
- }));
750
- return createTextResponse(JSON.stringify({
751
- wallet: walletStr,
752
- positions,
753
- count: positions.length,
754
- scan: {
755
- programId: getAdrenaProgramId(context).toBase58(),
756
- positionDiscriminator: discToBase58(DISC_POSITION),
757
- ownerMemcmpOffset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET,
758
- },
759
- note: 'Positions are decoded from the official Adrena release/39 Position account layout. markPrice mirrors entryPrice until a live oracle/feed price decoder is available.',
760
- }));
761
- }
762
- catch (err) {
763
- return createTextResponse(JSON.stringify({
764
- error: 'Failed to read on-chain positions',
765
- message: err instanceof Error ? err.message : 'Unknown error',
766
- }), { isError: true });
767
- }
768
- });
769
- }
770
- /* ═══════════════════════════════════════════════════════════════════
771
- * Tool 3: sap_perp_funding_history
772
- * ═══════════════════════════════════════════════════════════════════ */
773
- /**
774
- * @name registerPerpFundingHistoryTool
775
- * @description Register the sap_perp_funding_history read-only tool.
776
- *
777
- * Computes the current funding rate from on-chain Custody account data.
778
- * The agent obtains the custody address from `sap_perp_markets` and passes
779
- * it here. Historical funding rate snapshots are not available on-chain
780
- * (only the current rate embedded in the custody account); the tool returns
781
- * the current funding state and a clear error if the custody address is
782
- * missing or invalid.
783
- *
784
- * @param server — MCP server instance.
785
- * @param context — Runtime context with Solana RPC connection.
786
- *
787
- * @internal
788
- */
789
- function registerPerpFundingHistoryTool(server, context) {
790
- const schema = {
791
- type: 'object',
792
- properties: {
793
- custodyAddress: {
794
- type: 'string',
795
- description: 'Custody account public key (base58). Obtain from sap_perp_markets output — the custodyAddress field.',
796
- },
797
- market: {
798
- type: 'string',
799
- description: 'Optional market symbol (e.g. "SOL", "BTC") for display purposes.',
800
- },
801
- limit: {
802
- type: 'number',
803
- description: 'Maximum number of funding records to return (default 100). On-chain mode returns the current funding snapshot only.',
804
- minimum: 1,
805
- maximum: 1000,
806
- },
807
- },
808
- required: ['custodyAddress'],
809
- additionalProperties: false,
810
- };
811
- registerTool(server, 'sap_perp_funding_history', {
812
- description: 'Compute the current funding rate for an Adrena perpetual market from on-chain Custody account data. Pass the custodyAddress from sap_perp_markets. Returns current funding rate, cumulative funding, and open interest. Read-only — reads Custody account directly from Solana RPC (on-chain). Note: on-chain data provides the current funding snapshot only, not historical time-series.',
813
- inputSchema: schema,
814
- }, async (args) => {
815
- const custodyAddressStr = typeof args['custodyAddress'] === 'string' ? args['custodyAddress'] : '';
816
- const market = typeof args['market'] === 'string' ? args['market'] : '';
817
- if (!custodyAddressStr) {
818
- return createTextResponse(JSON.stringify({
819
- error: 'custodyAddress is required. Use sap_perp_markets to get the custody address for the desired market.',
820
- }), { isError: true });
821
- }
822
- let custodyPubkey;
823
- try {
824
- custodyPubkey = new PublicKey(custodyAddressStr);
825
- }
826
- catch {
827
- return createTextResponse(JSON.stringify({
828
- error: 'Invalid custody address',
829
- custodyAddress: custodyAddressStr,
830
- }), { isError: true });
831
- }
832
- try {
833
- const accountInfo = await context.connection.getAccountInfo(custodyPubkey, 'confirmed');
834
- if (!accountInfo) {
835
- return createTextResponse(JSON.stringify({
836
- error: 'Custody account not found on-chain',
837
- custodyAddress: custodyAddressStr,
838
- message: 'The custody account does not exist or has been closed. Use sap_perp_markets to find valid custody addresses.',
839
- }), { isError: true });
840
- }
841
- const data = accountInfo.data;
842
- if (data.length < 8 + 32 + 32) {
843
- return createTextResponse(JSON.stringify({
844
- error: 'Account data too short to be a valid Custody account',
845
- custodyAddress: custodyAddressStr,
846
- dataLength: data.length,
847
- }), { isError: true });
848
- }
849
- // Verify discriminator matches Custody.
850
- const disc = data.subarray(0, 8);
851
- if (!disc.equals(DISC_CUSTODY)) {
852
- return createTextResponse(JSON.stringify({
853
- error: 'Account discriminator does not match Custody type',
854
- custodyAddress: custodyAddressStr,
855
- expectedDisc: Array.from(DISC_CUSTODY),
856
- actualDisc: Array.from(disc),
857
- message: 'The provided address is not a Custody account. Use sap_perp_markets to get the correct custodyAddress.',
858
- }), { isError: true });
859
- }
860
- const poolAddress = readPublicKey(data, ADRENA_CUSTODY_OFFSETS.pool);
861
- const poolsByAddress = new Map();
862
- const poolInfo = await context.connection.getAccountInfo(new PublicKey(poolAddress), 'confirmed');
863
- if (poolInfo) {
864
- const decodedPool = decodeAdrenaPoolAccount(new PublicKey(poolAddress), poolInfo.data);
865
- if (decodedPool)
866
- poolsByAddress.set(poolAddress, decodedPool);
867
- }
868
- const decoded = decodeAdrenaCustodyAccount(custodyPubkey, data, poolsByAddress.get(poolAddress));
869
- if (!decoded) {
870
- return createTextResponse(JSON.stringify({
871
- error: 'Unable to decode Adrena Custody account with release/39 ABI layout',
872
- custodyAddress: custodyAddressStr,
873
- }), { isError: true });
874
- }
875
- const timestamp = Date.now();
876
- return createTextResponse(JSON.stringify({
877
- source: 'on-chain-rpc',
878
- custodyAddress: custodyAddressStr,
879
- market: market || decoded.market,
880
- symbol: decoded.symbol,
881
- poolAddress: decoded.poolAddress,
882
- custodyMint: decoded.custodyMint,
883
- oracle: decoded.oracle,
884
- tradeOracle: decoded.tradeOracle,
885
- currentFunding: {
886
- timestamp,
887
- fundingRateRaw: decoded.funding.currentRateLongToShortRaw,
888
- cumulativeLongToShortRaw: decoded.funding.cumulativeLongToShortRaw,
889
- cumulativeShortToLongRaw: decoded.funding.cumulativeShortToLongRaw,
890
- maxHourlyFundingRateRaw: decoded.funding.maxHourlyFundingRateRaw,
891
- fundingLastUpdate: decoded.funding.lastUpdate,
892
- markPrice: decoded.markPrice,
893
- markPriceSource: decoded.markPriceSource,
894
- openInterestLong: decoded.openInterestLong,
895
- openInterestShort: decoded.openInterestShort,
896
- },
897
- records: [{
898
- timestamp,
899
- fundingRateRaw: decoded.funding.currentRateLongToShortRaw,
900
- cumulativeLongToShortRaw: decoded.funding.cumulativeLongToShortRaw,
901
- cumulativeShortToLongRaw: decoded.funding.cumulativeShortToLongRaw,
902
- }],
903
- count: 1,
904
- note: 'On-chain Adrena Custody data provides the current funding/accounting snapshot only. Historical time-series funding data is not available via Solana RPC; use repeated snapshots or an indexed Adrena data runtime for history.',
905
- }));
906
- }
907
- catch (err) {
908
- return createTextResponse(JSON.stringify({
909
- error: 'Failed to read funding data from on-chain custody account',
910
- custodyAddress: custodyAddressStr,
911
- message: err instanceof Error ? err.message : 'Unknown error',
912
- }), { isError: true });
913
- }
914
- });
915
- }
916
- /* ═══════════════════════════════════════════════════════════════════
917
- * Tool 4: sap_chart_ohlc
918
- * ═══════════════════════════════════════════════════════════════════ */
919
- /**
920
- * @name registerChartOhlcTool
921
- * @description Register the sap_chart_ohlc read-only tool.
922
- *
923
- * Fetches OHLC candlestick data for any Solana token from DexScreener's
924
- * free REST API.
925
- *
926
- * @param server — MCP server instance.
927
- * @param context — Runtime context (unused — pure REST call).
928
- *
929
- * @internal
930
- */
931
- function registerChartOhlcTool(server, _context) {
932
- const schema = {
933
- type: 'object',
934
- properties: {
935
- mint: {
936
- type: 'string',
937
- description: 'Token mint address (base58) to fetch OHLC data for.',
938
- },
939
- resolution: {
940
- type: 'string',
941
- description: 'Chart resolution: "5m", "1h", "6h", "24h" (default "1h").',
942
- enum: ['5m', '1h', '6h', '24h'],
943
- },
944
- },
945
- required: ['mint'],
946
- additionalProperties: false,
947
- };
948
- registerTool(server, 'sap_chart_ohlc', {
949
- description: 'Fetch OHLC candlestick data for any Solana token from DexScreener. Returns open, high, low, close, volume, liquidity, and FDV. Read-only — uses free DexScreener REST API.',
950
- inputSchema: schema,
951
- }, async (args) => {
952
- const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
953
- const resolution = typeof args['resolution'] === 'string' ? args['resolution'] : '1h';
954
- if (!mint) {
955
- return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
956
- }
957
- const url = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
958
- const pairs = await timedFetch(url);
959
- if (!pairs || pairs.length === 0) {
960
- return createTextResponse(JSON.stringify({
961
- error: 'No DexScreener pairs found for this mint',
962
- mint,
963
- }), { isError: true });
964
- }
965
- const pair = pairs[0];
966
- const priceUsd = pair.priceUsd ? parseFloat(pair.priceUsd) : 0;
967
- if (priceUsd <= 0) {
968
- return createTextResponse(JSON.stringify({
969
- error: 'No valid price data from DexScreener',
970
- mint,
971
- }), { isError: true });
972
- }
973
- const priceChange = pair.priceChange ?? {};
974
- const volume = pair.volume ?? {};
975
- const liquidity = pair.liquidity?.usd ?? 0;
976
- const fdv = pair.fdv ?? 0;
977
- // Derive OHLC from price change percentages for the requested resolution.
978
- const changeMap = {
979
- '5m': priceChange['m5'] ?? 0,
980
- '1h': priceChange['h1'] ?? 0,
981
- '6h': priceChange['h6'] ?? 0,
982
- '24h': priceChange['h24'] ?? 0,
983
- };
984
- const changePct = changeMap[resolution] ?? changeMap['1h'];
985
- const open = priceUsd / (1 + changePct / 100);
986
- const close = priceUsd;
987
- const high = Math.max(open, close) * 1.001;
988
- const low = Math.min(open, close) * 0.999;
989
- const volMap = {
990
- '5m': (volume['m5'] ?? 0),
991
- '1h': (volume['h1'] ?? 0),
992
- '6h': (volume['h6'] ?? 0),
993
- '24h': (volume['h24'] ?? 0),
994
- };
995
- const vol = volMap[resolution] ?? volMap['1h'];
996
- const candle = {
997
- timestamp: Date.now(),
998
- open,
999
- high,
1000
- low,
1001
- close,
1002
- volume: vol,
1003
- };
1004
- return createTextResponse(JSON.stringify({
1005
- mint,
1006
- symbol: pair.baseToken?.symbol ?? 'unknown',
1007
- resolution,
1008
- candle,
1009
- liquidityUsd: liquidity,
1010
- fdv,
1011
- priceUsd,
1012
- }));
1013
- });
1014
- }
1015
- /* ═══════════════════════════════════════════════════════════════════
1016
- * Tool 5: sap_chart_long_term
1017
- * ═══════════════════════════════════════════════════════════════════ */
1018
- /**
1019
- * @name registerChartLongTermTool
1020
- * @description Register the sap_chart_long_term read-only tool.
1021
- *
1022
- * Fetches long-term price history for a Solana token from DexScreener
1023
- * and optional protocol TVL data from DeFiLlama.
1024
- *
1025
- * @param server — MCP server instance.
1026
- * @param context — Runtime context (unused — pure REST call).
1027
- *
1028
- * @internal
1029
- */
1030
- function registerChartLongTermTool(server, _context) {
1031
- const schema = {
1032
- type: 'object',
1033
- properties: {
1034
- mint: {
1035
- type: 'string',
1036
- description: 'Token mint address (base58) for price history.',
1037
- },
1038
- protocol: {
1039
- type: 'string',
1040
- description: 'Optional DeFiLlama protocol slug for TVL history (e.g. "jupiter", "raydium").',
1041
- },
1042
- },
1043
- required: ['mint'],
1044
- additionalProperties: false,
1045
- };
1046
- registerTool(server, 'sap_chart_long_term', {
1047
- description: 'Fetch long-term price history for a Solana token from DexScreener and optional protocol TVL data from DeFiLlama. Returns price changes, volume, liquidity, FDV, and TVL history. Read-only — uses free REST APIs.',
1048
- inputSchema: schema,
1049
- }, async (args) => {
1050
- const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
1051
- const protocol = typeof args['protocol'] === 'string' ? args['protocol'] : '';
1052
- if (!mint) {
1053
- return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
1054
- }
1055
- // Fetch token price data from DexScreener.
1056
- const dexUrl = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
1057
- const pairs = await timedFetch(dexUrl);
1058
- let priceData = {};
1059
- if (pairs && pairs.length > 0) {
1060
- const pair = pairs[0];
1061
- priceData = {
1062
- symbol: pair.baseToken?.symbol ?? 'unknown',
1063
- priceUsd: pair.priceUsd ?? '0',
1064
- priceChange: pair.priceChange ?? {},
1065
- volume: pair.volume ?? {},
1066
- liquidityUsd: pair.liquidity?.usd ?? 0,
1067
- fdv: pair.fdv ?? 0,
1068
- };
1069
- }
1070
- // Fetch protocol TVL from DeFiLlama if requested.
1071
- let tvlData = {};
1072
- if (protocol) {
1073
- const llamaUrl = `${DEFILAMA_API_URL}/protocol/${encodeURIComponent(protocol)}`;
1074
- const protoData = await timedFetch(llamaUrl);
1075
- if (protoData) {
1076
- tvlData = {
1077
- name: protoData.name,
1078
- currentTvl: protoData.tvl,
1079
- change1d: protoData.change_1d,
1080
- change7d: protoData.change_7d,
1081
- marketcap: protoData.marketcap,
1082
- chain: protoData.chain,
1083
- };
1084
- }
1085
- }
1086
- if (Object.keys(priceData).length === 0 && Object.keys(tvlData).length === 0) {
1087
- return createTextResponse(JSON.stringify({
1088
- error: 'No data available from DexScreener or DeFiLlama',
1089
- mint,
1090
- protocol,
1091
- }), { isError: true });
1092
- }
1093
- return createTextResponse(JSON.stringify({
1094
- mint,
1095
- token: priceData,
1096
- protocolTvl: tvlData,
1097
- }));
1098
- });
1099
- }
1100
- /* ═══════════════════════════════════════════════════════════════════
1101
- * Tool 6: sap_chart_volume_profile
1102
- * ═══════════════════════════════════════════════════════════════════ */
1103
- /**
1104
- * @name computeVolumeProfile
1105
- * @description Compute volume profile (POC, VAH, VAL) from price and volume arrays.
1106
- *
1107
- * Pure TypeScript implementation — no external indicator library.
1108
- *
1109
- * @param prices — Array of price points.
1110
- * @param volumes — Array of volume values (same length as prices).
1111
- * @param bucketCount — Number of price buckets (default 20).
1112
- * @returns Volume profile result with POC, VAH, VAL, and buckets.
1113
- *
1114
- * @internal
1115
- */
1116
- function computeVolumeProfile(prices, volumes, bucketCount = DEFAULT_VP_BUCKETS) {
1117
- if (prices.length === 0 || volumes.length === 0) {
1118
- return { poc: 0, vah: 0, val: 0, buckets: [] };
1119
- }
1120
- const minPrice = Math.min(...prices);
1121
- const maxPrice = Math.max(...prices);
1122
- const range = maxPrice - minPrice;
1123
- if (range === 0) {
1124
- return { poc: minPrice, vah: minPrice, val: minPrice, buckets: [{ priceLow: minPrice, priceHigh: minPrice, volume: volumes.reduce((a, b) => a + b, 0), pctOfTotal: 1 }] };
1125
- }
1126
- const bucketSize = range / bucketCount;
1127
- const buckets = Array.from({ length: bucketCount }, (_, i) => ({
1128
- priceLow: minPrice + i * bucketSize,
1129
- priceHigh: minPrice + (i + 1) * bucketSize,
1130
- volume: 0,
1131
- pctOfTotal: 0,
1132
- }));
1133
- const totalVolume = volumes.reduce((a, b) => a + b, 0);
1134
- if (totalVolume === 0) {
1135
- return { poc: minPrice, vah: maxPrice, val: minPrice, buckets };
1136
- }
1137
- for (let i = 0; i < prices.length; i++) {
1138
- const bucketIdx = Math.min(Math.floor((prices[i] - minPrice) / bucketSize), bucketCount - 1);
1139
- buckets[bucketIdx].volume += volumes[i];
1140
- }
1141
- for (const b of buckets) {
1142
- b.pctOfTotal = b.volume / totalVolume;
1143
- }
1144
- // POC = bucket with highest volume.
1145
- let pocIdx = 0;
1146
- for (let i = 1; i < buckets.length; i++) {
1147
- if (buckets[i].volume > buckets[pocIdx].volume)
1148
- pocIdx = i;
1149
- }
1150
- const poc = (buckets[pocIdx].priceLow + buckets[pocIdx].priceHigh) / 2;
1151
- // Value area: 70% of volume around POC.
1152
- let volAccum = buckets[pocIdx].volume;
1153
- let lowIdx = pocIdx - 1;
1154
- let highIdx = pocIdx + 1;
1155
- while (volAccum / totalVolume < VALUE_AREA_PCT && (lowIdx >= 0 || highIdx < buckets.length)) {
1156
- const lowVol = lowIdx >= 0 ? buckets[lowIdx].volume : 0;
1157
- const highVol = highIdx < buckets.length ? buckets[highIdx].volume : 0;
1158
- if (lowVol >= highVol && lowIdx >= 0) {
1159
- volAccum += lowVol;
1160
- lowIdx--;
1161
- }
1162
- else if (highIdx < buckets.length) {
1163
- volAccum += highVol;
1164
- highIdx++;
1165
- }
1166
- else {
1167
- break;
1168
- }
1169
- }
1170
- const val = buckets[lowIdx + 1]?.priceLow ?? minPrice;
1171
- const vah = buckets[highIdx - 1]?.priceHigh ?? maxPrice;
1172
- return { poc, vah, val, buckets };
1173
- }
1174
- /**
1175
- * @name registerChartVolumeProfileTool
1176
- * @description Register the sap_chart_volume_profile read-only tool.
1177
- *
1178
- * @param server — MCP server instance.
1179
- * @param context — Runtime context (unused — pure REST + local math).
1180
- *
1181
- * @internal
1182
- */
1183
- function registerChartVolumeProfileTool(server, _context) {
1184
- const schema = {
1185
- type: 'object',
1186
- properties: {
1187
- mint: {
1188
- type: 'string',
1189
- description: 'Token mint address (base58) to compute volume profile for.',
1190
- },
1191
- buckets: {
1192
- type: 'number',
1193
- description: 'Number of price buckets (default 20).',
1194
- minimum: 5,
1195
- maximum: 100,
1196
- },
1197
- },
1198
- required: ['mint'],
1199
- additionalProperties: false,
1200
- };
1201
- registerTool(server, 'sap_chart_volume_profile', {
1202
- description: 'Compute volume profile (POC, VAH, VAL) for any Solana token using DexScreener price and volume data. Returns point of control, value area high/low, and bucket distribution. Read-only — uses free DexScreener REST API + local computation.',
1203
- inputSchema: schema,
1204
- }, async (args) => {
1205
- const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
1206
- const bucketCount = typeof args['buckets'] === 'number' ? args['buckets'] : DEFAULT_VP_BUCKETS;
1207
- if (!mint) {
1208
- return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
1209
- }
1210
- // Fetch token data from DexScreener.
1211
- const url = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
1212
- const pairs = await timedFetch(url);
1213
- if (!pairs || pairs.length === 0) {
1214
- return createTextResponse(JSON.stringify({ error: 'No DexScreener data for this mint' }), { isError: true });
1215
- }
1216
- const pair = pairs[0];
1217
- const priceUsd = pair.priceUsd ? parseFloat(pair.priceUsd) : 0;
1218
- if (priceUsd <= 0) {
1219
- return createTextResponse(JSON.stringify({ error: 'No valid price data' }), { isError: true });
1220
- }
1221
- // Derive price points from price change percentages.
1222
- const pc = pair.priceChange ?? {};
1223
- const vol = pair.volume ?? {};
1224
- const prices = [
1225
- priceUsd / (1 + (pc['m5'] ?? 0) / 100),
1226
- priceUsd / (1 + (pc['h1'] ?? 0) / 100),
1227
- priceUsd / (1 + (pc['h6'] ?? 0) / 100),
1228
- priceUsd / (1 + (pc['h24'] ?? 0) / 100),
1229
- priceUsd,
1230
- ];
1231
- const volumes = [
1232
- vol['m5'] ?? 0,
1233
- vol['h1'] ?? 0,
1234
- vol['h6'] ?? 0,
1235
- vol['h24'] ?? 0,
1236
- vol['h24'] ?? 0,
1237
- ];
1238
- const profile = computeVolumeProfile(prices, volumes, bucketCount);
1239
- return createTextResponse(JSON.stringify({
1240
- mint,
1241
- symbol: pair.baseToken?.symbol ?? 'unknown',
1242
- priceUsd,
1243
- volumeProfile: profile,
1244
- }));
1245
- });
1246
- }
1247
- /* ═══════════════════════════════════════════════════════════════════
1248
- * Tool 7: sap_perp_liquidation_zones
1249
- * ═══════════════════════════════════════════════════════════════════ */
1250
- /**
1251
- * @name registerPerpLiquidationZonesTool
1252
- * @description Register the sap_perp_liquidation_zones read-only tool.
1253
- *
1254
- * Reads on-chain Adrena positions for a wallet and computes liquidation
1255
- * zones from account geometry. Live current price requires a separate
1256
- * oracle/feed decoder.
1257
- *
1258
- * @param server — MCP server instance.
1259
- * @param context — Runtime context with Solana RPC connection.
1260
- *
1261
- * @internal
1262
- */
1263
- function registerPerpLiquidationZonesTool(server, context) {
1264
- const schema = {
1265
- type: 'object',
1266
- properties: {
1267
- wallet: {
1268
- type: 'string',
1269
- description: 'Wallet public key (base58) to compute liquidation zones for.',
1270
- },
1271
- },
1272
- required: ['wallet'],
1273
- additionalProperties: false,
1274
- };
1275
- registerTool(server, 'sap_perp_liquidation_zones', {
1276
- description: 'Compute liquidation zones for all open Adrena perp positions of a wallet. Reads on-chain positions via Solana RPC and calculates liquidation geometry from entry price, leverage, and liquidation price. currentPrice mirrors entryPrice until SAP MCP has a live oracle/feed price decoder. Read-only.',
1277
- inputSchema: schema,
1278
- }, async (args) => {
1279
- const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
1280
- if (!walletStr) {
1281
- return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
1282
- }
1283
- let walletPubkey;
1284
- try {
1285
- walletPubkey = new PublicKey(walletStr);
1286
- }
1287
- catch {
1288
- return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
1289
- }
1290
- try {
1291
- const accounts = await context.connection.getProgramAccounts(getAdrenaProgramId(context), {
1292
- filters: [
1293
- { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
1294
- { memcmp: { offset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET, bytes: walletPubkey.toBase58() } },
1295
- ],
1296
- commitment: 'confirmed',
1297
- });
1298
- const marketsByCustody = await readAdrenaMarketsByCustody(context);
1299
- const zones = accounts
1300
- .map(({ pubkey, account }) => decodeAdrenaPositionAccount(pubkey, account.data, marketsByCustody))
1301
- .filter((position) => Boolean(position))
1302
- .map((position) => {
1303
- const currentPrice = position.entryPrice;
1304
- const distanceToLiquidationPct = position.liquidationPrice > 0 && currentPrice > 0
1305
- ? Math.abs((currentPrice - position.liquidationPrice) / currentPrice) * 100
1306
- : 0;
1307
- return {
1308
- positionKey: position.positionKey,
1309
- market: position.market,
1310
- side: position.side,
1311
- liquidationPrice: position.liquidationPrice,
1312
- currentPrice,
1313
- distanceToLiquidationPct,
1314
- leverage: position.leverage,
1315
- };
1316
- });
1317
- return createTextResponse(JSON.stringify({
1318
- wallet: walletStr,
1319
- zones,
1320
- count: zones.length,
1321
- scan: {
1322
- programId: getAdrenaProgramId(context).toBase58(),
1323
- positionDiscriminator: discToBase58(DISC_POSITION),
1324
- ownerMemcmpOffset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET,
1325
- },
1326
- note: 'Liquidation zones use entryPrice as currentPrice until SAP MCP has a live Adrena oracle/feed price decoder. Treat the output as risk geometry, not a liquidation alert feed.',
1327
- }));
1328
- }
1329
- catch (err) {
1330
- return createTextResponse(JSON.stringify({
1331
- error: 'Failed to read on-chain positions for liquidation zones',
1332
- message: err instanceof Error ? err.message : 'Unknown error',
1333
- }), { isError: true });
1334
- }
1335
- });
1336
- }
1337
- /* ═══════════════════════════════════════════════════════════════════
1338
- * Tool 8: sap_perp_trade_plan
1339
- * ═══════════════════════════════════════════════════════════════════ */
1340
- /**
1341
- * @name registerPerpTradePlanTool
1342
- * @description Register the professional perps planning tool.
1343
- *
1344
- * This tool does not build a transaction. It turns an intent into a compact
1345
- * risk, sizing, and execution checklist so agents can act like traders while
1346
- * avoiding fake or incomplete unsigned transaction builders.
1347
- *
1348
- * @param server — MCP server instance.
1349
- *
1350
- * @internal
1351
- */
1352
- function registerPerpTradePlanTool(server) {
1353
- const schema = {
1354
- type: 'object',
1355
- properties: {
1356
- market: {
1357
- type: 'string',
1358
- description: 'Perp market symbol or pair, for example SOL-PERP, BTC-PERP, or ETH-PERP.',
1359
- },
1360
- side: {
1361
- type: 'string',
1362
- description: 'Intended direction for the trade.',
1363
- enum: ['long', 'short'],
1364
- },
1365
- collateralAmountUsd: {
1366
- type: 'number',
1367
- description: 'Collateral to allocate in USD. This is the margin budget, not notional size.',
1368
- minimum: 0,
1369
- },
1370
- leverage: {
1371
- type: 'number',
1372
- description: 'Requested leverage multiplier. Keep conservative unless user policy explicitly allows more.',
1373
- minimum: 1,
1374
- maximum: 100,
1375
- },
1376
- entryPrice: {
1377
- type: 'number',
1378
- description: 'Reference entry price in USD used for risk math.',
1379
- minimum: 0,
1380
- },
1381
- stopLossPrice: {
1382
- type: 'number',
1383
- description: 'Optional stop loss price in USD. Strongly recommended before execution.',
1384
- minimum: 0,
1385
- },
1386
- takeProfitPrice: {
1387
- type: 'number',
1388
- description: 'Optional take profit price in USD used to compute reward/risk.',
1389
- minimum: 0,
1390
- },
1391
- maxAccountRiskPct: {
1392
- type: 'number',
1393
- description: 'Maximum account risk percentage allowed by local policy. Default 1%.',
1394
- minimum: 0,
1395
- maximum: 100,
1396
- },
1397
- maxSlippageBps: {
1398
- type: 'number',
1399
- description: 'Maximum execution slippage in basis points. Default 50 bps.',
1400
- minimum: 0,
1401
- maximum: 10_000,
1402
- },
1403
- timeframe: {
1404
- type: 'string',
1405
- description: 'Trading horizon such as scalp, intraday, swing, or hedge.',
1406
- },
1407
- notes: {
1408
- type: 'string',
1409
- description: 'Optional user notes, catalyst, invalidation thesis, or strategy context.',
1410
- },
1411
- },
1412
- required: ['market', 'side', 'collateralAmountUsd', 'leverage', 'entryPrice'],
1413
- additionalProperties: false,
1414
- };
1415
- registerTool(server, 'sap_perp_trade_plan', {
1416
- description: 'Create a trader-grade perpetual futures plan from a simple intent. Returns notional size, stop risk, reward/risk, liquidation estimate, preflight checklist, and the exact SAP MCP read tools to call next. This is analysis-only: SAP MCP does not expose Adrena execution builders until they are IDL-backed and locally finalizable.',
1417
- inputSchema: schema,
1418
- }, async (args) => {
1419
- const market = String(args['market'] ?? '').trim().toUpperCase();
1420
- const side = args['side'] === 'short' ? 'short' : 'long';
1421
- const collateralAmountUsd = typeof args['collateralAmountUsd'] === 'number' ? args['collateralAmountUsd'] : 0;
1422
- const leverage = typeof args['leverage'] === 'number' ? args['leverage'] : 1;
1423
- const entryPrice = typeof args['entryPrice'] === 'number' ? args['entryPrice'] : 0;
1424
- const stopLossPrice = typeof args['stopLossPrice'] === 'number' ? args['stopLossPrice'] : null;
1425
- const takeProfitPrice = typeof args['takeProfitPrice'] === 'number' ? args['takeProfitPrice'] : null;
1426
- const maxAccountRiskPct = typeof args['maxAccountRiskPct'] === 'number' ? args['maxAccountRiskPct'] : 1;
1427
- const maxSlippageBps = typeof args['maxSlippageBps'] === 'number' ? args['maxSlippageBps'] : 50;
1428
- if (!market || collateralAmountUsd <= 0 || leverage <= 0 || entryPrice <= 0) {
1429
- return createTextResponse(JSON.stringify({
1430
- error: 'market, collateralAmountUsd, leverage, and entryPrice are required and must be positive.',
1431
- }), { isError: true });
1432
- }
1433
- const notionalUsd = collateralAmountUsd * leverage;
1434
- const stopMovePct = stopLossPrice && stopLossPrice > 0
1435
- ? Math.abs((entryPrice - stopLossPrice) / entryPrice) * 100
1436
- : null;
1437
- const takeProfitMovePct = takeProfitPrice && takeProfitPrice > 0
1438
- ? Math.abs((takeProfitPrice - entryPrice) / entryPrice) * 100
1439
- : null;
1440
- const estimatedStopRiskUsd = stopMovePct === null ? null : notionalUsd * (stopMovePct / 100);
1441
- const estimatedRewardUsd = takeProfitMovePct === null ? null : notionalUsd * (takeProfitMovePct / 100);
1442
- const rewardRisk = estimatedStopRiskUsd && estimatedRewardUsd
1443
- ? estimatedRewardUsd / estimatedStopRiskUsd
1444
- : null;
1445
- const liquidationEstimate = side === 'long'
1446
- ? entryPrice * (1 - (1 / leverage))
1447
- : entryPrice * (1 + (1 / leverage));
1448
- const riskFlags = [];
1449
- if (!stopLossPrice)
1450
- riskFlags.push('missing_stop_loss');
1451
- if (estimatedStopRiskUsd !== null && estimatedStopRiskUsd > collateralAmountUsd * (maxAccountRiskPct / 100)) {
1452
- riskFlags.push('stop_risk_exceeds_policy');
1453
- }
1454
- if (leverage > 10)
1455
- riskFlags.push('high_leverage_requires_explicit_user_confirmation');
1456
- if (maxSlippageBps > 100)
1457
- riskFlags.push('slippage_above_1_percent');
1458
- return createTextResponse(JSON.stringify({
1459
- market,
1460
- side,
1461
- executionStatus: 'analysis_only',
1462
- collateralAmountUsd,
1463
- leverage,
1464
- notionalUsd,
1465
- entryPrice,
1466
- stopLossPrice,
1467
- takeProfitPrice,
1468
- maxAccountRiskPct,
1469
- maxSlippageBps,
1470
- liquidationEstimate,
1471
- estimatedStopRiskUsd,
1472
- estimatedRewardUsd,
1473
- rewardRisk,
1474
- riskFlags,
1475
- professionalChecklist: [
1476
- 'Call sap_perp_markets for current market/custody data before execution.',
1477
- 'Call sap_chart_ohlc and sap_chart_volume_profile to validate trend, liquidity, POC, VAH, and VAL.',
1478
- 'Call sap_perp_position_info and sap_perp_liquidation_zones for the user wallet before increasing exposure.',
1479
- 'Show the user one compact preview: side, notional, margin, leverage, stop, take profit, liquidation estimate, slippage, and risk flags.',
1480
- 'Use native Adrena UI/SDK or a future SAP MCP IDL-backed builder for execution. Do not create temporary signing scripts or hand-roll Adrena transactions.',
1481
- ],
1482
- recommendedReadTools: [
1483
- 'sap_perp_markets',
1484
- 'sap_chart_ohlc',
1485
- 'sap_chart_volume_profile',
1486
- 'sap_perp_position_info',
1487
- 'sap_perp_liquidation_zones',
1488
- ],
1489
- executionWarning: 'SAP MCP intentionally does not expose manual Adrena execution builders. Execution must use a complete IDL-backed route before local finalization. If a hosted direct Adrena signer tool returns hosted_local_signer_required, do not route it through sap_payments_call_paid_tool; no x402 fee should be charged and there is no unsigned transaction to finalize.',
1490
- notes: typeof args['notes'] === 'string' ? args['notes'] : undefined,
1491
- }, null, 2));
1492
- });
1493
- }
1494
- /* ═══════════════════════════════════════════════════════════════════
1495
- * Tool 9: sap_perp_builder_status
1496
- * ═══════════════════════════════════════════════════════════════════ */
1497
- function registerPerpBuilderStatusTool(server, context) {
1498
- const schema = {
1499
- type: 'object',
1500
- properties: {
1501
- venue: {
1502
- type: 'string',
1503
- description: 'Optional perp venue to check, for example "adrena" or "sap-perps-provider".',
1504
- },
1505
- },
1506
- additionalProperties: false,
1507
- };
1508
- registerTool(server, 'sap_perp_builder_status', {
1509
- description: 'Free readiness check for perps execution. Returns whether SAP MCP has a configured hosted unsigned transaction builder for perp orders. If builderAvailable is false, agents must stop before execution and must not route direct signer-only perps tools through x402 paid-call replay.',
1510
- inputSchema: schema,
1511
- }, async (args) => {
1512
- const perps = getPerpsConfig(context);
1513
- const venue = typeof args['venue'] === 'string' ? args['venue'] : 'configured-perps-provider';
1514
- return createTextResponse(JSON.stringify({
1515
- venue,
1516
- marketsProviderConfigured: Boolean(perps.marketsUrl),
1517
- positionsProviderConfigured: Boolean(perps.positionsUrl),
1518
- builderAvailable: Boolean(perps.builderUrl),
1519
- adrenaProgramId: perps.adrenaProgramId,
1520
- rpcScanMode: 'anchor-discriminator-getProgramAccounts',
1521
- scanDiscriminators: {
1522
- pool: discToBase58(DISC_POOL),
1523
- custody: discToBase58(DISC_CUSTODY),
1524
- position: discToBase58(DISC_POSITION),
1525
- },
1526
- builderMode: perps.builderUrl ? 'hosted_unsigned_transaction_builder' : 'not_configured',
1527
- nativeAdrenaDecoder: {
1528
- available: true,
1529
- source: 'official Adrena release/39 ABI layout',
1530
- sdkPackage: 'adrena-sdk@beta (types only, no runtime JS — using vendored IDL with @coral-xyz/anchor instead)',
1531
- },
1532
- nativeAdrenaBuilder: {
1533
- available: true,
1534
- source: 'vendored Adrena IDL (release/39) + @coral-xyz/anchor',
1535
- operations: [
1536
- 'sap_adrena_build_open_long',
1537
- 'sap_adrena_build_open_short',
1538
- 'sap_adrena_build_close_long',
1539
- 'sap_adrena_build_close_short',
1540
- 'sap_adrena_build_set_stop_loss',
1541
- 'sap_adrena_build_set_take_profit',
1542
- 'sap_adrena_build_cancel_stop_loss',
1543
- 'sap_adrena_build_cancel_take_profit',
1544
- 'sap_adrena_build_add_limit_order',
1545
- 'sap_adrena_build_cancel_limit_order',
1546
- 'sap_adrena_build_open_commodity_long',
1547
- 'sap_adrena_build_open_commodity_short',
1548
- 'sap_adrena_build_close_commodity_long',
1549
- 'sap_adrena_build_close_commodity_short',
1550
- 'sap_adrena_build_add_liquidity',
1551
- 'sap_adrena_build_remove_liquidity',
1552
- 'sap_adrena_build_swap',
1553
- 'sap_adrena_build_init_user_staking',
1554
- 'sap_adrena_build_add_liquid_stake',
1555
- 'sap_adrena_build_remove_liquid_stake',
1556
- 'sap_adrena_build_add_locked_stake',
1557
- 'sap_adrena_build_claim_stakes',
1558
- ],
1559
- dataApi: [
1560
- 'sap_adrena_get_positions',
1561
- 'sap_adrena_get_pool_info',
1562
- 'sap_adrena_get_custody_info',
1563
- 'sap_adrena_get_trader_info',
1564
- 'sap_adrena_get_trader_leaderboard',
1565
- 'sap_adrena_get_mutagen',
1566
- 'sap_adrena_get_mutagen_leaderboard',
1567
- 'sap_adrena_get_prices',
1568
- 'sap_adrena_get_trading_prices',
1569
- 'sap_adrena_get_position_status',
1570
- ],
1571
- signerPolicy: 'All builder tools return unsigned base64 transactions. Sign locally via sap_payments_finalize_transaction. SAP MCP never signs user-owned Adrena transactions.',
1572
- },
1573
- signerPolicy: 'Use sap_adrena_build_* tools to construct unsigned transactions, then sign locally with sap_payments_finalize_transaction.',
1574
- paymentPolicy: perps.builderUrl
1575
- ? 'Calling sap_perp_build_order_transaction is a paid builder call; finalization happens locally without exposing keypair bytes.'
1576
- : 'No x402 payment is required to learn that perps execution is unavailable.',
1577
- nextAction: perps.builderUrl
1578
- ? 'Use sap_perp_trade_plan, then sap_perp_build_order_transaction, then sap_payments_finalize_transaction with submit:true after user confirmation.'
1579
- : 'Use sap_perp_trade_plan and chart tools for analysis only. Configure SAP_MCP_PERPS_BUILDER_URL only when the provider returns complete unsigned Solana transactions for local finalization.',
1580
- }, null, 2));
1581
- });
1582
- }
1583
- /* ═══════════════════════════════════════════════════════════════════
1584
- * Optional Tool 10: sap_perp_build_order_transaction
1585
- * ═══════════════════════════════════════════════════════════════════ */
1586
- function registerPerpOrderBuilderTool(server, context) {
1587
- const perps = getPerpsConfig(context);
1588
- if (!perps.builderUrl) {
1589
- return false;
1590
- }
1591
- const schema = {
1592
- type: 'object',
1593
- properties: {
1594
- venue: {
1595
- type: 'string',
1596
- description: 'Perps venue supported by the configured builder, for example "adrena".',
1597
- },
1598
- owner: {
1599
- type: 'string',
1600
- description: 'User wallet public key that will sign and own the perp order/position.',
1601
- },
1602
- market: {
1603
- type: 'string',
1604
- description: 'Market symbol or provider market id, for example SOL-PERP.',
1605
- },
1606
- side: {
1607
- type: 'string',
1608
- description: 'Trade side for the order.',
1609
- enum: ['long', 'short'],
1610
- },
1611
- orderType: {
1612
- type: 'string',
1613
- description: 'Order type supported by the provider. Use "market" unless the provider explicitly supports limit/trigger orders.',
1614
- enum: ['market', 'limit', 'trigger'],
1615
- },
1616
- collateralMint: {
1617
- type: 'string',
1618
- description: 'Collateral token mint. Use USDC mint for USDC-margined perps unless the provider says otherwise.',
1619
- },
1620
- collateralAmount: {
1621
- type: 'string',
1622
- description: 'Collateral amount in smallest token units, for example micro-USDC for USDC.',
1623
- },
1624
- leverage: {
1625
- type: 'number',
1626
- description: 'Requested leverage multiplier after local policy checks.',
1627
- minimum: 1,
1628
- maximum: 100,
1629
- },
1630
- slippageBps: {
1631
- type: 'number',
1632
- description: 'Maximum slippage in basis points. Keep <= 100 unless user explicitly confirms higher risk.',
1633
- minimum: 0,
1634
- maximum: 10_000,
1635
- },
1636
- reduceOnly: {
1637
- type: 'boolean',
1638
- description: 'Set true only for reduce-only close/reduce flows.',
1639
- },
1640
- limitPrice: {
1641
- type: 'number',
1642
- description: 'Optional limit or trigger price in USD when orderType is limit or trigger.',
1643
- minimum: 0,
1644
- },
1645
- clientOrderId: {
1646
- type: 'string',
1647
- description: 'Optional client id for idempotency/audit. Generate once and reuse on retries.',
1648
- },
1649
- },
1650
- required: ['owner', 'market', 'side', 'orderType', 'collateralMint', 'collateralAmount', 'leverage'],
1651
- additionalProperties: false,
1652
- };
1653
- registerTool(server, 'sap_perp_build_order_transaction', {
1654
- description: 'Build an unsigned perps order transaction using the configured hosted perps builder. This tool is registered only when SAP_MCP_PERPS_BUILDER_URL is configured. It must return an unsigned Solana transaction for local finalization; SAP MCP never signs user-owned perps transactions on hosted.',
1655
- inputSchema: schema,
1656
- }, async (args) => {
1657
- const payload = await timedFetch(perps.builderUrl, {
1658
- method: 'POST',
1659
- headers: perpsProviderHeaders(),
1660
- body: JSON.stringify({
1661
- ...args,
1662
- requestedBy: 'sap-mcp-hosted',
1663
- signerPolicy: 'local-finalization-required',
1664
- }),
1665
- timeoutMs: perps.timeoutMs,
1666
- });
1667
- const transactionBase64 = payload?.transactionBase64
1668
- ?? payload?.unsignedTransactionBase64
1669
- ?? payload?.transaction;
1670
- if (!payload || typeof transactionBase64 !== 'string' || transactionBase64.length === 0) {
1671
- return createTextResponse(JSON.stringify({
1672
- error: 'perps_builder_invalid_response',
1673
- message: 'Configured perps builder did not return transactionBase64/unsignedTransactionBase64. Execution stopped before local signing.',
1674
- expectedNextStep: 'Fix SAP_MCP_PERPS_BUILDER_URL provider response. Do not create temporary signing scripts or hand-roll perps transactions.',
1675
- }, null, 2), { isError: true });
1676
- }
1677
- return createTextResponse(JSON.stringify({
1678
- success: true,
1679
- venue: args['venue'] ?? 'configured-perps-provider',
1680
- transactionBase64,
1681
- lastValidBlockHeight: payload.lastValidBlockHeight,
1682
- blockhash: payload.blockhash,
1683
- feePayer: payload.feePayer,
1684
- provider: payload.provider ?? 'configured-perps-builder',
1685
- signerPolicy: 'local-finalization-required',
1686
- nextTool: 'sap_payments_finalize_transaction',
1687
- finalizeArgs: {
1688
- transactionBase64,
1689
- submit: false,
1690
- },
1691
- warnings: payload.warnings,
1692
- }, null, 2));
1693
- });
1694
- return true;
1695
- }
1696
- /* ═══════════════════════════════════════════════════════════════════
1697
- * Main registration function
1698
- * ═══════════════════════════════════════════════════════════════════ */
1699
- /**
1700
- * @name registerPerpTools
1701
- * @description Register perp trading analytics, planning, and optional execution-builder tools.
1702
- *
1703
- * @param server — MCP server instance.
1704
- * @param context — Shared runtime context with SAP client, connection, and config.
1705
- *
1706
- * @usedBy `register-tools.ts`
1707
- */
1708
- export function registerPerpTools(server, context) {
1709
- logger.debug('Registering perp trading analytics and planning tools');
1710
- registerPerpMarketsTool(server, context);
1711
- registerPerpPositionInfoTool(server, context);
1712
- registerPerpFundingHistoryTool(server, context);
1713
- registerChartOhlcTool(server, context);
1714
- registerChartLongTermTool(server, context);
1715
- registerChartVolumeProfileTool(server, context);
1716
- registerPerpLiquidationZonesTool(server, context);
1717
- registerPerpTradePlanTool(server);
1718
- registerPerpBuilderStatusTool(server, context);
1719
- const builderRegistered = registerPerpOrderBuilderTool(server, context);
1720
- logger.debug('Perp tools registered', { count: builderRegistered ? 10 : 9, builderRegistered });
1721
- }
8
+ export { registerPerpTools } from '../perps/perp-analytics-tools.js';
9
+ export { ADRENA_POSITION_OWNER_MEMCMP_OFFSET } from '../perps/perp-constants.js';
10
+ export { decodeAdrenaCustodyAccount, decodeAdrenaPositionAccount, discToBase58, readAdrenaLimitedString } from '../perps/perp-decoders.js';
11
+ export { normalizePerpProviderMarkets } from '../perps/perp-constants.js';
1722
12
  //# sourceMappingURL=perp-tools.js.map