@oobe-protocol-labs/sap-mcp-server 0.9.22 → 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.
- package/.env.example +10 -0
- package/CHANGELOG.md +100 -6
- package/README.md +3 -3
- package/USER_DOCS/00_START_HERE.md +9 -1
- package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +1 -1
- package/USER_DOCS/04_CLIENT_CONFIGS.md +3 -3
- package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +5 -5
- package/dist/config/env.d.ts +18 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +14 -2
- package/dist/config/env.js.map +1 -1
- package/dist/config/mcp-client-injection.js +3 -3
- package/dist/config/paths.d.ts +9 -1
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +15 -1
- package/dist/config/paths.js.map +1 -1
- package/dist/config/wizard.d.ts.map +1 -1
- package/dist/config/wizard.js +3 -1
- package/dist/config/wizard.js.map +1 -1
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/logger.js +1 -1
- package/dist/memory/async-processor.d.ts +107 -0
- package/dist/memory/async-processor.d.ts.map +1 -0
- package/dist/memory/async-processor.js +204 -0
- package/dist/memory/async-processor.js.map +1 -0
- package/dist/memory/auto-record.d.ts +47 -0
- package/dist/memory/auto-record.d.ts.map +1 -0
- package/dist/memory/auto-record.js +133 -0
- package/dist/memory/auto-record.js.map +1 -0
- package/dist/memory/database.d.ts +103 -0
- package/dist/memory/database.d.ts.map +1 -0
- package/dist/memory/database.js +337 -0
- package/dist/memory/database.js.map +1 -0
- package/dist/memory/hermes-bridge.d.ts +70 -0
- package/dist/memory/hermes-bridge.d.ts.map +1 -0
- package/dist/memory/hermes-bridge.js +155 -0
- package/dist/memory/hermes-bridge.js.map +1 -0
- package/dist/memory/index.d.ts +23 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +22 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory-store.d.ts +76 -0
- package/dist/memory/memory-store.d.ts.map +1 -0
- package/dist/memory/memory-store.js +231 -0
- package/dist/memory/memory-store.js.map +1 -0
- package/dist/memory/stream-buffer-store.d.ts +69 -0
- package/dist/memory/stream-buffer-store.d.ts.map +1 -0
- package/dist/memory/stream-buffer-store.js +182 -0
- package/dist/memory/stream-buffer-store.js.map +1 -0
- package/dist/memory/tool-call-store.d.ts +68 -0
- package/dist/memory/tool-call-store.d.ts.map +1 -0
- package/dist/memory/tool-call-store.js +189 -0
- package/dist/memory/tool-call-store.js.map +1 -0
- package/dist/memory/types.d.ts +197 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +15 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/memory/utils.d.ts +30 -0
- package/dist/memory/utils.d.ts.map +1 -0
- package/dist/memory/utils.js +47 -0
- package/dist/memory/utils.js.map +1 -0
- package/dist/payments/hosted-tool-eligibility.d.ts.map +1 -1
- package/dist/payments/hosted-tool-eligibility.js +4 -6
- package/dist/payments/hosted-tool-eligibility.js.map +1 -1
- package/dist/payments/monetization-gate.d.ts +39 -0
- package/dist/payments/monetization-gate.d.ts.map +1 -1
- package/dist/payments/monetization-gate.js +165 -11
- package/dist/payments/monetization-gate.js.map +1 -1
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +69 -0
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/x402-paid-call.js +1 -1
- package/dist/premium/builtin-plugins.d.ts +42 -0
- package/dist/premium/builtin-plugins.d.ts.map +1 -1
- package/dist/premium/builtin-plugins.js +91 -10
- package/dist/premium/builtin-plugins.js.map +1 -1
- package/dist/premium/manifest-builder.d.ts.map +1 -1
- package/dist/premium/manifest-builder.js +43 -7
- package/dist/premium/manifest-builder.js.map +1 -1
- package/dist/premium/meme-radar-capabilities.js +9 -9
- package/dist/premium/meme-radar-capabilities.js.map +1 -1
- package/dist/premium/plugin-validator.d.ts.map +1 -1
- package/dist/premium/plugin-validator.js +62 -1
- package/dist/premium/plugin-validator.js.map +1 -1
- package/dist/premium/provider-bridge.d.ts +25 -1
- package/dist/premium/provider-bridge.d.ts.map +1 -1
- package/dist/premium/provider-bridge.js +130 -20
- package/dist/premium/provider-bridge.js.map +1 -1
- package/dist/premium/session-manager.d.ts.map +1 -1
- package/dist/premium/session-manager.js +12 -3
- package/dist/premium/session-manager.js.map +1 -1
- package/dist/premium/tech-fundamentals-capabilities.js +5 -5
- package/dist/premium/tech-fundamentals-capabilities.js.map +1 -1
- package/dist/premium/trading-capabilities.js +5 -5
- package/dist/premium/trading-capabilities.js.map +1 -1
- package/dist/premium/webhook-engine.d.ts.map +1 -1
- package/dist/premium/webhook-engine.js +7 -1
- package/dist/premium/webhook-engine.js.map +1 -1
- package/dist/prompts/context/sap-agent-context.prompt.js +1 -1
- package/dist/prompts/payments/explain-x402-settlement.prompt.js +1 -1
- package/dist/remote/premium-routes.d.ts.map +1 -1
- package/dist/remote/premium-routes.js +3 -2
- package/dist/remote/premium-routes.js.map +1 -1
- package/dist/remote/public-home/sections.d.ts.map +1 -1
- package/dist/remote/public-home/sections.js +2 -3
- package/dist/remote/public-home/sections.js.map +1 -1
- package/dist/remote/public-home/styles.d.ts +1 -1
- package/dist/remote/public-home/styles.js +2 -2
- package/dist/remote/server.d.ts.map +1 -1
- package/dist/remote/server.js +19 -4
- package/dist/remote/server.js.map +1 -1
- package/dist/server/server-metadata.js +1 -1
- package/dist/strategies/strategy-store.d.ts +82 -0
- package/dist/strategies/strategy-store.d.ts.map +1 -0
- package/dist/strategies/strategy-store.js +165 -0
- package/dist/strategies/strategy-store.js.map +1 -0
- package/dist/tools/agent-start-tool.d.ts.map +1 -1
- package/dist/tools/agent-start-tool.js +97 -10
- package/dist/tools/agent-start-tool.js.map +1 -1
- package/dist/tools/client-sdk-tools.d.ts.map +1 -1
- package/dist/tools/client-sdk-tools.js +152 -9
- package/dist/tools/client-sdk-tools.js.map +1 -1
- package/dist/tools/estimate-tool-cost.js +1 -1
- package/dist/tools/estimate-tool-cost.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/magicblock-tools.js +5 -5
- package/dist/tools/magicblock-tools.js.map +1 -1
- package/dist/tools/memory-tools.d.ts +26 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +533 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/perp-tools.d.ts +7 -9
- package/dist/tools/perp-tools.d.ts.map +1 -1
- package/dist/tools/perp-tools.js +386 -367
- package/dist/tools/perp-tools.js.map +1 -1
- package/dist/tools/premium-tools.d.ts.map +1 -1
- package/dist/tools/premium-tools.js +8 -2
- package/dist/tools/premium-tools.js.map +1 -1
- package/dist/tools/profile-tools.d.ts.map +1 -1
- package/dist/tools/profile-tools.js +18 -1
- package/dist/tools/profile-tools.js.map +1 -1
- package/dist/tools/quick-context-tool.d.ts.map +1 -1
- package/dist/tools/quick-context-tool.js +100 -9
- package/dist/tools/quick-context-tool.js.map +1 -1
- package/dist/tools/register-tools.d.ts.map +1 -1
- package/dist/tools/register-tools.js +3 -0
- package/dist/tools/register-tools.js.map +1 -1
- package/dist/tools/sap-sdk-tools.js +3 -3
- package/dist/tools/sap-sdk-tools.js.map +1 -1
- package/dist/tools/session-context-packet.d.ts +19 -0
- package/dist/tools/session-context-packet.d.ts.map +1 -0
- package/dist/tools/session-context-packet.js +256 -0
- package/dist/tools/session-context-packet.js.map +1 -0
- package/dist/tools/skills-tools.d.ts +10 -0
- package/dist/tools/skills-tools.d.ts.map +1 -1
- package/dist/tools/skills-tools.js +19 -0
- package/dist/tools/skills-tools.js.map +1 -1
- package/dist/tools/transaction-tools.d.ts +1 -0
- package/dist/tools/transaction-tools.d.ts.map +1 -1
- package/dist/tools/transaction-tools.js +248 -9
- package/dist/tools/transaction-tools.js.map +1 -1
- package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
- package/dist/tools/x402-paid-call-tool.js +55 -21
- package/dist/tools/x402-paid-call-tool.js.map +1 -1
- package/dist/wizard-core/desktop-flow.d.ts.map +1 -1
- package/dist/wizard-core/desktop-flow.js +4 -2
- package/dist/wizard-core/desktop-flow.js.map +1 -1
- package/docs/06_PAYMENTS_X402_AND_PAYSH.md +14 -5
- package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
- package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +13 -5
- package/docs/13_BOUNTY_PROGRAM_PROPOSAL.md +1 -1
- package/docs/14_DESKTOP_WIZARD_RELEASE.md +3 -3
- package/docs/15_DASHBOARD_SCREENSHARE_SCRIPT.md +1 -1
- package/docs/BRANCH_AND_CI.md +2 -2
- package/docs/README.md +1 -1
- package/package.json +3 -1
- package/server.json +3 -3
- package/skills/README.md +1 -1
- package/skills/sap-agentkit/SKILL.md +1 -2
- package/skills/sap-defi/SKILL.md +31 -2
- package/skills/sap-mcp/SKILL.md +24 -13
- package/skills/sap-mcp/TOOL_REFERENCE.md +2 -4
- package/skills/sap-payments-x402/SKILL.md +15 -3
- package/assets/logos/drift.svg +0 -12
package/dist/tools/perp-tools.js
CHANGED
|
@@ -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
|
|
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 —
|
|
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
|
-
*
|
|
17
|
-
* -
|
|
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).
|
|
23
|
-
*
|
|
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
|
|
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
|
|
45
|
+
* Adrena account discriminators (Anchor 0.31 — sha256("account:<Name>")[0..8])
|
|
50
46
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
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
|
|
91
|
-
* @description
|
|
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
|
|
94
|
-
* @returns
|
|
88
|
+
* @param disc — 8-byte discriminator Buffer.
|
|
89
|
+
* @returns Base58-encoded string.
|
|
95
90
|
*
|
|
96
91
|
* @internal
|
|
97
92
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
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
|
|
116
|
-
* @description
|
|
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 —
|
|
119
|
-
* @param
|
|
120
|
-
* @
|
|
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
|
|
125
|
-
|
|
126
|
-
buf.writeBigUInt64LE(bigVal, offset);
|
|
106
|
+
function readU64LE(buf, offset) {
|
|
107
|
+
return Number(buf.readBigUInt64LE(offset));
|
|
127
108
|
}
|
|
128
109
|
/**
|
|
129
|
-
* @name
|
|
130
|
-
* @description
|
|
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 —
|
|
133
|
-
* @param
|
|
134
|
-
* @param
|
|
114
|
+
* @param buf — Source buffer.
|
|
115
|
+
* @param offset — Offset to the string field.
|
|
116
|
+
* @param maxLen — Maximum bytes to read (default 16).
|
|
117
|
+
* @returns Decoded symbol string, or empty string on failure.
|
|
135
118
|
*
|
|
136
119
|
* @internal
|
|
137
120
|
*/
|
|
138
|
-
function
|
|
139
|
-
|
|
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
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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 —
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
257
|
-
//
|
|
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
|
-
{
|
|
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
|
-
*
|
|
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
|
|
384
|
+
* @param context — Runtime context with Solana RPC connection.
|
|
314
385
|
*
|
|
315
386
|
* @internal
|
|
316
387
|
*/
|
|
317
|
-
function registerPerpFundingHistoryTool(server,
|
|
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: '
|
|
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: ['
|
|
407
|
+
required: ['custodyAddress'],
|
|
333
408
|
additionalProperties: false,
|
|
334
409
|
};
|
|
335
410
|
registerTool(server, 'sap_perp_funding_history', {
|
|
336
|
-
description: '
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
421
|
+
let custodyPubkey;
|
|
422
|
+
try {
|
|
423
|
+
custodyPubkey = new PublicKey(custodyAddressStr);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
347
426
|
return createTextResponse(JSON.stringify({
|
|
348
|
-
error: '
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
{
|
|
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:
|
|
949
|
+
* Tool 8: sap_perp_trade_plan
|
|
785
950
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
786
951
|
/**
|
|
787
|
-
* @name
|
|
788
|
-
* @description Register the
|
|
952
|
+
* @name registerPerpTradePlanTool
|
|
953
|
+
* @description Register the professional perps planning tool.
|
|
789
954
|
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
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
|
|
794
|
-
* @param context — Runtime context with Solana RPC connection.
|
|
959
|
+
* @param server — MCP server instance.
|
|
795
960
|
*
|
|
796
961
|
* @internal
|
|
797
962
|
*/
|
|
798
|
-
function
|
|
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: '
|
|
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: '
|
|
973
|
+
description: 'Intended direction for the trade.',
|
|
813
974
|
enum: ['long', 'short'],
|
|
814
975
|
},
|
|
815
|
-
|
|
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
|
|
822
|
-
minimum:
|
|
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: '
|
|
983
|
+
description: 'Requested leverage multiplier. Keep conservative unless user policy explicitly allows more.',
|
|
827
984
|
minimum: 1,
|
|
828
985
|
maximum: 100,
|
|
829
986
|
},
|
|
830
|
-
|
|
987
|
+
entryPrice: {
|
|
831
988
|
type: 'number',
|
|
832
|
-
description: '
|
|
989
|
+
description: 'Reference entry price in USD used for risk math.',
|
|
990
|
+
minimum: 0,
|
|
833
991
|
},
|
|
834
|
-
|
|
992
|
+
stopLossPrice: {
|
|
835
993
|
type: 'number',
|
|
836
|
-
description: 'Optional
|
|
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
|
-
|
|
926
|
-
type: '
|
|
927
|
-
description: '
|
|
928
|
-
|
|
997
|
+
takeProfitPrice: {
|
|
998
|
+
type: 'number',
|
|
999
|
+
description: 'Optional take profit price in USD used to compute reward/risk.',
|
|
1000
|
+
minimum: 0,
|
|
929
1001
|
},
|
|
930
|
-
|
|
1002
|
+
maxAccountRiskPct: {
|
|
931
1003
|
type: 'number',
|
|
932
|
-
description: '
|
|
933
|
-
minimum:
|
|
1004
|
+
description: 'Maximum account risk percentage allowed by local policy. Default 1%.',
|
|
1005
|
+
minimum: 0,
|
|
934
1006
|
maximum: 100,
|
|
935
1007
|
},
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
1014
|
+
timeframe: {
|
|
1012
1015
|
type: 'string',
|
|
1013
|
-
description: '
|
|
1014
|
-
enum: ['long', 'short'],
|
|
1016
|
+
description: 'Trading horizon such as scalp, intraday, swing, or hedge.',
|
|
1015
1017
|
},
|
|
1016
|
-
|
|
1018
|
+
notes: {
|
|
1017
1019
|
type: 'string',
|
|
1018
|
-
description: '
|
|
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: ['
|
|
1023
|
+
required: ['market', 'side', 'collateralAmountUsd', 'leverage', 'entryPrice'],
|
|
1028
1024
|
additionalProperties: false,
|
|
1029
1025
|
};
|
|
1030
|
-
registerTool(server, '
|
|
1031
|
-
description: '
|
|
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
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1073
|
-
positionKey: positionKeyStr,
|
|
1074
|
-
action,
|
|
1070
|
+
market,
|
|
1075
1071
|
side,
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1106
|
-
|
|
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
|