@oobe-protocol-labs/sap-mcp-server 0.9.21 → 0.9.35

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 (190) hide show
  1. package/.env.example +11 -1
  2. package/CHANGELOG.md +233 -6
  3. package/README.md +3 -3
  4. package/USER_DOCS/00_START_HERE.md +9 -1
  5. package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +1 -1
  6. package/USER_DOCS/04_CLIENT_CONFIGS.md +3 -3
  7. package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +5 -5
  8. package/config.example.json +1 -1
  9. package/config.secure-example.json +1 -1
  10. package/dist/config/env.d.ts +18 -0
  11. package/dist/config/env.d.ts.map +1 -1
  12. package/dist/config/env.js +14 -2
  13. package/dist/config/env.js.map +1 -1
  14. package/dist/config/mcp-client-injection.js +3 -3
  15. package/dist/config/paths.d.ts +9 -1
  16. package/dist/config/paths.d.ts.map +1 -1
  17. package/dist/config/paths.js +15 -1
  18. package/dist/config/paths.js.map +1 -1
  19. package/dist/config/wizard.d.ts.map +1 -1
  20. package/dist/config/wizard.js +3 -1
  21. package/dist/config/wizard.js.map +1 -1
  22. package/dist/core/constants.d.ts +1 -1
  23. package/dist/core/constants.js +1 -1
  24. package/dist/core/logger.js +1 -1
  25. package/dist/memory/async-processor.d.ts +107 -0
  26. package/dist/memory/async-processor.d.ts.map +1 -0
  27. package/dist/memory/async-processor.js +204 -0
  28. package/dist/memory/async-processor.js.map +1 -0
  29. package/dist/memory/auto-record.d.ts +47 -0
  30. package/dist/memory/auto-record.d.ts.map +1 -0
  31. package/dist/memory/auto-record.js +133 -0
  32. package/dist/memory/auto-record.js.map +1 -0
  33. package/dist/memory/database.d.ts +103 -0
  34. package/dist/memory/database.d.ts.map +1 -0
  35. package/dist/memory/database.js +337 -0
  36. package/dist/memory/database.js.map +1 -0
  37. package/dist/memory/hermes-bridge.d.ts +70 -0
  38. package/dist/memory/hermes-bridge.d.ts.map +1 -0
  39. package/dist/memory/hermes-bridge.js +155 -0
  40. package/dist/memory/hermes-bridge.js.map +1 -0
  41. package/dist/memory/index.d.ts +23 -0
  42. package/dist/memory/index.d.ts.map +1 -0
  43. package/dist/memory/index.js +22 -0
  44. package/dist/memory/index.js.map +1 -0
  45. package/dist/memory/memory-store.d.ts +76 -0
  46. package/dist/memory/memory-store.d.ts.map +1 -0
  47. package/dist/memory/memory-store.js +231 -0
  48. package/dist/memory/memory-store.js.map +1 -0
  49. package/dist/memory/stream-buffer-store.d.ts +69 -0
  50. package/dist/memory/stream-buffer-store.d.ts.map +1 -0
  51. package/dist/memory/stream-buffer-store.js +182 -0
  52. package/dist/memory/stream-buffer-store.js.map +1 -0
  53. package/dist/memory/tool-call-store.d.ts +68 -0
  54. package/dist/memory/tool-call-store.d.ts.map +1 -0
  55. package/dist/memory/tool-call-store.js +189 -0
  56. package/dist/memory/tool-call-store.js.map +1 -0
  57. package/dist/memory/types.d.ts +197 -0
  58. package/dist/memory/types.d.ts.map +1 -0
  59. package/dist/memory/types.js +15 -0
  60. package/dist/memory/types.js.map +1 -0
  61. package/dist/memory/utils.d.ts +30 -0
  62. package/dist/memory/utils.d.ts.map +1 -0
  63. package/dist/memory/utils.js +47 -0
  64. package/dist/memory/utils.js.map +1 -0
  65. package/dist/payments/hosted-tool-eligibility.d.ts.map +1 -1
  66. package/dist/payments/hosted-tool-eligibility.js +4 -6
  67. package/dist/payments/hosted-tool-eligibility.js.map +1 -1
  68. package/dist/payments/monetization-gate.d.ts +39 -0
  69. package/dist/payments/monetization-gate.d.ts.map +1 -1
  70. package/dist/payments/monetization-gate.js +165 -11
  71. package/dist/payments/monetization-gate.js.map +1 -1
  72. package/dist/payments/pricing.d.ts.map +1 -1
  73. package/dist/payments/pricing.js +69 -0
  74. package/dist/payments/pricing.js.map +1 -1
  75. package/dist/payments/x402-paid-call.js +1 -1
  76. package/dist/premium/builtin-plugins.d.ts +42 -0
  77. package/dist/premium/builtin-plugins.d.ts.map +1 -1
  78. package/dist/premium/builtin-plugins.js +91 -10
  79. package/dist/premium/builtin-plugins.js.map +1 -1
  80. package/dist/premium/manifest-builder.d.ts.map +1 -1
  81. package/dist/premium/manifest-builder.js +43 -7
  82. package/dist/premium/manifest-builder.js.map +1 -1
  83. package/dist/premium/meme-radar-capabilities.js +9 -9
  84. package/dist/premium/meme-radar-capabilities.js.map +1 -1
  85. package/dist/premium/plugin-validator.d.ts.map +1 -1
  86. package/dist/premium/plugin-validator.js +62 -1
  87. package/dist/premium/plugin-validator.js.map +1 -1
  88. package/dist/premium/provider-bridge.d.ts +25 -1
  89. package/dist/premium/provider-bridge.d.ts.map +1 -1
  90. package/dist/premium/provider-bridge.js +130 -20
  91. package/dist/premium/provider-bridge.js.map +1 -1
  92. package/dist/premium/session-manager.d.ts.map +1 -1
  93. package/dist/premium/session-manager.js +12 -3
  94. package/dist/premium/session-manager.js.map +1 -1
  95. package/dist/premium/tech-fundamentals-capabilities.js +5 -5
  96. package/dist/premium/tech-fundamentals-capabilities.js.map +1 -1
  97. package/dist/premium/trading-capabilities.js +5 -5
  98. package/dist/premium/trading-capabilities.js.map +1 -1
  99. package/dist/premium/webhook-engine.d.ts.map +1 -1
  100. package/dist/premium/webhook-engine.js +7 -1
  101. package/dist/premium/webhook-engine.js.map +1 -1
  102. package/dist/prompts/context/sap-agent-context.prompt.js +1 -1
  103. package/dist/prompts/payments/explain-x402-settlement.prompt.js +1 -1
  104. package/dist/remote/premium-routes.d.ts.map +1 -1
  105. package/dist/remote/premium-routes.js +3 -2
  106. package/dist/remote/premium-routes.js.map +1 -1
  107. package/dist/remote/public-home/sections.d.ts.map +1 -1
  108. package/dist/remote/public-home/sections.js +2 -3
  109. package/dist/remote/public-home/sections.js.map +1 -1
  110. package/dist/remote/public-home/styles.d.ts +1 -1
  111. package/dist/remote/public-home/styles.js +2 -2
  112. package/dist/remote/server.d.ts.map +1 -1
  113. package/dist/remote/server.js +19 -4
  114. package/dist/remote/server.js.map +1 -1
  115. package/dist/server/server-metadata.js +1 -1
  116. package/dist/strategies/strategy-store.d.ts +82 -0
  117. package/dist/strategies/strategy-store.d.ts.map +1 -0
  118. package/dist/strategies/strategy-store.js +165 -0
  119. package/dist/strategies/strategy-store.js.map +1 -0
  120. package/dist/tools/agent-start-tool.d.ts.map +1 -1
  121. package/dist/tools/agent-start-tool.js +97 -10
  122. package/dist/tools/agent-start-tool.js.map +1 -1
  123. package/dist/tools/client-sdk-tools.d.ts.map +1 -1
  124. package/dist/tools/client-sdk-tools.js +152 -9
  125. package/dist/tools/client-sdk-tools.js.map +1 -1
  126. package/dist/tools/estimate-tool-cost.js +1 -1
  127. package/dist/tools/estimate-tool-cost.js.map +1 -1
  128. package/dist/tools/index.d.ts +1 -0
  129. package/dist/tools/index.d.ts.map +1 -1
  130. package/dist/tools/index.js +2 -0
  131. package/dist/tools/index.js.map +1 -1
  132. package/dist/tools/magicblock-tools.js +5 -5
  133. package/dist/tools/magicblock-tools.js.map +1 -1
  134. package/dist/tools/memory-tools.d.ts +26 -0
  135. package/dist/tools/memory-tools.d.ts.map +1 -0
  136. package/dist/tools/memory-tools.js +533 -0
  137. package/dist/tools/memory-tools.js.map +1 -0
  138. package/dist/tools/perp-tools.d.ts +7 -9
  139. package/dist/tools/perp-tools.d.ts.map +1 -1
  140. package/dist/tools/perp-tools.js +386 -367
  141. package/dist/tools/perp-tools.js.map +1 -1
  142. package/dist/tools/premium-tools.d.ts.map +1 -1
  143. package/dist/tools/premium-tools.js +8 -2
  144. package/dist/tools/premium-tools.js.map +1 -1
  145. package/dist/tools/profile-tools.d.ts.map +1 -1
  146. package/dist/tools/profile-tools.js +18 -1
  147. package/dist/tools/profile-tools.js.map +1 -1
  148. package/dist/tools/quick-context-tool.d.ts.map +1 -1
  149. package/dist/tools/quick-context-tool.js +100 -9
  150. package/dist/tools/quick-context-tool.js.map +1 -1
  151. package/dist/tools/register-tools.d.ts.map +1 -1
  152. package/dist/tools/register-tools.js +3 -0
  153. package/dist/tools/register-tools.js.map +1 -1
  154. package/dist/tools/sap-sdk-tools.js +3 -3
  155. package/dist/tools/sap-sdk-tools.js.map +1 -1
  156. package/dist/tools/session-context-packet.d.ts +19 -0
  157. package/dist/tools/session-context-packet.d.ts.map +1 -0
  158. package/dist/tools/session-context-packet.js +256 -0
  159. package/dist/tools/session-context-packet.js.map +1 -0
  160. package/dist/tools/skills-tools.d.ts +10 -0
  161. package/dist/tools/skills-tools.d.ts.map +1 -1
  162. package/dist/tools/skills-tools.js +19 -0
  163. package/dist/tools/skills-tools.js.map +1 -1
  164. package/dist/tools/transaction-tools.d.ts +1 -0
  165. package/dist/tools/transaction-tools.d.ts.map +1 -1
  166. package/dist/tools/transaction-tools.js +248 -9
  167. package/dist/tools/transaction-tools.js.map +1 -1
  168. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
  169. package/dist/tools/x402-paid-call-tool.js +55 -21
  170. package/dist/tools/x402-paid-call-tool.js.map +1 -1
  171. package/dist/wizard-core/desktop-flow.d.ts.map +1 -1
  172. package/dist/wizard-core/desktop-flow.js +4 -2
  173. package/dist/wizard-core/desktop-flow.js.map +1 -1
  174. package/docs/06_PAYMENTS_X402_AND_PAYSH.md +14 -5
  175. package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
  176. package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +13 -5
  177. package/docs/13_BOUNTY_PROGRAM_PROPOSAL.md +1 -1
  178. package/docs/14_DESKTOP_WIZARD_RELEASE.md +3 -3
  179. package/docs/15_DASHBOARD_SCREENSHARE_SCRIPT.md +1 -1
  180. package/docs/BRANCH_AND_CI.md +2 -2
  181. package/docs/README.md +1 -1
  182. package/package.json +3 -1
  183. package/server.json +3 -3
  184. package/skills/README.md +1 -1
  185. package/skills/sap-agentkit/SKILL.md +1 -2
  186. package/skills/sap-defi/SKILL.md +31 -2
  187. package/skills/sap-mcp/SKILL.md +24 -13
  188. package/skills/sap-mcp/TOOL_REFERENCE.md +2 -4
  189. package/skills/sap-payments-x402/SKILL.md +15 -3
  190. package/assets/logos/drift.svg +0 -12
@@ -2,29 +2,27 @@
2
2
  * @name tools/perp-tools
3
3
  * @description MCP tools for perpetual futures trading and chart analysis on Solana.
4
4
  *
5
- * Provides 10 tools across two categories:
5
+ * Provides 8 tools across two categories:
6
6
  *
7
7
  * Read-only tools (7):
8
8
  * - sap_perp_markets — List Adrena perp markets with mark price, funding, OI.
9
9
  * - sap_perp_position_info — Read on-chain perp positions for a wallet.
10
- * - sap_perp_funding_history — Fetch funding rate history from Adrena API.
10
+ * - sap_perp_funding_history — Compute funding rate from on-chain custody account.
11
11
  * - sap_chart_ohlc — OHLC candlestick data for any Solana token.
12
12
  * - sap_chart_long_term — Long-term price history + protocol TVL.
13
13
  * - sap_chart_volume_profile — Volume profile analysis (POC, VAH, VAL).
14
14
  * - sap_perp_liquidation_zones — Compute liquidation zones for open positions.
15
15
  *
16
- * Inscribed tools (3 — build unsigned transactions for local signing):
17
- * - sap_perp_build_open — Build tx to open a leveraged perp position.
18
- * - sap_perp_build_close — Build tx to close a perp position.
19
- * - sap_perp_build_modify — Build tx to add/remove collateral.
16
+ * Professional planning tools (1):
17
+ * - sap_perp_trade_plan — Build a trader-grade risk, route, and execution checklist.
20
18
  *
21
19
  * All read-only tools use free APIs (DexScreener, DeFiLlama) and Solana RPC
22
- * (Triton). Inscribed tools build unsigned transactions with @solana/web3.js
23
- * the agent signs locally, no server-side signing keys.
20
+ * (Triton). Execution builders are intentionally not exposed unless the
21
+ * account graph is IDL-backed and safe for local finalization.
24
22
  *
25
23
  * @module tools/perp-tools
26
24
  */
27
- import { PublicKey, Transaction, TransactionInstruction, SystemProgram } from '@solana/web3.js';
25
+ import { PublicKey } from '@solana/web3.js';
28
26
  import { createTextResponse } from '../adapters/mcp/tool-response.js';
29
27
  import { registerTool } from '../adapters/mcp/sdk-compat.js';
30
28
  import { logger } from '../core/logger.js';
@@ -37,8 +35,6 @@ const ADRENA_PROGRAM_ID = new PublicKey('13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3S
37
35
  const DEXSCREENER_API_URL = 'https://api.dexscreener.com';
38
36
  /** DeFiLlama REST API base URL (free, no API key). */
39
37
  const DEFILAMA_API_URL = 'https://api.llama.fi';
40
- /** Adrena REST API base URL (free, may be unreachable — tools return structured errors). */
41
- const ADRENA_API_URL = 'https://datapi.adrena.xyz';
42
38
  /** Fetch timeout for external API calls (ms). */
43
39
  const FETCH_TIMEOUT_MS = 8_000;
44
40
  /** Default number of volume profile buckets. */
@@ -46,16 +42,14 @@ const DEFAULT_VP_BUCKETS = 20;
46
42
  /** Value area percentage for volume profile (70%). */
47
43
  const VALUE_AREA_PCT = 0.70;
48
44
  /* ═══════════════════════════════════════════════════════════════════
49
- * Adrena instruction discriminators (from IDL v2.1.5, Anchor 0.31)
45
+ * Adrena account discriminators (Anchor 0.31 — sha256("account:<Name>")[0..8])
50
46
  * ═══════════════════════════════════════════════════════════════════ */
51
- const DISC_OPEN_LONG = Buffer.from([224, 114, 146, 60, 127, 166, 244, 56]);
52
- const DISC_OPEN_SHORT = Buffer.from([196, 212, 161, 82, 250, 39, 201, 102]);
53
- const DISC_CLOSE_LONG = Buffer.from([50, 66, 35, 214, 218, 31, 152, 68]);
54
- const DISC_CLOSE_SHORT = Buffer.from([158, 216, 38, 16, 140, 37, 15, 131]);
55
- const DISC_ADD_COLLATERAL_LONG = Buffer.from([101, 191, 243, 208, 154, 22, 72, 19]);
56
- const DISC_ADD_COLLATERAL_SHORT = Buffer.from([197, 235, 47, 1, 228, 10, 200, 184]);
57
- const DISC_REMOVE_COLLATERAL_LONG = Buffer.from([179, 122, 186, 139, 223, 72, 205, 58]);
58
- const DISC_REMOVE_COLLATERAL_SHORT = Buffer.from([242, 74, 116, 29, 106, 148, 241, 205]);
47
+ /** Pool account discriminator — sha256("account:Pool")[0..8]. */
48
+ const DISC_POOL = Buffer.from([241, 154, 109, 4, 17, 177, 109, 188]);
49
+ /** Custody account discriminator — sha256("account:Custody")[0..8]. */
50
+ const DISC_CUSTODY = Buffer.from([1, 184, 48, 81, 93, 131, 63, 145]);
51
+ /** Position account discriminator — sha256("account:Position")[0..8]. */
52
+ const DISC_POSITION = Buffer.from([170, 188, 143, 228, 122, 64, 247, 208]);
59
53
  /* ═══════════════════════════════════════════════════════════════════
60
54
  * Helper: timed fetch
61
55
  * ═══════════════════════════════════════════════════════════════════ */
@@ -87,56 +81,53 @@ async function timedFetch(url) {
87
81
  }
88
82
  }
89
83
  /**
90
- * @name buildInscribedTx
91
- * @description Build an unsigned transaction from a single instruction and serialize it.
84
+ * @name discToBase58
85
+ * @description Convert an 8-byte discriminator Buffer to a base58 string for
86
+ * use in `getProgramAccounts` memcmp filters.
92
87
  *
93
- * @param instructionThe TransactionInstruction to include.
94
- * @returns Serialized base64 transaction + metadata.
88
+ * @param disc8-byte discriminator Buffer.
89
+ * @returns Base58-encoded string.
95
90
  *
96
91
  * @internal
97
92
  */
98
- function buildInscribedTx(instruction) {
99
- const tx = new Transaction();
100
- tx.add(instruction);
101
- const serialized = Buffer.from(tx.serialize({ requireAllSignatures: false })).toString('base64');
102
- return {
103
- transaction: serialized,
104
- programId: instruction.programId.toBase58(),
105
- instruction: 'adrena',
106
- accounts: instruction.keys.map(k => ({
107
- pubkey: k.pubkey.toBase58(),
108
- isSigner: k.isSigner,
109
- isWritable: k.isWritable,
110
- })),
111
- dataLength: instruction.data.length,
112
- };
93
+ function discToBase58(disc) {
94
+ return new PublicKey(disc).toBase58();
113
95
  }
114
96
  /**
115
- * @name writeNumberU64LE
116
- * @description Write a u64 number as little-endian bytes into a Buffer at an offset.
97
+ * @name readU64LE
98
+ * @description Read a u64 little-endian value from a Buffer and return as a JS number.
117
99
  *
118
- * @param buf — Target buffer.
119
- * @param value Number to write (must fit in u64).
120
- * @param offset Write offset in the buffer.
100
+ * @param buf — Source buffer.
101
+ * @param offset Read offset.
102
+ * @returns The value as a JavaScript number.
121
103
  *
122
104
  * @internal
123
105
  */
124
- function writeNumberU64LE(buf, value, offset) {
125
- const bigVal = BigInt(Math.floor(value));
126
- buf.writeBigUInt64LE(bigVal, offset);
106
+ function readU64LE(buf, offset) {
107
+ return Number(buf.readBigUInt64LE(offset));
127
108
  }
128
109
  /**
129
- * @name writeNumberU32LE
130
- * @description Write a u32 number as little-endian bytes into a Buffer at an offset.
110
+ * @name readSymbol
111
+ * @description Read a short string (Anchor `String` 4-byte LE length + UTF-8 bytes)
112
+ * from an account data buffer.
131
113
  *
132
- * @param buf — Target buffer.
133
- * @param value Number to write (must fit in u32).
134
- * @param offsetWrite offset in the buffer.
114
+ * @param buf — Source buffer.
115
+ * @param offset Offset to the string field.
116
+ * @param maxLenMaximum bytes to read (default 16).
117
+ * @returns Decoded symbol string, or empty string on failure.
135
118
  *
136
119
  * @internal
137
120
  */
138
- function writeNumberU32LE(buf, value, offset) {
139
- buf.writeUInt32LE(Math.floor(value), offset);
121
+ function readSymbol(buf, offset, maxLen = 16) {
122
+ try {
123
+ const len = buf.readUInt32LE(offset);
124
+ if (len === 0 || len > maxLen)
125
+ return '';
126
+ return buf.subarray(offset + 4, offset + 4 + len).toString('utf8').replace(/\0/g, '');
127
+ }
128
+ catch {
129
+ return '';
130
+ }
140
131
  }
141
132
  /* ═══════════════════════════════════════════════════════════════════
142
133
  * Tool 1: sap_perp_markets
@@ -145,9 +136,9 @@ function writeNumberU32LE(buf, value, offset) {
145
136
  * @name registerPerpMarketsTool
146
137
  * @description Register the sap_perp_markets read-only tool.
147
138
  *
148
- * Fetches available perp markets from the Adrena REST API. If the API is
149
- * unreachable, falls back to reading on-chain program accounts via Solana RPC
150
- * to discover market accounts.
139
+ * Reads Pool and Custody accounts directly from Solana RPC using
140
+ * `getProgramAccounts` with memcmp discriminator filters. Decodes symbol,
141
+ * price, funding rate, and open interest from raw account data.
151
142
  *
152
143
  * @param server — MCP server instance.
153
144
  * @param context — Runtime context with Solana RPC connection.
@@ -166,45 +157,119 @@ function registerPerpMarketsTool(server, context) {
166
157
  additionalProperties: false,
167
158
  };
168
159
  registerTool(server, 'sap_perp_markets', {
169
- description: 'List available perpetual futures markets on Adrena with mark price, funding rate, open interest, and 24h volume. Read-only — uses free Adrena REST API and Solana RPC.',
160
+ description: 'List available perpetual futures markets on Adrena with mark price, funding rate, open interest, and 24h volume. Read-only — reads Pool and Custody accounts directly from Solana RPC (on-chain). Use with sap_perp_trade_plan for professional pre-trade risk planning.',
170
161
  inputSchema: schema,
171
162
  }, async (args) => {
172
163
  const marketFilter = typeof args['market'] === 'string' ? args['market'].toUpperCase() : '';
173
- // Try Adrena REST API first.
174
- const apiUrl = marketFilter
175
- ? `${ADRENA_API_URL}/v1/markets?market=${encodeURIComponent(marketFilter)}`
176
- : `${ADRENA_API_URL}/v1/markets`;
177
- const apiResult = await timedFetch(apiUrl);
178
- if (apiResult && apiResult.length > 0) {
179
- const filtered = marketFilter
180
- ? apiResult.filter(m => m.symbol.toUpperCase() === marketFilter)
181
- : apiResult;
182
- return createTextResponse(JSON.stringify({
183
- source: 'adrena-api',
184
- markets: filtered,
185
- }));
186
- }
187
- // Fallback: read on-chain program accounts to discover markets.
188
164
  try {
189
- const accounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
190
- filters: [{ dataSize: 256 }],
165
+ // Fetch Custody accounts these contain per-token price, funding rate, and OI.
166
+ // We use memcmp on the first 8 bytes (Anchor account discriminator).
167
+ const custodyAccounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
168
+ filters: [
169
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_CUSTODY) } },
170
+ ],
191
171
  commitment: 'confirmed',
192
172
  });
193
- const markets = accounts.slice(0, 20).map(({ pubkey, account }) => ({
194
- address: pubkey.toBase58(),
195
- lamports: account.lamports,
196
- owner: account.owner.toBase58(),
197
- dataSize: account.data.length,
198
- }));
173
+ // Fetch Pool accounts these contain pool configuration and token info.
174
+ const poolAccounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
175
+ filters: [
176
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POOL) } },
177
+ ],
178
+ commitment: 'confirmed',
179
+ });
180
+ // Build a map of pool addresses for cross-referencing.
181
+ const poolAddresses = new Set(poolAccounts.map(({ pubkey }) => pubkey.toBase58()));
182
+ // Decode custody accounts into market data.
183
+ // Custody layout (simplified, after 8-byte discriminator):
184
+ // pool(32) + custodyMint(32) + symbol(String: 4+len) + decimals(u8) +
185
+ // ... + pricing data (price u64, fundingRate u64, openInterestLong u64, openInterestShort u64, ...)
186
+ // The exact offsets may vary by IDL version; we attempt common layouts.
187
+ const markets = [];
188
+ for (const { pubkey, account } of custodyAccounts) {
189
+ const data = account.data;
190
+ if (data.length < 8 + 32 + 32 + 4)
191
+ continue;
192
+ // After discriminator(8): pool(32) + custodyMint(32) = offset 72
193
+ const poolPubkey = new PublicKey(data.subarray(8, 8 + 32));
194
+ const custodyMint = new PublicKey(data.subarray(8 + 32, 8 + 64));
195
+ const poolAddress = poolPubkey.toBase58();
196
+ // Try to read symbol at offset 72 (after pool + custodyMint).
197
+ // Anchor String = 4-byte LE length + UTF-8 bytes.
198
+ let symbol = readSymbol(data, 8 + 64, 16);
199
+ // Try alternative offsets if symbol is empty (layout may differ).
200
+ if (!symbol) {
201
+ // Try after pool(32) only — some IDL versions place symbol before custodyMint.
202
+ symbol = readSymbol(data, 8 + 32, 16);
203
+ }
204
+ // Read decimals (u8) — typically right after the symbol string.
205
+ // We try offset after symbol, but since symbol length varies, we also
206
+ // try fixed offsets as fallback.
207
+ let decimals = 6;
208
+ const symbolEndOffset = 8 + 64 + 4 + (symbol ? symbol.length : 0);
209
+ if (symbolEndOffset < data.length) {
210
+ const dec = data[symbolEndOffset];
211
+ if (dec > 0 && dec <= 18)
212
+ decimals = dec;
213
+ }
214
+ // Price and funding data: attempt to read from deeper in the account.
215
+ // Common Adrena Custody layout has pricing fields after config section.
216
+ // We try multiple offsets; if reads fail, default to 0.
217
+ // Pricing section typically starts around offset 120-200 depending on symbol length.
218
+ let markPrice = 0;
219
+ let fundingRate = 0;
220
+ let openInterestLong = 0;
221
+ let openInterestShort = 0;
222
+ // Try reading price/funding at various known offsets.
223
+ // Adrena Custody has: ... config fields ... then pricing struct with:
224
+ // price(u64) + fundingRate(u64) + openInterestLong(u64) + openInterestShort(u64)
225
+ // We scan for plausible price data.
226
+ for (const baseOffset of [120, 144, 160, 176, 192, 200, 208, 224, 240]) {
227
+ if (baseOffset + 32 > data.length)
228
+ continue;
229
+ const price = readU64LE(data, baseOffset);
230
+ const funding = readU64LE(data, baseOffset + 8);
231
+ const oiLong = readU64LE(data, baseOffset + 16);
232
+ const oiShort = readU64LE(data, baseOffset + 24);
233
+ // Heuristic: price should be a reasonable value (> 0, not absurdly large)
234
+ // and funding should be small relative to price.
235
+ if (price > 0 && price < 1e15 && Math.abs(funding) < price) {
236
+ markPrice = price;
237
+ fundingRate = funding;
238
+ openInterestLong = oiLong;
239
+ openInterestShort = oiShort;
240
+ break;
241
+ }
242
+ }
243
+ const marketEntry = {
244
+ symbol: symbol || 'UNKNOWN',
245
+ custodyAddress: pubkey.toBase58(),
246
+ poolAddress,
247
+ custodyMint: custodyMint.toBase58(),
248
+ markPrice,
249
+ fundingRate,
250
+ openInterestLong,
251
+ openInterestShort,
252
+ decimals,
253
+ inPool: poolAddresses.has(poolAddress),
254
+ };
255
+ markets.push(marketEntry);
256
+ }
257
+ // Apply market filter if provided.
258
+ const filtered = marketFilter
259
+ ? markets.filter(m => m.symbol.toUpperCase() === marketFilter)
260
+ : markets;
199
261
  return createTextResponse(JSON.stringify({
200
262
  source: 'on-chain-rpc',
201
- markets,
202
- note: 'Adrena REST API was unreachable. Showing raw on-chain program accounts. Use sap_perp_position_info for position-level data.',
263
+ markets: filtered,
264
+ count: filtered.length,
265
+ totalCustodies: custodyAccounts.length,
266
+ totalPools: poolAccounts.length,
267
+ note: 'Data read directly from Solana on-chain Pool/Custody accounts. Use custodyAddress and poolAddress for analysis and sap_perp_trade_plan before any external execution route.',
203
268
  }));
204
269
  }
205
270
  catch (err) {
206
271
  return createTextResponse(JSON.stringify({
207
- error: 'Failed to fetch perp markets from both Adrena API and Solana RPC',
272
+ error: 'Failed to fetch perp markets from Solana RPC',
208
273
  message: err instanceof Error ? err.message : 'Unknown error',
209
274
  }), { isError: true });
210
275
  }
@@ -253,11 +318,12 @@ function registerPerpPositionInfoTool(server, context) {
253
318
  return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
254
319
  }
255
320
  try {
256
- // Adrena Position accounts contain the owner at a fixed offset.
257
- // We filter by data size matching the Position struct and memcmp on owner.
321
+ // Adrena Position accounts: filter by Position discriminator + memcmp on owner.
322
+ // Position layout (from IDL, after 8-byte discriminator):
323
+ // owner(32) + pool(32) + custody(32) + side(1) + price(u64) + sizeUsd(u64) + collateralUsd(u64) + ...
258
324
  const accounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
259
325
  filters: [
260
- { dataSize: 200 },
326
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
261
327
  { memcmp: { offset: 8, bytes: walletPubkey.toBase58() } },
262
328
  ],
263
329
  commitment: 'confirmed',
@@ -307,54 +373,153 @@ function registerPerpPositionInfoTool(server, context) {
307
373
  * @name registerPerpFundingHistoryTool
308
374
  * @description Register the sap_perp_funding_history read-only tool.
309
375
  *
310
- * Fetches funding rate history from the Adrena REST API.
376
+ * Computes the current funding rate from on-chain Custody account data.
377
+ * The agent obtains the custody address from `sap_perp_markets` and passes
378
+ * it here. Historical funding rate snapshots are not available on-chain
379
+ * (only the current rate embedded in the custody account); the tool returns
380
+ * the current funding state and a clear error if the custody address is
381
+ * missing or invalid.
311
382
  *
312
383
  * @param server — MCP server instance.
313
- * @param context — Runtime context (unused pure REST call).
384
+ * @param context — Runtime context with Solana RPC connection.
314
385
  *
315
386
  * @internal
316
387
  */
317
- function registerPerpFundingHistoryTool(server, _context) {
388
+ function registerPerpFundingHistoryTool(server, context) {
318
389
  const schema = {
319
390
  type: 'object',
320
391
  properties: {
392
+ custodyAddress: {
393
+ type: 'string',
394
+ description: 'Custody account public key (base58). Obtain from sap_perp_markets output — the custodyAddress field.',
395
+ },
321
396
  market: {
322
397
  type: 'string',
323
- description: 'Market symbol (e.g. "SOL", "BTC", "ETH").',
398
+ description: 'Optional market symbol (e.g. "SOL", "BTC") for display purposes.',
324
399
  },
325
400
  limit: {
326
401
  type: 'number',
327
- description: 'Maximum number of funding records to return (default 100).',
402
+ description: 'Maximum number of funding records to return (default 100). On-chain mode returns the current funding snapshot only.',
328
403
  minimum: 1,
329
404
  maximum: 1000,
330
405
  },
331
406
  },
332
- required: ['market'],
407
+ required: ['custodyAddress'],
333
408
  additionalProperties: false,
334
409
  };
335
410
  registerTool(server, 'sap_perp_funding_history', {
336
- description: 'Fetch historical funding rates for a perpetual market on Adrena. Returns timestamp, funding rate, and cumulative funding. Read-only — uses free Adrena REST API.',
411
+ 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.',
337
412
  inputSchema: schema,
338
413
  }, async (args) => {
414
+ const custodyAddressStr = typeof args['custodyAddress'] === 'string' ? args['custodyAddress'] : '';
339
415
  const market = typeof args['market'] === 'string' ? args['market'] : '';
340
- const limit = typeof args['limit'] === 'number' ? args['limit'] : 100;
341
- if (!market) {
342
- return createTextResponse(JSON.stringify({ error: 'market is required' }), { isError: true });
416
+ if (!custodyAddressStr) {
417
+ return createTextResponse(JSON.stringify({
418
+ error: 'custodyAddress is required. Use sap_perp_markets to get the custody address for the desired market.',
419
+ }), { isError: true });
343
420
  }
344
- const url = `${ADRENA_API_URL}/v1/funding-history?market=${encodeURIComponent(market)}&limit=${limit}`;
345
- const result = await timedFetch(url);
346
- if (!result) {
421
+ let custodyPubkey;
422
+ try {
423
+ custodyPubkey = new PublicKey(custodyAddressStr);
424
+ }
425
+ catch {
347
426
  return createTextResponse(JSON.stringify({
348
- error: 'Adrena funding history API unreachable',
349
- market,
350
- message: 'The Adrena REST API may be offline or rate-limited. Try again later.',
427
+ error: 'Invalid custody address',
428
+ custodyAddress: custodyAddressStr,
429
+ }), { isError: true });
430
+ }
431
+ try {
432
+ const accountInfo = await context.connection.getAccountInfo(custodyPubkey, 'confirmed');
433
+ if (!accountInfo) {
434
+ return createTextResponse(JSON.stringify({
435
+ error: 'Custody account not found on-chain',
436
+ custodyAddress: custodyAddressStr,
437
+ message: 'The custody account does not exist or has been closed. Use sap_perp_markets to find valid custody addresses.',
438
+ }), { isError: true });
439
+ }
440
+ const data = accountInfo.data;
441
+ if (data.length < 8 + 32 + 32) {
442
+ return createTextResponse(JSON.stringify({
443
+ error: 'Account data too short to be a valid Custody account',
444
+ custodyAddress: custodyAddressStr,
445
+ dataLength: data.length,
446
+ }), { isError: true });
447
+ }
448
+ // Verify discriminator matches Custody.
449
+ const disc = data.subarray(0, 8);
450
+ if (!disc.equals(DISC_CUSTODY)) {
451
+ return createTextResponse(JSON.stringify({
452
+ error: 'Account discriminator does not match Custody type',
453
+ custodyAddress: custodyAddressStr,
454
+ expectedDisc: Array.from(DISC_CUSTODY),
455
+ actualDisc: Array.from(disc),
456
+ message: 'The provided address is not a Custody account. Use sap_perp_markets to get the correct custodyAddress.',
457
+ }), { isError: true });
458
+ }
459
+ // Decode funding-related fields from the Custody account.
460
+ // Pool(32) + custodyMint(32) are at offset 8.
461
+ const poolPubkey = new PublicKey(data.subarray(8, 8 + 32));
462
+ const custodyMint = new PublicKey(data.subarray(8 + 32, 8 + 64));
463
+ // Read symbol at offset 72 (after pool + custodyMint).
464
+ let symbol = readSymbol(data, 8 + 64, 16);
465
+ if (!symbol) {
466
+ symbol = readSymbol(data, 8 + 32, 16);
467
+ }
468
+ // Scan for funding rate data at common offsets.
469
+ let markPrice = 0;
470
+ let fundingRate = 0;
471
+ let openInterestLong = 0;
472
+ let openInterestShort = 0;
473
+ let cumulativeFunding = 0;
474
+ for (const baseOffset of [120, 144, 160, 176, 192, 200, 208, 224, 240, 256]) {
475
+ if (baseOffset + 48 > data.length)
476
+ continue;
477
+ const price = readU64LE(data, baseOffset);
478
+ const funding = readU64LE(data, baseOffset + 8);
479
+ const oiLong = readU64LE(data, baseOffset + 16);
480
+ const oiShort = readU64LE(data, baseOffset + 24);
481
+ const cumFunding = readU64LE(data, baseOffset + 32);
482
+ if (price > 0 && price < 1e15 && Math.abs(funding) < price) {
483
+ markPrice = price;
484
+ fundingRate = funding;
485
+ openInterestLong = oiLong;
486
+ openInterestShort = oiShort;
487
+ cumulativeFunding = cumFunding;
488
+ break;
489
+ }
490
+ }
491
+ const timestamp = Date.now();
492
+ return createTextResponse(JSON.stringify({
493
+ source: 'on-chain-rpc',
494
+ custodyAddress: custodyAddressStr,
495
+ market: market || symbol || 'UNKNOWN',
496
+ symbol: symbol || 'UNKNOWN',
497
+ poolAddress: poolPubkey.toBase58(),
498
+ custodyMint: custodyMint.toBase58(),
499
+ currentFunding: {
500
+ timestamp,
501
+ fundingRate,
502
+ cumulativeFunding,
503
+ markPrice,
504
+ openInterestLong,
505
+ openInterestShort,
506
+ },
507
+ records: [{
508
+ timestamp,
509
+ fundingRate,
510
+ cumulativeFunding,
511
+ }],
512
+ count: 1,
513
+ note: 'On-chain data provides the current funding snapshot only. Historical time-series funding data is not available via Solana RPC — use sap_perp_markets to monitor funding rate changes over time.',
514
+ }));
515
+ }
516
+ catch (err) {
517
+ return createTextResponse(JSON.stringify({
518
+ error: 'Failed to read funding data from on-chain custody account',
519
+ custodyAddress: custodyAddressStr,
520
+ message: err instanceof Error ? err.message : 'Unknown error',
351
521
  }), { isError: true });
352
522
  }
353
- return createTextResponse(JSON.stringify({
354
- market,
355
- records: result,
356
- count: result.length,
357
- }));
358
523
  });
359
524
  }
360
525
  /* ═══════════════════════════════════════════════════════════════════
@@ -733,7 +898,7 @@ function registerPerpLiquidationZonesTool(server, context) {
733
898
  try {
734
899
  const accounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
735
900
  filters: [
736
- { dataSize: 200 },
901
+ { memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
737
902
  { memcmp: { offset: 8, bytes: walletPubkey.toBase58() } },
738
903
  ],
739
904
  commitment: 'confirmed',
@@ -781,312 +946,168 @@ function registerPerpLiquidationZonesTool(server, context) {
781
946
  });
782
947
  }
783
948
  /* ═══════════════════════════════════════════════════════════════════
784
- * Tool 8: sap_perp_build_open (inscribedTool)
949
+ * Tool 8: sap_perp_trade_plan
785
950
  * ═══════════════════════════════════════════════════════════════════ */
786
951
  /**
787
- * @name registerPerpBuildOpenTool
788
- * @description Register the sap_perp_build_open inscribedTool.
952
+ * @name registerPerpTradePlanTool
953
+ * @description Register the professional perps planning tool.
789
954
  *
790
- * Builds an unsigned transaction for opening a leveraged perp position on
791
- * Adrena. The agent signs locally no server-side signing keys.
955
+ * This tool does not build a transaction. It turns an intent into a compact
956
+ * risk, sizing, and execution checklist so agents can act like traders while
957
+ * avoiding fake or incomplete unsigned transaction builders.
792
958
  *
793
- * @param server — MCP server instance.
794
- * @param context — Runtime context with Solana RPC connection.
959
+ * @param server — MCP server instance.
795
960
  *
796
961
  * @internal
797
962
  */
798
- function registerPerpBuildOpenTool(server, _context) {
963
+ function registerPerpTradePlanTool(server) {
799
964
  const schema = {
800
965
  type: 'object',
801
966
  properties: {
802
- wallet: {
803
- type: 'string',
804
- description: 'Wallet public key (base58) — the signer and fee payer.',
805
- },
806
967
  market: {
807
968
  type: 'string',
808
- description: 'Market symbol (e.g. "SOL", "BTC", "ETH").',
969
+ description: 'Perp market symbol or pair, for example SOL-PERP, BTC-PERP, or ETH-PERP.',
809
970
  },
810
971
  side: {
811
972
  type: 'string',
812
- description: 'Position direction.',
973
+ description: 'Intended direction for the trade.',
813
974
  enum: ['long', 'short'],
814
975
  },
815
- collateralMint: {
816
- type: 'string',
817
- description: 'Collateral token mint address (base58, e.g. USDC mint).',
818
- },
819
- collateralAmount: {
976
+ collateralAmountUsd: {
820
977
  type: 'number',
821
- description: 'Collateral amount in raw token units (account for decimals).',
822
- minimum: 1,
978
+ description: 'Collateral to allocate in USD. This is the margin budget, not notional size.',
979
+ minimum: 0,
823
980
  },
824
981
  leverage: {
825
982
  type: 'number',
826
- description: 'Leverage multiplier (1-100 for Adrena).',
983
+ description: 'Requested leverage multiplier. Keep conservative unless user policy explicitly allows more.',
827
984
  minimum: 1,
828
985
  maximum: 100,
829
986
  },
830
- stopLoss: {
987
+ entryPrice: {
831
988
  type: 'number',
832
- description: 'Optional stop-loss price in USD.',
989
+ description: 'Reference entry price in USD used for risk math.',
990
+ minimum: 0,
833
991
  },
834
- takeProfit: {
992
+ stopLossPrice: {
835
993
  type: 'number',
836
- description: 'Optional take-profit price in USD.',
837
- },
838
- },
839
- required: ['wallet', 'market', 'side', 'collateralMint', 'collateralAmount', 'leverage'],
840
- additionalProperties: false,
841
- };
842
- registerTool(server, 'sap_perp_build_open', {
843
- description: 'Build an unsigned transaction to open a leveraged perpetual position on Adrena. Returns serialized base64 transaction for the agent to sign locally. No server-side signing — the agent uses sap_sign_transaction and sap_submit_signed_transaction.',
844
- inputSchema: schema,
845
- }, async (args) => {
846
- const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
847
- const side = args['side'];
848
- const collateralAmount = args['collateralAmount'];
849
- const leverage = args['leverage'];
850
- const stopLoss = typeof args['stopLoss'] === 'number' ? args['stopLoss'] : 0;
851
- const takeProfit = typeof args['takeProfit'] === 'number' ? args['takeProfit'] : 0;
852
- let walletPubkey;
853
- try {
854
- walletPubkey = new PublicKey(walletStr);
855
- }
856
- catch {
857
- return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
858
- }
859
- // Build instruction data: discriminator(8) + price(u64) + collateral(u64) + leverage(u32) + stopLoss(u64) + takeProfit(u64)
860
- const disc = side === 'long' ? DISC_OPEN_LONG : DISC_OPEN_SHORT;
861
- const dataLen = 8 + 8 + 8 + 4 + 8 + 8;
862
- const data = Buffer.alloc(dataLen);
863
- let offset = 0;
864
- disc.copy(data, offset);
865
- offset += 8;
866
- writeNumberU64LE(data, takeProfit, offset);
867
- offset += 8; // price (take-profit as limit price, 0 for market)
868
- writeNumberU64LE(data, collateralAmount, offset);
869
- offset += 8;
870
- writeNumberU32LE(data, leverage, offset);
871
- offset += 4;
872
- writeNumberU64LE(data, stopLoss, offset);
873
- offset += 8;
874
- writeNumberU64LE(data, takeProfit, offset);
875
- offset += 8;
876
- // Build instruction with required Adrena accounts.
877
- // Real account layout from IDL: signer, pool, custody, collateral_custody, position, userProfile, system_program, token_program
878
- const instruction = new TransactionInstruction({
879
- programId: ADRENA_PROGRAM_ID,
880
- keys: [
881
- { pubkey: walletPubkey, isSigner: true, isWritable: true },
882
- // Pool and custody accounts must be derived per-market at runtime.
883
- // The agent or frontend resolves these from the Adrena SDK or on-chain registry.
884
- // We include placeholder pubkeys that the agent must replace before signing.
885
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
886
- ],
887
- data,
888
- });
889
- const result = buildInscribedTx(instruction);
890
- return createTextResponse(JSON.stringify({
891
- ...result,
892
- side,
893
- leverage,
894
- collateralAmount,
895
- stopLoss,
896
- takeProfit,
897
- note: 'Transaction is unsigned. The agent must resolve pool/custody/position accounts from the Adrena SDK or on-chain registry, then sign locally with sap_sign_transaction.',
898
- }));
899
- });
900
- }
901
- /* ═══════════════════════════════════════════════════════════════════
902
- * Tool 9: sap_perp_build_close (inscribedTool)
903
- * ═══════════════════════════════════════════════════════════════════ */
904
- /**
905
- * @name registerPerpBuildCloseTool
906
- * @description Register the sap_perp_build_close inscribedTool.
907
- *
908
- * @param server — MCP server instance.
909
- * @param context — Runtime context (unused — builds instruction only).
910
- *
911
- * @internal
912
- */
913
- function registerPerpBuildCloseTool(server, _context) {
914
- const schema = {
915
- type: 'object',
916
- properties: {
917
- wallet: {
918
- type: 'string',
919
- description: 'Wallet public key (base58) — the signer.',
920
- },
921
- positionKey: {
922
- type: 'string',
923
- description: 'Adrena position account public key (base58) to close.',
994
+ description: 'Optional stop loss price in USD. Strongly recommended before execution.',
995
+ minimum: 0,
924
996
  },
925
- side: {
926
- type: 'string',
927
- description: 'Position side (determines which instruction discriminator to use).',
928
- enum: ['long', 'short'],
997
+ takeProfitPrice: {
998
+ type: 'number',
999
+ description: 'Optional take profit price in USD used to compute reward/risk.',
1000
+ minimum: 0,
929
1001
  },
930
- percentage: {
1002
+ maxAccountRiskPct: {
931
1003
  type: 'number',
932
- description: 'Percentage of position to close (1-100, default 100 = full close).',
933
- minimum: 1,
1004
+ description: 'Maximum account risk percentage allowed by local policy. Default 1%.',
1005
+ minimum: 0,
934
1006
  maximum: 100,
935
1007
  },
936
- },
937
- required: ['wallet', 'positionKey', 'side'],
938
- additionalProperties: false,
939
- };
940
- registerTool(server, 'sap_perp_build_close', {
941
- description: 'Build an unsigned transaction to close a perpetual position on Adrena. Returns serialized base64 transaction for local signing. Supports partial closes via percentage parameter.',
942
- inputSchema: schema,
943
- }, async (args) => {
944
- const walletStr = args['wallet'];
945
- const positionKeyStr = args['positionKey'];
946
- const side = args['side'];
947
- const percentage = typeof args['percentage'] === 'number' ? args['percentage'] : 100;
948
- let walletPubkey;
949
- let positionPubkey;
950
- try {
951
- walletPubkey = new PublicKey(walletStr);
952
- positionPubkey = new PublicKey(positionKeyStr);
953
- }
954
- catch {
955
- return createTextResponse(JSON.stringify({ error: 'Invalid wallet or position key' }), { isError: true });
956
- }
957
- const disc = side === 'long' ? DISC_CLOSE_LONG : DISC_CLOSE_SHORT;
958
- // close_position params: price(Option<u64>) + percentage(u64)
959
- const dataLen = 8 + 8 + 8;
960
- const data = Buffer.alloc(dataLen);
961
- let offset = 0;
962
- disc.copy(data, offset);
963
- offset += 8;
964
- writeNumberU64LE(data, 0, offset);
965
- offset += 8; // price = 0 (market close)
966
- writeNumberU64LE(data, percentage * 1_000_000, offset);
967
- offset += 8; // percentage in u64
968
- const instruction = new TransactionInstruction({
969
- programId: ADRENA_PROGRAM_ID,
970
- keys: [
971
- { pubkey: walletPubkey, isSigner: true, isWritable: true },
972
- { pubkey: positionPubkey, isSigner: false, isWritable: true },
973
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
974
- ],
975
- data,
976
- });
977
- const result = buildInscribedTx(instruction);
978
- return createTextResponse(JSON.stringify({
979
- ...result,
980
- positionKey: positionKeyStr,
981
- side,
982
- percentage,
983
- note: 'Transaction is unsigned. Resolve pool/custody accounts from the Adrena SDK before signing locally.',
984
- }));
985
- });
986
- }
987
- /* ═══════════════════════════════════════════════════════════════════
988
- * Tool 10: sap_perp_build_modify (inscribedTool)
989
- * ═══════════════════════════════════════════════════════════════════ */
990
- /**
991
- * @name registerPerpBuildModifyTool
992
- * @description Register the sap_perp_build_modify inscribedTool.
993
- *
994
- * @param server — MCP server instance.
995
- * @param context — Runtime context (unused — builds instruction only).
996
- *
997
- * @internal
998
- */
999
- function registerPerpBuildModifyTool(server, _context) {
1000
- const schema = {
1001
- type: 'object',
1002
- properties: {
1003
- wallet: {
1004
- type: 'string',
1005
- description: 'Wallet public key (base58) — the signer.',
1006
- },
1007
- positionKey: {
1008
- type: 'string',
1009
- description: 'Adrena position account public key (base58).',
1008
+ maxSlippageBps: {
1009
+ type: 'number',
1010
+ description: 'Maximum execution slippage in basis points. Default 50 bps.',
1011
+ minimum: 0,
1012
+ maximum: 10_000,
1010
1013
  },
1011
- side: {
1014
+ timeframe: {
1012
1015
  type: 'string',
1013
- description: 'Position side (determines instruction discriminator).',
1014
- enum: ['long', 'short'],
1016
+ description: 'Trading horizon such as scalp, intraday, swing, or hedge.',
1015
1017
  },
1016
- action: {
1018
+ notes: {
1017
1019
  type: 'string',
1018
- description: 'Collateral action.',
1019
- enum: ['add', 'remove'],
1020
- },
1021
- amount: {
1022
- type: 'number',
1023
- description: 'Collateral amount in raw token units.',
1024
- minimum: 1,
1020
+ description: 'Optional user notes, catalyst, invalidation thesis, or strategy context.',
1025
1021
  },
1026
1022
  },
1027
- required: ['wallet', 'positionKey', 'side', 'action', 'amount'],
1023
+ required: ['market', 'side', 'collateralAmountUsd', 'leverage', 'entryPrice'],
1028
1024
  additionalProperties: false,
1029
1025
  };
1030
- registerTool(server, 'sap_perp_build_modify', {
1031
- description: 'Build an unsigned transaction to add or remove collateral from an Adrena perp position. Returns serialized base64 transaction for local signing.',
1026
+ registerTool(server, 'sap_perp_trade_plan', {
1027
+ 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.',
1032
1028
  inputSchema: schema,
1033
1029
  }, async (args) => {
1034
- const walletStr = args['wallet'];
1035
- const positionKeyStr = args['positionKey'];
1036
- const side = args['side'];
1037
- const action = args['action'];
1038
- const amount = args['amount'];
1039
- let walletPubkey;
1040
- let positionPubkey;
1041
- try {
1042
- walletPubkey = new PublicKey(walletStr);
1043
- positionPubkey = new PublicKey(positionKeyStr);
1030
+ const market = String(args['market'] ?? '').trim().toUpperCase();
1031
+ const side = args['side'] === 'short' ? 'short' : 'long';
1032
+ const collateralAmountUsd = typeof args['collateralAmountUsd'] === 'number' ? args['collateralAmountUsd'] : 0;
1033
+ const leverage = typeof args['leverage'] === 'number' ? args['leverage'] : 1;
1034
+ const entryPrice = typeof args['entryPrice'] === 'number' ? args['entryPrice'] : 0;
1035
+ const stopLossPrice = typeof args['stopLossPrice'] === 'number' ? args['stopLossPrice'] : null;
1036
+ const takeProfitPrice = typeof args['takeProfitPrice'] === 'number' ? args['takeProfitPrice'] : null;
1037
+ const maxAccountRiskPct = typeof args['maxAccountRiskPct'] === 'number' ? args['maxAccountRiskPct'] : 1;
1038
+ const maxSlippageBps = typeof args['maxSlippageBps'] === 'number' ? args['maxSlippageBps'] : 50;
1039
+ if (!market || collateralAmountUsd <= 0 || leverage <= 0 || entryPrice <= 0) {
1040
+ return createTextResponse(JSON.stringify({
1041
+ error: 'market, collateralAmountUsd, leverage, and entryPrice are required and must be positive.',
1042
+ }), { isError: true });
1044
1043
  }
1045
- catch {
1046
- return createTextResponse(JSON.stringify({ error: 'Invalid wallet or position key' }), { isError: true });
1044
+ const notionalUsd = collateralAmountUsd * leverage;
1045
+ const stopMovePct = stopLossPrice && stopLossPrice > 0
1046
+ ? Math.abs((entryPrice - stopLossPrice) / entryPrice) * 100
1047
+ : null;
1048
+ const takeProfitMovePct = takeProfitPrice && takeProfitPrice > 0
1049
+ ? Math.abs((takeProfitPrice - entryPrice) / entryPrice) * 100
1050
+ : null;
1051
+ const estimatedStopRiskUsd = stopMovePct === null ? null : notionalUsd * (stopMovePct / 100);
1052
+ const estimatedRewardUsd = takeProfitMovePct === null ? null : notionalUsd * (takeProfitMovePct / 100);
1053
+ const rewardRisk = estimatedStopRiskUsd && estimatedRewardUsd
1054
+ ? estimatedRewardUsd / estimatedStopRiskUsd
1055
+ : null;
1056
+ const liquidationEstimate = side === 'long'
1057
+ ? entryPrice * (1 - (1 / leverage))
1058
+ : entryPrice * (1 + (1 / leverage));
1059
+ const riskFlags = [];
1060
+ if (!stopLossPrice)
1061
+ riskFlags.push('missing_stop_loss');
1062
+ if (estimatedStopRiskUsd !== null && estimatedStopRiskUsd > collateralAmountUsd * (maxAccountRiskPct / 100)) {
1063
+ riskFlags.push('stop_risk_exceeds_policy');
1047
1064
  }
1048
- // Select discriminator based on action + side.
1049
- const disc = action === 'add'
1050
- ? (side === 'long' ? DISC_ADD_COLLATERAL_LONG : DISC_ADD_COLLATERAL_SHORT)
1051
- : (side === 'long' ? DISC_REMOVE_COLLATERAL_LONG : DISC_REMOVE_COLLATERAL_SHORT);
1052
- // add_collateral params: collateral(u64)
1053
- // remove_collateral params: collateral_usd(u64)
1054
- const dataLen = 8 + 8;
1055
- const data = Buffer.alloc(dataLen);
1056
- let offset = 0;
1057
- disc.copy(data, offset);
1058
- offset += 8;
1059
- writeNumberU64LE(data, amount, offset);
1060
- offset += 8;
1061
- const instruction = new TransactionInstruction({
1062
- programId: ADRENA_PROGRAM_ID,
1063
- keys: [
1064
- { pubkey: walletPubkey, isSigner: true, isWritable: true },
1065
- { pubkey: positionPubkey, isSigner: false, isWritable: true },
1066
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1067
- ],
1068
- data,
1069
- });
1070
- const result = buildInscribedTx(instruction);
1065
+ if (leverage > 10)
1066
+ riskFlags.push('high_leverage_requires_explicit_user_confirmation');
1067
+ if (maxSlippageBps > 100)
1068
+ riskFlags.push('slippage_above_1_percent');
1071
1069
  return createTextResponse(JSON.stringify({
1072
- ...result,
1073
- positionKey: positionKeyStr,
1074
- action,
1070
+ market,
1075
1071
  side,
1076
- amount,
1077
- note: 'Transaction is unsigned. Resolve custody/collateral accounts from the Adrena SDK before signing locally.',
1078
- }));
1072
+ executionStatus: 'analysis_only',
1073
+ collateralAmountUsd,
1074
+ leverage,
1075
+ notionalUsd,
1076
+ entryPrice,
1077
+ stopLossPrice,
1078
+ takeProfitPrice,
1079
+ maxAccountRiskPct,
1080
+ maxSlippageBps,
1081
+ liquidationEstimate,
1082
+ estimatedStopRiskUsd,
1083
+ estimatedRewardUsd,
1084
+ rewardRisk,
1085
+ riskFlags,
1086
+ professionalChecklist: [
1087
+ 'Call sap_perp_markets for current market/custody data before execution.',
1088
+ 'Call sap_chart_ohlc and sap_chart_volume_profile to validate trend, liquidity, POC, VAH, and VAL.',
1089
+ 'Call sap_perp_position_info and sap_perp_liquidation_zones for the user wallet before increasing exposure.',
1090
+ 'Show the user one compact preview: side, notional, margin, leverage, stop, take profit, liquidation estimate, slippage, and risk flags.',
1091
+ '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.',
1092
+ ],
1093
+ recommendedReadTools: [
1094
+ 'sap_perp_markets',
1095
+ 'sap_chart_ohlc',
1096
+ 'sap_chart_volume_profile',
1097
+ 'sap_perp_position_info',
1098
+ 'sap_perp_liquidation_zones',
1099
+ ],
1100
+ executionWarning: 'SAP MCP 0.9.35 intentionally does not expose manual Adrena execution builders. Execution must use a complete IDL-backed route before local finalization.',
1101
+ notes: typeof args['notes'] === 'string' ? args['notes'] : undefined,
1102
+ }, null, 2));
1079
1103
  });
1080
1104
  }
1081
- /* ═══════════════════════════════════════════════════════════════════
1082
- * Import SystemProgram for inscribed tools
1083
- * ═══════════════════════════════════════════════════════════════════ */
1084
1105
  /* ═══════════════════════════════════════════════════════════════════
1085
1106
  * Main registration function
1086
1107
  * ═══════════════════════════════════════════════════════════════════ */
1087
1108
  /**
1088
1109
  * @name registerPerpTools
1089
- * @description Register all 10 perp trading and chart analysis MCP tools.
1110
+ * @description Register all 8 perp trading analytics and planning MCP tools.
1090
1111
  *
1091
1112
  * @param server — MCP server instance.
1092
1113
  * @param context — Shared runtime context with SAP client, connection, and config.
@@ -1094,7 +1115,7 @@ function registerPerpBuildModifyTool(server, _context) {
1094
1115
  * @usedBy `register-tools.ts`
1095
1116
  */
1096
1117
  export function registerPerpTools(server, context) {
1097
- logger.debug('Registering perp trading and chart tools');
1118
+ logger.debug('Registering perp trading analytics and planning tools');
1098
1119
  registerPerpMarketsTool(server, context);
1099
1120
  registerPerpPositionInfoTool(server, context);
1100
1121
  registerPerpFundingHistoryTool(server, context);
@@ -1102,9 +1123,7 @@ export function registerPerpTools(server, context) {
1102
1123
  registerChartLongTermTool(server, context);
1103
1124
  registerChartVolumeProfileTool(server, context);
1104
1125
  registerPerpLiquidationZonesTool(server, context);
1105
- registerPerpBuildOpenTool(server, context);
1106
- registerPerpBuildCloseTool(server, context);
1107
- registerPerpBuildModifyTool(server, context);
1108
- logger.debug('Perp tools registered', { count: 10 });
1126
+ registerPerpTradePlanTool(server);
1127
+ logger.debug('Perp tools registered', { count: 8 });
1109
1128
  }
1110
1129
  //# sourceMappingURL=perp-tools.js.map