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

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 (165) 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/payments/monetization-gate.d.ts +5 -0
  6. package/dist/payments/monetization-gate.d.ts.map +1 -1
  7. package/dist/payments/monetization-gate.js +39 -2
  8. package/dist/payments/monetization-gate.js.map +1 -1
  9. package/dist/payments/prepaid-credit-store.d.ts +111 -0
  10. package/dist/payments/prepaid-credit-store.d.ts.map +1 -0
  11. package/dist/payments/prepaid-credit-store.js +230 -0
  12. package/dist/payments/prepaid-credit-store.js.map +1 -0
  13. package/dist/payments/pricing.d.ts.map +1 -1
  14. package/dist/payments/pricing.js +1 -0
  15. package/dist/payments/pricing.js.map +1 -1
  16. package/dist/payments/schema-validation.d.ts +52 -0
  17. package/dist/payments/schema-validation.d.ts.map +1 -0
  18. package/dist/payments/schema-validation.js +112 -0
  19. package/dist/payments/schema-validation.js.map +1 -0
  20. package/dist/payments/x402-paid-call.d.ts +21 -0
  21. package/dist/payments/x402-paid-call.d.ts.map +1 -1
  22. package/dist/payments/x402-paid-call.js +108 -3
  23. package/dist/payments/x402-paid-call.js.map +1 -1
  24. package/dist/perps/adrena/adrena-builder-commodity.d.ts +63 -0
  25. package/dist/perps/adrena/adrena-builder-commodity.d.ts.map +1 -0
  26. package/dist/perps/adrena/adrena-builder-commodity.js +200 -0
  27. package/dist/perps/adrena/adrena-builder-commodity.js.map +1 -0
  28. package/dist/perps/adrena/adrena-builder-core.d.ts +319 -0
  29. package/dist/perps/adrena/adrena-builder-core.d.ts.map +1 -0
  30. package/dist/perps/adrena/adrena-builder-core.js +517 -0
  31. package/dist/perps/adrena/adrena-builder-core.js.map +1 -0
  32. package/dist/perps/adrena/adrena-builder-liquidity.d.ts +50 -0
  33. package/dist/perps/adrena/adrena-builder-liquidity.d.ts.map +1 -0
  34. package/dist/perps/adrena/adrena-builder-liquidity.js +196 -0
  35. package/dist/perps/adrena/adrena-builder-liquidity.js.map +1 -0
  36. package/dist/perps/adrena/adrena-builder-staking.d.ts +57 -0
  37. package/dist/perps/adrena/adrena-builder-staking.d.ts.map +1 -0
  38. package/dist/perps/adrena/adrena-builder-staking.js +368 -0
  39. package/dist/perps/adrena/adrena-builder-staking.js.map +1 -0
  40. package/dist/perps/adrena/adrena-builder-trading.d.ts +160 -0
  41. package/dist/perps/adrena/adrena-builder-trading.d.ts.map +1 -0
  42. package/dist/perps/adrena/adrena-builder-trading.js +835 -0
  43. package/dist/perps/adrena/adrena-builder-trading.js.map +1 -0
  44. package/dist/perps/adrena/adrena-builder.d.ts +14 -293
  45. package/dist/perps/adrena/adrena-builder.d.ts.map +1 -1
  46. package/dist/perps/adrena/adrena-builder.js +14 -1140
  47. package/dist/perps/adrena/adrena-builder.js.map +1 -1
  48. package/dist/perps/adrena/adrena-constants.d.ts +33 -5
  49. package/dist/perps/adrena/adrena-constants.d.ts.map +1 -1
  50. package/dist/perps/adrena/adrena-constants.js +29 -7
  51. package/dist/perps/adrena/adrena-constants.js.map +1 -1
  52. package/dist/perps/adrena/adrena-idl.d.ts +15084 -0
  53. package/dist/perps/adrena/adrena-idl.d.ts.map +1 -0
  54. package/dist/perps/adrena/adrena-idl.js +29903 -0
  55. package/dist/perps/adrena/adrena-idl.js.map +1 -0
  56. package/dist/perps/adrena/adrena-pda.d.ts +5 -4
  57. package/dist/perps/adrena/adrena-pda.d.ts.map +1 -1
  58. package/dist/perps/adrena/adrena-pda.js +6 -10
  59. package/dist/perps/adrena/adrena-pda.js.map +1 -1
  60. package/dist/perps/adrena/index.d.ts +1 -0
  61. package/dist/perps/adrena/index.d.ts.map +1 -1
  62. package/dist/perps/adrena/index.js +1 -0
  63. package/dist/perps/adrena/index.js.map +1 -1
  64. package/dist/perps/chart-indicators.d.ts +27 -0
  65. package/dist/perps/chart-indicators.d.ts.map +1 -0
  66. package/dist/perps/chart-indicators.js +345 -0
  67. package/dist/perps/chart-indicators.js.map +1 -0
  68. package/dist/perps/chart-tools.d.ts +45 -0
  69. package/dist/perps/chart-tools.d.ts.map +1 -0
  70. package/dist/perps/chart-tools.js +341 -0
  71. package/dist/perps/chart-tools.js.map +1 -0
  72. package/dist/perps/perp-analytics-tools.d.ts +20 -0
  73. package/dist/perps/perp-analytics-tools.d.ts.map +1 -0
  74. package/dist/perps/perp-analytics-tools.js +868 -0
  75. package/dist/perps/perp-analytics-tools.js.map +1 -0
  76. package/dist/perps/perp-constants.d.ts +362 -0
  77. package/dist/perps/perp-constants.d.ts.map +1 -0
  78. package/dist/perps/perp-constants.js +210 -0
  79. package/dist/perps/perp-constants.js.map +1 -0
  80. package/dist/perps/perp-decoders.d.ts +47 -0
  81. package/dist/perps/perp-decoders.d.ts.map +1 -0
  82. package/dist/perps/perp-decoders.js +332 -0
  83. package/dist/perps/perp-decoders.js.map +1 -0
  84. package/dist/policy/policy-engine.d.ts +42 -0
  85. package/dist/policy/policy-engine.d.ts.map +1 -1
  86. package/dist/policy/policy-engine.js +43 -0
  87. package/dist/policy/policy-engine.js.map +1 -1
  88. package/dist/premium/activation-manager.d.ts.map +1 -1
  89. package/dist/premium/activation-manager.js +20 -2
  90. package/dist/premium/activation-manager.js.map +1 -1
  91. package/dist/prompts/context/sap-agent-context.prompt.js +15 -1
  92. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  93. package/dist/server/server-metadata.d.ts +13 -0
  94. package/dist/server/server-metadata.d.ts.map +1 -1
  95. package/dist/server/server-metadata.js +15 -2
  96. package/dist/server/server-metadata.js.map +1 -1
  97. package/dist/solana/ata-utils.d.ts +39 -0
  98. package/dist/solana/ata-utils.d.ts.map +1 -0
  99. package/dist/solana/ata-utils.js +49 -0
  100. package/dist/solana/ata-utils.js.map +1 -0
  101. package/dist/strategies/trade-journal.d.ts +78 -0
  102. package/dist/strategies/trade-journal.d.ts.map +1 -0
  103. package/dist/strategies/trade-journal.js +92 -0
  104. package/dist/strategies/trade-journal.js.map +1 -0
  105. package/dist/tools/adrena/adrena-commodity-tools.d.ts +15 -0
  106. package/dist/tools/adrena/adrena-commodity-tools.d.ts.map +1 -0
  107. package/dist/tools/adrena/adrena-commodity-tools.js +137 -0
  108. package/dist/tools/adrena/adrena-commodity-tools.js.map +1 -0
  109. package/dist/tools/adrena/adrena-data-tools.d.ts +23 -0
  110. package/dist/tools/adrena/adrena-data-tools.d.ts.map +1 -0
  111. package/dist/tools/adrena/adrena-data-tools.js +437 -0
  112. package/dist/tools/adrena/adrena-data-tools.js.map +1 -0
  113. package/dist/tools/adrena/adrena-helpers.d.ts +46 -0
  114. package/dist/tools/adrena/adrena-helpers.d.ts.map +1 -0
  115. package/dist/tools/adrena/adrena-helpers.js +41 -0
  116. package/dist/tools/adrena/adrena-helpers.js.map +1 -0
  117. package/dist/tools/adrena/adrena-index.d.ts +20 -0
  118. package/dist/tools/adrena/adrena-index.d.ts.map +1 -0
  119. package/dist/tools/adrena/adrena-index.js +75 -0
  120. package/dist/tools/adrena/adrena-index.js.map +1 -0
  121. package/dist/tools/adrena/adrena-limit-order-tools.d.ts +22 -0
  122. package/dist/tools/adrena/adrena-limit-order-tools.d.ts.map +1 -0
  123. package/dist/tools/adrena/adrena-limit-order-tools.js +91 -0
  124. package/dist/tools/adrena/adrena-limit-order-tools.js.map +1 -0
  125. package/dist/tools/adrena/adrena-liquidity-tools.d.ts +15 -0
  126. package/dist/tools/adrena/adrena-liquidity-tools.d.ts.map +1 -0
  127. package/dist/tools/adrena/adrena-liquidity-tools.js +108 -0
  128. package/dist/tools/adrena/adrena-liquidity-tools.js.map +1 -0
  129. package/dist/tools/adrena/adrena-snapshot-tools.d.ts +42 -0
  130. package/dist/tools/adrena/adrena-snapshot-tools.d.ts.map +1 -0
  131. package/dist/tools/adrena/adrena-snapshot-tools.js +124 -0
  132. package/dist/tools/adrena/adrena-snapshot-tools.js.map +1 -0
  133. package/dist/tools/adrena/adrena-staking-tools.d.ts +15 -0
  134. package/dist/tools/adrena/adrena-staking-tools.d.ts.map +1 -0
  135. package/dist/tools/adrena/adrena-staking-tools.js +139 -0
  136. package/dist/tools/adrena/adrena-staking-tools.js.map +1 -0
  137. package/dist/tools/adrena/adrena-trading-tools.d.ts +78 -0
  138. package/dist/tools/adrena/adrena-trading-tools.d.ts.map +1 -0
  139. package/dist/tools/adrena/adrena-trading-tools.js +556 -0
  140. package/dist/tools/adrena/adrena-trading-tools.js.map +1 -0
  141. package/dist/tools/adrena-tools.d.ts +1 -66
  142. package/dist/tools/adrena-tools.d.ts.map +1 -1
  143. package/dist/tools/adrena-tools.js +1 -1061
  144. package/dist/tools/adrena-tools.js.map +1 -1
  145. package/dist/tools/client-sdk-tools.js +1 -1
  146. package/dist/tools/client-sdk-tools.js.map +1 -1
  147. package/dist/tools/memory-tools.d.ts.map +1 -1
  148. package/dist/tools/memory-tools.js +170 -0
  149. package/dist/tools/memory-tools.js.map +1 -1
  150. package/dist/tools/perp-tools.d.ts +6 -174
  151. package/dist/tools/perp-tools.d.ts.map +1 -1
  152. package/dist/tools/perp-tools.js +6 -1716
  153. package/dist/tools/perp-tools.js.map +1 -1
  154. package/dist/tools/premium-tools.d.ts.map +1 -1
  155. package/dist/tools/premium-tools.js +123 -5
  156. package/dist/tools/premium-tools.js.map +1 -1
  157. package/dist/tools/transaction-tools.d.ts +21 -1
  158. package/dist/tools/transaction-tools.d.ts.map +1 -1
  159. package/dist/tools/transaction-tools.js +114 -30
  160. package/dist/tools/transaction-tools.js.map +1 -1
  161. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
  162. package/dist/tools/x402-paid-call-tool.js +288 -0
  163. package/dist/tools/x402-paid-call-tool.js.map +1 -1
  164. package/package.json +3 -2
  165. package/server.json +3 -3
@@ -0,0 +1,868 @@
1
+ /**
2
+ * @name perps/perp-analytics-tools
3
+ * @description Perp analytics tool registrations: markets, position info, funding history,
4
+ * liquidation zones, trade plan, builder status, and optional order builder.
5
+ *
6
+ * @module perps/perp-analytics-tools
7
+ */
8
+ import { PublicKey } from '@solana/web3.js';
9
+ import { createTextResponse } from '../adapters/mcp/tool-response.js';
10
+ import { registerTool } from '../adapters/mcp/sdk-compat.js';
11
+ import { logger } from '../core/logger.js';
12
+ import { ADRENA_CUSTODY_OFFSETS, ADRENA_POSITION_OWNER_MEMCMP_OFFSET, DISC_CUSTODY, DISC_POOL, DISC_POSITION, getPerpsConfig, perpsProviderHeaders, timedFetch, } from './perp-constants.js';
13
+ import { decodeAdrenaCustodyAccount, decodeAdrenaPoolAccount, decodeAdrenaPositionAccount, discToBase58, readAdrenaMarketsByCustody, readPublicKey, fetchConfiguredPerpMarkets, getAdrenaProgramId, } from './perp-decoders.js';
14
+ import { registerChartOhlcTool, registerChartLongTermTool, registerChartVolumeProfileTool, } from './chart-tools.js';
15
+ import { registerChartMultiOhlcTool, registerChartIndicatorsTool } from './chart-indicators.js';
16
+ /* ═══════════════════════════════════════════════════════════════════
17
+ * Tool 1: sap_perp_markets
18
+ * ═══════════════════════════════════════════════════════════════════ */
19
+ /**
20
+ * @name registerPerpMarketsTool
21
+ * @description Register the sap_perp_markets read-only tool.
22
+ *
23
+ * Reads Pool and Custody accounts directly from Solana RPC using
24
+ * `getProgramAccounts` with memcmp discriminator filters. Decodes symbol,
25
+ * price, funding rate, and open interest from raw account data.
26
+ *
27
+ * @param server — MCP server instance.
28
+ * @param context — Runtime context with Solana RPC connection.
29
+ *
30
+ * @internal
31
+ */
32
+ function registerPerpMarketsTool(server, context) {
33
+ const schema = {
34
+ type: 'object',
35
+ properties: {
36
+ market: {
37
+ type: 'string',
38
+ description: 'Optional market symbol filter (e.g. "SOL", "BTC"). Empty = all markets.',
39
+ },
40
+ },
41
+ additionalProperties: false,
42
+ };
43
+ registerTool(server, 'sap_perp_markets', {
44
+ 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.',
45
+ inputSchema: schema,
46
+ }, async (args) => {
47
+ const marketFilter = typeof args['market'] === 'string' ? args['market'].toUpperCase() : '';
48
+ try {
49
+ const providerMarkets = await fetchConfiguredPerpMarkets(context, marketFilter);
50
+ if (providerMarkets) {
51
+ return createTextResponse(JSON.stringify({
52
+ source: providerMarkets.source,
53
+ markets: providerMarkets.markets,
54
+ count: providerMarkets.markets.length,
55
+ timestamp: providerMarkets.timestamp ?? Date.now(),
56
+ dataAvailability: {
57
+ status: providerMarkets.markets.length > 0 ? 'available' : 'empty_from_provider',
58
+ reason: providerMarkets.markets.length > 0
59
+ ? 'Configured perps market provider returned market data.'
60
+ : 'Configured perps market provider responded but returned no matching markets for the requested filter.',
61
+ agentAction: providerMarkets.markets.length > 0
62
+ ? 'Use these records for professional planning. Before execution, call sap_perp_builder_status to verify whether a hosted unsigned builder is configured.'
63
+ : 'Do not infer that the venue has no markets. Try a broader market filter or check provider status before planning execution.',
64
+ },
65
+ recommendedNextTools: [
66
+ 'sap_perp_builder_status',
67
+ 'sap_chart_ohlc',
68
+ 'sap_chart_volume_profile',
69
+ 'sap_perp_trade_plan',
70
+ ],
71
+ executionStatus: getPerpsConfig(context).builderUrl
72
+ ? 'hosted_unsigned_builder_configured'
73
+ : 'analysis_only_no_hosted_builder',
74
+ note: 'Market data came from the configured hosted perps provider, not heuristic account decoding. Execution still requires a typed unsigned builder and local finalization.',
75
+ }, null, 2));
76
+ }
77
+ const adrenaProgramId = getAdrenaProgramId(context);
78
+ // Fetch Custody accounts — these contain per-token price, funding rate, and OI.
79
+ // We use memcmp on the first 8 bytes (Anchor account discriminator).
80
+ const custodyAccounts = await context.connection.getProgramAccounts(adrenaProgramId, {
81
+ filters: [
82
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_CUSTODY) } },
83
+ ],
84
+ commitment: 'confirmed',
85
+ });
86
+ // Fetch Pool accounts — these contain pool configuration and token info.
87
+ const poolAccounts = await context.connection.getProgramAccounts(adrenaProgramId, {
88
+ filters: [
89
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POOL) } },
90
+ ],
91
+ commitment: 'confirmed',
92
+ });
93
+ const poolInfoByAddress = new Map();
94
+ for (const { pubkey, account } of poolAccounts) {
95
+ const decoded = decodeAdrenaPoolAccount(pubkey, account.data);
96
+ if (decoded) {
97
+ poolInfoByAddress.set(decoded.poolAddress, decoded);
98
+ }
99
+ }
100
+ const markets = custodyAccounts
101
+ .map(({ pubkey, account }) => {
102
+ const poolAddress = account.data.length >= ADRENA_CUSTODY_OFFSETS.pool + 32
103
+ ? readPublicKey(account.data, ADRENA_CUSTODY_OFFSETS.pool)
104
+ : '';
105
+ return decodeAdrenaCustodyAccount(pubkey, account.data, poolInfoByAddress.get(poolAddress));
106
+ })
107
+ .filter((market) => Boolean(market));
108
+ // Apply market filter if provided.
109
+ const filtered = marketFilter
110
+ ? markets.filter(m => m.symbol.toUpperCase() === marketFilter || m.market.toUpperCase() === marketFilter)
111
+ : markets;
112
+ const scannedAccounts = custodyAccounts.length + poolAccounts.length;
113
+ const dataAvailability = scannedAccounts === 0
114
+ ? {
115
+ status: 'rpc_scan_empty',
116
+ 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.',
117
+ 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.',
118
+ }
119
+ : filtered.length === 0
120
+ ? {
121
+ status: 'decoded_no_matching_markets',
122
+ reason: 'The configured RPC returned Adrena Pool/Custody accounts, but no decoded market matched the requested filter. This is usually a market-filter issue.',
123
+ 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.',
124
+ }
125
+ : {
126
+ status: 'available',
127
+ reason: 'Adrena account scan returned on-chain accounts from the configured RPC.',
128
+ agentAction: 'Use custodyAddress and poolAddress only for analysis. Call sap_perp_builder_status before any execution attempt.',
129
+ };
130
+ return createTextResponse(JSON.stringify({
131
+ source: 'on-chain-rpc',
132
+ markets: filtered,
133
+ count: filtered.length,
134
+ totalCustodies: custodyAccounts.length,
135
+ totalPools: poolAccounts.length,
136
+ scan: {
137
+ programId: adrenaProgramId.toBase58(),
138
+ custodyDiscriminator: discToBase58(DISC_CUSTODY),
139
+ poolDiscriminator: discToBase58(DISC_POOL),
140
+ },
141
+ dataAvailability,
142
+ recommendedNextTools: [
143
+ 'sap_perp_builder_status',
144
+ 'sap_chart_ohlc',
145
+ 'sap_chart_volume_profile',
146
+ 'sap_perp_trade_plan',
147
+ ],
148
+ executionStatus: getPerpsConfig(context).builderUrl
149
+ ? 'hosted_unsigned_builder_configured'
150
+ : 'analysis_only_no_hosted_builder',
151
+ 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.',
152
+ 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.',
153
+ }));
154
+ }
155
+ catch (err) {
156
+ return createTextResponse(JSON.stringify({
157
+ error: 'Failed to fetch perp markets from Solana RPC',
158
+ message: err instanceof Error ? err.message : 'Unknown error',
159
+ }), { isError: true });
160
+ }
161
+ });
162
+ }
163
+ /* ═══════════════════════════════════════════════════════════════════
164
+ * Tool 2: sap_perp_position_info
165
+ * ═══════════════════════════════════════════════════════════════════ */
166
+ /**
167
+ * @name registerPerpPositionInfoTool
168
+ * @description Register the sap_perp_position_info read-only tool.
169
+ *
170
+ * Reads on-chain Adrena position accounts for a given wallet using
171
+ * `getProgramAccounts` with a memcmp filter on the owner field.
172
+ *
173
+ * @param server — MCP server instance.
174
+ * @param context — Runtime context with Solana RPC connection.
175
+ *
176
+ * @internal
177
+ */
178
+ function registerPerpPositionInfoTool(server, context) {
179
+ const schema = {
180
+ type: 'object',
181
+ properties: {
182
+ wallet: {
183
+ type: 'string',
184
+ description: 'Wallet public key (base58) to read positions for.',
185
+ },
186
+ },
187
+ required: ['wallet'],
188
+ additionalProperties: false,
189
+ };
190
+ registerTool(server, 'sap_perp_position_info', {
191
+ 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.',
192
+ inputSchema: schema,
193
+ }, async (args) => {
194
+ const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
195
+ if (!walletStr) {
196
+ return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
197
+ }
198
+ let walletPubkey;
199
+ try {
200
+ walletPubkey = new PublicKey(walletStr);
201
+ }
202
+ catch {
203
+ return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
204
+ }
205
+ try {
206
+ const accounts = await context.connection.getProgramAccounts(getAdrenaProgramId(context), {
207
+ filters: [
208
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
209
+ { memcmp: { offset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET, bytes: walletPubkey.toBase58() } },
210
+ ],
211
+ commitment: 'confirmed',
212
+ });
213
+ const marketsByCustody = await readAdrenaMarketsByCustody(context);
214
+ const positions = accounts
215
+ .map(({ pubkey, account }) => decodeAdrenaPositionAccount(pubkey, account.data, marketsByCustody))
216
+ .filter((position) => Boolean(position))
217
+ .map((position) => ({
218
+ positionKey: position.positionKey,
219
+ market: position.market,
220
+ side: position.side,
221
+ size: position.size,
222
+ collateral: position.collateral,
223
+ entryPrice: position.entryPrice,
224
+ markPrice: position.entryPrice,
225
+ leverage: position.leverage,
226
+ unrealizedPnl: position.unrealizedPnl,
227
+ liquidationPrice: position.liquidationPrice,
228
+ }));
229
+ return createTextResponse(JSON.stringify({
230
+ wallet: walletStr,
231
+ positions,
232
+ count: positions.length,
233
+ scan: {
234
+ programId: getAdrenaProgramId(context).toBase58(),
235
+ positionDiscriminator: discToBase58(DISC_POSITION),
236
+ ownerMemcmpOffset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET,
237
+ },
238
+ 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.',
239
+ }));
240
+ }
241
+ catch (err) {
242
+ return createTextResponse(JSON.stringify({
243
+ error: 'Failed to read on-chain positions',
244
+ message: err instanceof Error ? err.message : 'Unknown error',
245
+ }), { isError: true });
246
+ }
247
+ });
248
+ }
249
+ /* ═══════════════════════════════════════════════════════════════════
250
+ * Tool 3: sap_perp_funding_history
251
+ * ═══════════════════════════════════════════════════════════════════ */
252
+ /**
253
+ * @name registerPerpFundingHistoryTool
254
+ * @description Register the sap_perp_funding_history read-only tool.
255
+ *
256
+ * Computes the current funding rate from on-chain Custody account data.
257
+ * The agent obtains the custody address from `sap_perp_markets` and passes
258
+ * it here. Historical funding rate snapshots are not available on-chain
259
+ * (only the current rate embedded in the custody account); the tool returns
260
+ * the current funding state and a clear error if the custody address is
261
+ * missing or invalid.
262
+ *
263
+ * @param server — MCP server instance.
264
+ * @param context — Runtime context with Solana RPC connection.
265
+ *
266
+ * @internal
267
+ */
268
+ function registerPerpFundingHistoryTool(server, context) {
269
+ const schema = {
270
+ type: 'object',
271
+ properties: {
272
+ custodyAddress: {
273
+ type: 'string',
274
+ description: 'Custody account public key (base58). Obtain from sap_perp_markets output — the custodyAddress field.',
275
+ },
276
+ market: {
277
+ type: 'string',
278
+ description: 'Optional market symbol (e.g. "SOL", "BTC") for display purposes.',
279
+ },
280
+ limit: {
281
+ type: 'number',
282
+ description: 'Maximum number of funding records to return (default 100). On-chain mode returns the current funding snapshot only.',
283
+ minimum: 1,
284
+ maximum: 1000,
285
+ },
286
+ },
287
+ required: ['custodyAddress'],
288
+ additionalProperties: false,
289
+ };
290
+ registerTool(server, 'sap_perp_funding_history', {
291
+ 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.',
292
+ inputSchema: schema,
293
+ }, async (args) => {
294
+ const custodyAddressStr = typeof args['custodyAddress'] === 'string' ? args['custodyAddress'] : '';
295
+ const market = typeof args['market'] === 'string' ? args['market'] : '';
296
+ if (!custodyAddressStr) {
297
+ return createTextResponse(JSON.stringify({
298
+ error: 'custodyAddress is required. Use sap_perp_markets to get the custody address for the desired market.',
299
+ }), { isError: true });
300
+ }
301
+ let custodyPubkey;
302
+ try {
303
+ custodyPubkey = new PublicKey(custodyAddressStr);
304
+ }
305
+ catch {
306
+ return createTextResponse(JSON.stringify({
307
+ error: 'Invalid custody address',
308
+ custodyAddress: custodyAddressStr,
309
+ }), { isError: true });
310
+ }
311
+ try {
312
+ const accountInfo = await context.connection.getAccountInfo(custodyPubkey, 'confirmed');
313
+ if (!accountInfo) {
314
+ return createTextResponse(JSON.stringify({
315
+ error: 'Custody account not found on-chain',
316
+ custodyAddress: custodyAddressStr,
317
+ message: 'The custody account does not exist or has been closed. Use sap_perp_markets to find valid custody addresses.',
318
+ }), { isError: true });
319
+ }
320
+ const data = accountInfo.data;
321
+ if (data.length < 8 + 32 + 32) {
322
+ return createTextResponse(JSON.stringify({
323
+ error: 'Account data too short to be a valid Custody account',
324
+ custodyAddress: custodyAddressStr,
325
+ dataLength: data.length,
326
+ }), { isError: true });
327
+ }
328
+ // Verify discriminator matches Custody.
329
+ const disc = data.subarray(0, 8);
330
+ if (!disc.equals(DISC_CUSTODY)) {
331
+ return createTextResponse(JSON.stringify({
332
+ error: 'Account discriminator does not match Custody type',
333
+ custodyAddress: custodyAddressStr,
334
+ expectedDisc: Array.from(DISC_CUSTODY),
335
+ actualDisc: Array.from(disc),
336
+ message: 'The provided address is not a Custody account. Use sap_perp_markets to get the correct custodyAddress.',
337
+ }), { isError: true });
338
+ }
339
+ const poolAddress = readPublicKey(data, ADRENA_CUSTODY_OFFSETS.pool);
340
+ const poolsByAddress = new Map();
341
+ const poolInfo = await context.connection.getAccountInfo(new PublicKey(poolAddress), 'confirmed');
342
+ if (poolInfo) {
343
+ const decodedPool = decodeAdrenaPoolAccount(new PublicKey(poolAddress), poolInfo.data);
344
+ if (decodedPool)
345
+ poolsByAddress.set(poolAddress, decodedPool);
346
+ }
347
+ const decoded = decodeAdrenaCustodyAccount(custodyPubkey, data, poolsByAddress.get(poolAddress));
348
+ if (!decoded) {
349
+ return createTextResponse(JSON.stringify({
350
+ error: 'Unable to decode Adrena Custody account with release/39 ABI layout',
351
+ custodyAddress: custodyAddressStr,
352
+ }), { isError: true });
353
+ }
354
+ const timestamp = Date.now();
355
+ return createTextResponse(JSON.stringify({
356
+ source: 'on-chain-rpc',
357
+ custodyAddress: custodyAddressStr,
358
+ market: market || decoded.market,
359
+ symbol: decoded.symbol,
360
+ poolAddress: decoded.poolAddress,
361
+ custodyMint: decoded.custodyMint,
362
+ oracle: decoded.oracle,
363
+ tradeOracle: decoded.tradeOracle,
364
+ currentFunding: {
365
+ timestamp,
366
+ fundingRateRaw: decoded.funding.currentRateLongToShortRaw,
367
+ cumulativeLongToShortRaw: decoded.funding.cumulativeLongToShortRaw,
368
+ cumulativeShortToLongRaw: decoded.funding.cumulativeShortToLongRaw,
369
+ maxHourlyFundingRateRaw: decoded.funding.maxHourlyFundingRateRaw,
370
+ fundingLastUpdate: decoded.funding.lastUpdate,
371
+ markPrice: decoded.markPrice,
372
+ markPriceSource: decoded.markPriceSource,
373
+ openInterestLong: decoded.openInterestLong,
374
+ openInterestShort: decoded.openInterestShort,
375
+ },
376
+ records: [{
377
+ timestamp,
378
+ fundingRateRaw: decoded.funding.currentRateLongToShortRaw,
379
+ cumulativeLongToShortRaw: decoded.funding.cumulativeLongToShortRaw,
380
+ cumulativeShortToLongRaw: decoded.funding.cumulativeShortToLongRaw,
381
+ }],
382
+ count: 1,
383
+ 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.',
384
+ }));
385
+ }
386
+ catch (err) {
387
+ return createTextResponse(JSON.stringify({
388
+ error: 'Failed to read funding data from on-chain custody account',
389
+ custodyAddress: custodyAddressStr,
390
+ message: err instanceof Error ? err.message : 'Unknown error',
391
+ }), { isError: true });
392
+ }
393
+ });
394
+ }
395
+ /* ═══════════════════════════════════════════════════════════════════
396
+ * Tool 7: sap_perp_liquidation_zones
397
+ * ═══════════════════════════════════════════════════════════════════ */
398
+ /**
399
+ * @name registerPerpLiquidationZonesTool
400
+ * @description Register the sap_perp_liquidation_zones read-only tool.
401
+ *
402
+ * Reads on-chain Adrena positions for a wallet and computes liquidation
403
+ * zones from account geometry. Live current price requires a separate
404
+ * oracle/feed decoder.
405
+ *
406
+ * @param server — MCP server instance.
407
+ * @param context — Runtime context with Solana RPC connection.
408
+ *
409
+ * @internal
410
+ */
411
+ function registerPerpLiquidationZonesTool(server, context) {
412
+ const schema = {
413
+ type: 'object',
414
+ properties: {
415
+ wallet: {
416
+ type: 'string',
417
+ description: 'Wallet public key (base58) to compute liquidation zones for.',
418
+ },
419
+ },
420
+ required: ['wallet'],
421
+ additionalProperties: false,
422
+ };
423
+ registerTool(server, 'sap_perp_liquidation_zones', {
424
+ 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.',
425
+ inputSchema: schema,
426
+ }, async (args) => {
427
+ const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
428
+ if (!walletStr) {
429
+ return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
430
+ }
431
+ let walletPubkey;
432
+ try {
433
+ walletPubkey = new PublicKey(walletStr);
434
+ }
435
+ catch {
436
+ return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
437
+ }
438
+ try {
439
+ const accounts = await context.connection.getProgramAccounts(getAdrenaProgramId(context), {
440
+ filters: [
441
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
442
+ { memcmp: { offset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET, bytes: walletPubkey.toBase58() } },
443
+ ],
444
+ commitment: 'confirmed',
445
+ });
446
+ const marketsByCustody = await readAdrenaMarketsByCustody(context);
447
+ const zones = accounts
448
+ .map(({ pubkey, account }) => decodeAdrenaPositionAccount(pubkey, account.data, marketsByCustody))
449
+ .filter((position) => Boolean(position))
450
+ .map((position) => {
451
+ const currentPrice = position.entryPrice;
452
+ const distanceToLiquidationPct = position.liquidationPrice > 0 && currentPrice > 0
453
+ ? Math.abs((currentPrice - position.liquidationPrice) / currentPrice) * 100
454
+ : 0;
455
+ return {
456
+ positionKey: position.positionKey,
457
+ market: position.market,
458
+ side: position.side,
459
+ liquidationPrice: position.liquidationPrice,
460
+ currentPrice,
461
+ distanceToLiquidationPct,
462
+ leverage: position.leverage,
463
+ };
464
+ });
465
+ return createTextResponse(JSON.stringify({
466
+ wallet: walletStr,
467
+ zones,
468
+ count: zones.length,
469
+ scan: {
470
+ programId: getAdrenaProgramId(context).toBase58(),
471
+ positionDiscriminator: discToBase58(DISC_POSITION),
472
+ ownerMemcmpOffset: ADRENA_POSITION_OWNER_MEMCMP_OFFSET,
473
+ },
474
+ 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.',
475
+ }));
476
+ }
477
+ catch (err) {
478
+ return createTextResponse(JSON.stringify({
479
+ error: 'Failed to read on-chain positions for liquidation zones',
480
+ message: err instanceof Error ? err.message : 'Unknown error',
481
+ }), { isError: true });
482
+ }
483
+ });
484
+ }
485
+ /* ═══════════════════════════════════════════════════════════════════
486
+ * Tool 8: sap_perp_trade_plan
487
+ * ═══════════════════════════════════════════════════════════════════ */
488
+ /**
489
+ * @name registerPerpTradePlanTool
490
+ * @description Register the professional perps planning tool.
491
+ *
492
+ * This tool does not build a transaction. It turns an intent into a compact
493
+ * risk, sizing, and execution checklist so agents can act like traders while
494
+ * avoiding fake or incomplete unsigned transaction builders.
495
+ *
496
+ * @param server — MCP server instance.
497
+ *
498
+ * @internal
499
+ */
500
+ function registerPerpTradePlanTool(server) {
501
+ const schema = {
502
+ type: 'object',
503
+ properties: {
504
+ market: {
505
+ type: 'string',
506
+ description: 'Perp market symbol or pair, for example SOL-PERP, BTC-PERP, or ETH-PERP.',
507
+ },
508
+ side: {
509
+ type: 'string',
510
+ description: 'Intended direction for the trade.',
511
+ enum: ['long', 'short'],
512
+ },
513
+ collateralAmountUsd: {
514
+ type: 'number',
515
+ description: 'Collateral to allocate in USD. This is the margin budget, not notional size.',
516
+ minimum: 0,
517
+ },
518
+ leverage: {
519
+ type: 'number',
520
+ description: 'Requested leverage multiplier. Keep conservative unless user policy explicitly allows more.',
521
+ minimum: 1,
522
+ maximum: 100,
523
+ },
524
+ entryPrice: {
525
+ type: 'number',
526
+ description: 'Reference entry price in USD used for risk math.',
527
+ minimum: 0,
528
+ },
529
+ stopLossPrice: {
530
+ type: 'number',
531
+ description: 'Optional stop loss price in USD. Strongly recommended before execution.',
532
+ minimum: 0,
533
+ },
534
+ takeProfitPrice: {
535
+ type: 'number',
536
+ description: 'Optional take profit price in USD used to compute reward/risk.',
537
+ minimum: 0,
538
+ },
539
+ maxAccountRiskPct: {
540
+ type: 'number',
541
+ description: 'Maximum account risk percentage allowed by local policy. Default 1%.',
542
+ minimum: 0,
543
+ maximum: 100,
544
+ },
545
+ maxSlippageBps: {
546
+ type: 'number',
547
+ description: 'Maximum execution slippage in basis points. Default 50 bps.',
548
+ minimum: 0,
549
+ maximum: 10_000,
550
+ },
551
+ timeframe: {
552
+ type: 'string',
553
+ description: 'Trading horizon such as scalp, intraday, swing, or hedge.',
554
+ },
555
+ notes: {
556
+ type: 'string',
557
+ description: 'Optional user notes, catalyst, invalidation thesis, or strategy context.',
558
+ },
559
+ },
560
+ required: ['market', 'side', 'collateralAmountUsd', 'leverage', 'entryPrice'],
561
+ additionalProperties: false,
562
+ };
563
+ registerTool(server, 'sap_perp_trade_plan', {
564
+ 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.',
565
+ inputSchema: schema,
566
+ }, async (args) => {
567
+ const market = String(args['market'] ?? '').trim().toUpperCase();
568
+ const side = args['side'] === 'short' ? 'short' : 'long';
569
+ const collateralAmountUsd = typeof args['collateralAmountUsd'] === 'number' ? args['collateralAmountUsd'] : 0;
570
+ const leverage = typeof args['leverage'] === 'number' ? args['leverage'] : 1;
571
+ const entryPrice = typeof args['entryPrice'] === 'number' ? args['entryPrice'] : 0;
572
+ const stopLossPrice = typeof args['stopLossPrice'] === 'number' ? args['stopLossPrice'] : null;
573
+ const takeProfitPrice = typeof args['takeProfitPrice'] === 'number' ? args['takeProfitPrice'] : null;
574
+ const maxAccountRiskPct = typeof args['maxAccountRiskPct'] === 'number' ? args['maxAccountRiskPct'] : 1;
575
+ const maxSlippageBps = typeof args['maxSlippageBps'] === 'number' ? args['maxSlippageBps'] : 50;
576
+ if (!market || collateralAmountUsd <= 0 || leverage <= 0 || entryPrice <= 0) {
577
+ return createTextResponse(JSON.stringify({
578
+ error: 'market, collateralAmountUsd, leverage, and entryPrice are required and must be positive.',
579
+ }), { isError: true });
580
+ }
581
+ const notionalUsd = collateralAmountUsd * leverage;
582
+ const stopMovePct = stopLossPrice && stopLossPrice > 0
583
+ ? Math.abs((entryPrice - stopLossPrice) / entryPrice) * 100
584
+ : null;
585
+ const takeProfitMovePct = takeProfitPrice && takeProfitPrice > 0
586
+ ? Math.abs((takeProfitPrice - entryPrice) / entryPrice) * 100
587
+ : null;
588
+ const estimatedStopRiskUsd = stopMovePct === null ? null : notionalUsd * (stopMovePct / 100);
589
+ const estimatedRewardUsd = takeProfitMovePct === null ? null : notionalUsd * (takeProfitMovePct / 100);
590
+ const rewardRisk = estimatedStopRiskUsd && estimatedRewardUsd
591
+ ? estimatedRewardUsd / estimatedStopRiskUsd
592
+ : null;
593
+ const liquidationEstimate = side === 'long'
594
+ ? entryPrice * (1 - (1 / leverage))
595
+ : entryPrice * (1 + (1 / leverage));
596
+ const riskFlags = [];
597
+ if (!stopLossPrice)
598
+ riskFlags.push('missing_stop_loss');
599
+ if (estimatedStopRiskUsd !== null && estimatedStopRiskUsd > collateralAmountUsd * (maxAccountRiskPct / 100)) {
600
+ riskFlags.push('stop_risk_exceeds_policy');
601
+ }
602
+ if (leverage > 10)
603
+ riskFlags.push('high_leverage_requires_explicit_user_confirmation');
604
+ if (maxSlippageBps > 100)
605
+ riskFlags.push('slippage_above_1_percent');
606
+ return createTextResponse(JSON.stringify({
607
+ market,
608
+ side,
609
+ executionStatus: 'analysis_only',
610
+ collateralAmountUsd,
611
+ leverage,
612
+ notionalUsd,
613
+ entryPrice,
614
+ stopLossPrice,
615
+ takeProfitPrice,
616
+ maxAccountRiskPct,
617
+ maxSlippageBps,
618
+ liquidationEstimate,
619
+ estimatedStopRiskUsd,
620
+ estimatedRewardUsd,
621
+ rewardRisk,
622
+ riskFlags,
623
+ professionalChecklist: [
624
+ 'Call sap_perp_markets for current market/custody data before execution.',
625
+ 'Call sap_chart_ohlc and sap_chart_volume_profile to validate trend, liquidity, POC, VAH, and VAL.',
626
+ 'Call sap_perp_position_info and sap_perp_liquidation_zones for the user wallet before increasing exposure.',
627
+ 'Show the user one compact preview: side, notional, margin, leverage, stop, take profit, liquidation estimate, slippage, and risk flags.',
628
+ '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.',
629
+ ],
630
+ recommendedReadTools: [
631
+ 'sap_perp_markets',
632
+ 'sap_chart_ohlc',
633
+ 'sap_chart_volume_profile',
634
+ 'sap_perp_position_info',
635
+ 'sap_perp_liquidation_zones',
636
+ ],
637
+ 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.',
638
+ notes: typeof args['notes'] === 'string' ? args['notes'] : undefined,
639
+ }, null, 2));
640
+ });
641
+ }
642
+ /* ═══════════════════════════════════════════════════════════════════
643
+ * Tool 9: sap_perp_builder_status
644
+ * ═══════════════════════════════════════════════════════════════════ */
645
+ function registerPerpBuilderStatusTool(server, context) {
646
+ const schema = {
647
+ type: 'object',
648
+ properties: {
649
+ venue: {
650
+ type: 'string',
651
+ description: 'Optional perp venue to check, for example "adrena" or "sap-perps-provider".',
652
+ },
653
+ },
654
+ additionalProperties: false,
655
+ };
656
+ registerTool(server, 'sap_perp_builder_status', {
657
+ description: 'Free readiness check for perps execution. Returns whether SAP MCP has native Adrena perps builders (available since 0.9.38) or a configured hosted unsigned transaction builder. If builderAvailable is false, agents must stop before execution and must not route direct signer-only perps tools through x402 paid-call replay.',
658
+ inputSchema: schema,
659
+ }, async (args) => {
660
+ const perps = getPerpsConfig(context);
661
+ const venue = typeof args['venue'] === 'string' ? args['venue'] : 'configured-perps-provider';
662
+ return createTextResponse(JSON.stringify({
663
+ venue,
664
+ marketsProviderConfigured: Boolean(perps.marketsUrl),
665
+ positionsProviderConfigured: Boolean(perps.positionsUrl),
666
+ builderAvailable: true,
667
+ adrenaProgramId: perps.adrenaProgramId,
668
+ rpcScanMode: 'anchor-discriminator-getProgramAccounts',
669
+ scanDiscriminators: {
670
+ pool: discToBase58(DISC_POOL),
671
+ custody: discToBase58(DISC_CUSTODY),
672
+ position: discToBase58(DISC_POSITION),
673
+ },
674
+ builderMode: perps.builderUrl ? 'hosted_unsigned_transaction_builder' : 'not_configured',
675
+ nativeAdrenaDecoder: {
676
+ available: true,
677
+ source: 'official Adrena release/39 ABI layout',
678
+ sdkPackage: 'adrena-sdk@beta (types only, no runtime JS — using vendored IDL with @coral-xyz/anchor instead)',
679
+ },
680
+ nativeAdrenaBuilder: {
681
+ available: true,
682
+ source: 'vendored Adrena IDL (release/39) + @coral-xyz/anchor',
683
+ operations: [
684
+ 'sap_adrena_build_open_long',
685
+ 'sap_adrena_build_open_short',
686
+ 'sap_adrena_build_close_long',
687
+ 'sap_adrena_build_close_short',
688
+ 'sap_adrena_build_set_stop_loss',
689
+ 'sap_adrena_build_set_take_profit',
690
+ 'sap_adrena_build_cancel_stop_loss',
691
+ 'sap_adrena_build_cancel_take_profit',
692
+ 'sap_adrena_build_add_limit_order',
693
+ 'sap_adrena_build_cancel_limit_order',
694
+ 'sap_adrena_build_open_commodity_long',
695
+ 'sap_adrena_build_open_commodity_short',
696
+ 'sap_adrena_build_close_commodity_long',
697
+ 'sap_adrena_build_close_commodity_short',
698
+ 'sap_adrena_build_add_liquidity',
699
+ 'sap_adrena_build_remove_liquidity',
700
+ 'sap_adrena_build_swap',
701
+ 'sap_adrena_build_init_user_staking',
702
+ 'sap_adrena_build_add_liquid_stake',
703
+ 'sap_adrena_build_remove_liquid_stake',
704
+ 'sap_adrena_build_add_locked_stake',
705
+ 'sap_adrena_build_claim_stakes',
706
+ ],
707
+ dataApi: [
708
+ 'sap_adrena_get_positions',
709
+ 'sap_adrena_get_pool_info',
710
+ 'sap_adrena_get_custody_info',
711
+ 'sap_adrena_get_trader_info',
712
+ 'sap_adrena_get_trader_leaderboard',
713
+ 'sap_adrena_get_mutagen',
714
+ 'sap_adrena_get_mutagen_leaderboard',
715
+ 'sap_adrena_get_prices',
716
+ 'sap_adrena_get_trading_prices',
717
+ 'sap_adrena_get_position_status',
718
+ ],
719
+ signerPolicy: 'All builder tools return unsigned base64 transactions. Sign locally via sap_payments_finalize_transaction. SAP MCP never signs user-owned Adrena transactions.',
720
+ },
721
+ signerPolicy: 'Use sap_adrena_build_* tools to construct unsigned transactions, then sign locally with sap_payments_finalize_transaction.',
722
+ paymentPolicy: 'Adrena builder tools (sap_adrena_build_*) are paid builder calls at $0.006 each. Data API tools (sap_adrena_get_*) are micro-read at $0.001 each. Finalization via sap_payments_finalize_transaction is free.',
723
+ nextAction: 'Use sap_adrena_get_pool_info + sap_adrena_get_trading_prices for market data, then sap_adrena_build_open_long (or short), then sap_payments_finalize_transaction with submit:true after user confirmation.',
724
+ }, null, 2));
725
+ });
726
+ }
727
+ /* ═══════════════════════════════════════════════════════════════════
728
+ * Optional Tool 10: sap_perp_build_order_transaction
729
+ * ═══════════════════════════════════════════════════════════════════ */
730
+ function registerPerpOrderBuilderTool(server, context) {
731
+ const perps = getPerpsConfig(context);
732
+ if (!perps.builderUrl) {
733
+ return false;
734
+ }
735
+ const schema = {
736
+ type: 'object',
737
+ properties: {
738
+ venue: {
739
+ type: 'string',
740
+ description: 'Perps venue supported by the configured builder, for example "adrena".',
741
+ },
742
+ owner: {
743
+ type: 'string',
744
+ description: 'User wallet public key that will sign and own the perp order/position.',
745
+ },
746
+ market: {
747
+ type: 'string',
748
+ description: 'Market symbol or provider market id, for example SOL-PERP.',
749
+ },
750
+ side: {
751
+ type: 'string',
752
+ description: 'Trade side for the order.',
753
+ enum: ['long', 'short'],
754
+ },
755
+ orderType: {
756
+ type: 'string',
757
+ description: 'Order type supported by the provider. Use "market" unless the provider explicitly supports limit/trigger orders.',
758
+ enum: ['market', 'limit', 'trigger'],
759
+ },
760
+ collateralMint: {
761
+ type: 'string',
762
+ description: 'Collateral token mint. Use USDC mint for USDC-margined perps unless the provider says otherwise.',
763
+ },
764
+ collateralAmount: {
765
+ type: 'string',
766
+ description: 'Collateral amount in smallest token units, for example micro-USDC for USDC.',
767
+ },
768
+ leverage: {
769
+ type: 'number',
770
+ description: 'Requested leverage multiplier after local policy checks.',
771
+ minimum: 1,
772
+ maximum: 100,
773
+ },
774
+ slippageBps: {
775
+ type: 'number',
776
+ description: 'Maximum slippage in basis points. Keep <= 100 unless user explicitly confirms higher risk.',
777
+ minimum: 0,
778
+ maximum: 10_000,
779
+ },
780
+ reduceOnly: {
781
+ type: 'boolean',
782
+ description: 'Set true only for reduce-only close/reduce flows.',
783
+ },
784
+ limitPrice: {
785
+ type: 'number',
786
+ description: 'Optional limit or trigger price in USD when orderType is limit or trigger.',
787
+ minimum: 0,
788
+ },
789
+ clientOrderId: {
790
+ type: 'string',
791
+ description: 'Optional client id for idempotency/audit. Generate once and reuse on retries.',
792
+ },
793
+ },
794
+ required: ['owner', 'market', 'side', 'orderType', 'collateralMint', 'collateralAmount', 'leverage'],
795
+ additionalProperties: false,
796
+ };
797
+ registerTool(server, 'sap_perp_build_order_transaction', {
798
+ 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.',
799
+ inputSchema: schema,
800
+ }, async (args) => {
801
+ const payload = await timedFetch(perps.builderUrl, {
802
+ method: 'POST',
803
+ headers: perpsProviderHeaders(),
804
+ body: JSON.stringify({
805
+ ...args,
806
+ requestedBy: 'sap-mcp-hosted',
807
+ signerPolicy: 'local-finalization-required',
808
+ }),
809
+ timeoutMs: perps.timeoutMs,
810
+ });
811
+ const transactionBase64 = payload?.transactionBase64
812
+ ?? payload?.unsignedTransactionBase64
813
+ ?? payload?.transaction;
814
+ if (!payload || typeof transactionBase64 !== 'string' || transactionBase64.length === 0) {
815
+ return createTextResponse(JSON.stringify({
816
+ error: 'perps_builder_invalid_response',
817
+ message: 'Configured perps builder did not return transactionBase64/unsignedTransactionBase64. Execution stopped before local signing.',
818
+ expectedNextStep: 'Fix SAP_MCP_PERPS_BUILDER_URL provider response. Do not create temporary signing scripts or hand-roll perps transactions.',
819
+ }, null, 2), { isError: true });
820
+ }
821
+ return createTextResponse(JSON.stringify({
822
+ success: true,
823
+ venue: args['venue'] ?? 'configured-perps-provider',
824
+ transactionBase64,
825
+ lastValidBlockHeight: payload.lastValidBlockHeight,
826
+ blockhash: payload.blockhash,
827
+ feePayer: payload.feePayer,
828
+ provider: payload.provider ?? 'configured-perps-builder',
829
+ signerPolicy: 'local-finalization-required',
830
+ nextTool: 'sap_payments_finalize_transaction',
831
+ finalizeArgs: {
832
+ transactionBase64,
833
+ submit: false,
834
+ },
835
+ warnings: payload.warnings,
836
+ }, null, 2));
837
+ });
838
+ return true;
839
+ }
840
+ /* ═══════════════════════════════════════════════════════════════════
841
+ * Main registration function
842
+ * ═══════════════════════════════════════════════════════════════════ */
843
+ /**
844
+ * @name registerPerpTools
845
+ * @description Register perp trading analytics, planning, and optional execution-builder tools.
846
+ *
847
+ * @param server — MCP server instance.
848
+ * @param context — Shared runtime context with SAP client, connection, and config.
849
+ *
850
+ * @usedBy `register-tools.ts`
851
+ */
852
+ export function registerPerpTools(server, context) {
853
+ logger.debug('Registering perp trading analytics and planning tools');
854
+ registerPerpMarketsTool(server, context);
855
+ registerPerpPositionInfoTool(server, context);
856
+ registerPerpFundingHistoryTool(server, context);
857
+ registerChartOhlcTool(server, context);
858
+ registerChartLongTermTool(server, context);
859
+ registerChartVolumeProfileTool(server, context);
860
+ registerChartMultiOhlcTool(server, context);
861
+ registerChartIndicatorsTool(server, context);
862
+ registerPerpLiquidationZonesTool(server, context);
863
+ registerPerpTradePlanTool(server);
864
+ registerPerpBuilderStatusTool(server, context);
865
+ const builderRegistered = registerPerpOrderBuilderTool(server, context);
866
+ logger.debug('Perp tools registered', { count: builderRegistered ? 10 : 9, builderRegistered });
867
+ }
868
+ //# sourceMappingURL=perp-analytics-tools.js.map