@oobe-protocol-labs/sap-mcp-server 0.9.37 → 0.9.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/CHANGELOG.md +85 -0
- package/README.md +3 -3
- 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 +51 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +32 -2
- package/dist/config/env.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/payments/monetization-gate.d.ts +5 -0
- package/dist/payments/monetization-gate.d.ts.map +1 -1
- package/dist/payments/monetization-gate.js +39 -2
- package/dist/payments/monetization-gate.js.map +1 -1
- package/dist/payments/prepaid-credit-store.d.ts +111 -0
- package/dist/payments/prepaid-credit-store.d.ts.map +1 -0
- package/dist/payments/prepaid-credit-store.js +230 -0
- package/dist/payments/prepaid-credit-store.js.map +1 -0
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +40 -1
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/schema-validation.d.ts +52 -0
- package/dist/payments/schema-validation.d.ts.map +1 -0
- package/dist/payments/schema-validation.js +112 -0
- package/dist/payments/schema-validation.js.map +1 -0
- package/dist/payments/x402-paid-call.d.ts +21 -0
- package/dist/payments/x402-paid-call.d.ts.map +1 -1
- package/dist/payments/x402-paid-call.js +108 -3
- package/dist/payments/x402-paid-call.js.map +1 -1
- package/dist/perps/adrena/adrena-builder-commodity.d.ts +63 -0
- package/dist/perps/adrena/adrena-builder-commodity.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder-commodity.js +200 -0
- package/dist/perps/adrena/adrena-builder-commodity.js.map +1 -0
- package/dist/perps/adrena/adrena-builder-core.d.ts +319 -0
- package/dist/perps/adrena/adrena-builder-core.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder-core.js +517 -0
- package/dist/perps/adrena/adrena-builder-core.js.map +1 -0
- package/dist/perps/adrena/adrena-builder-liquidity.d.ts +50 -0
- package/dist/perps/adrena/adrena-builder-liquidity.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder-liquidity.js +196 -0
- package/dist/perps/adrena/adrena-builder-liquidity.js.map +1 -0
- package/dist/perps/adrena/adrena-builder-staking.d.ts +57 -0
- package/dist/perps/adrena/adrena-builder-staking.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder-staking.js +368 -0
- package/dist/perps/adrena/adrena-builder-staking.js.map +1 -0
- package/dist/perps/adrena/adrena-builder-trading.d.ts +160 -0
- package/dist/perps/adrena/adrena-builder-trading.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder-trading.js +835 -0
- package/dist/perps/adrena/adrena-builder-trading.js.map +1 -0
- package/dist/perps/adrena/adrena-builder.d.ts +22 -0
- package/dist/perps/adrena/adrena-builder.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-builder.js +22 -0
- package/dist/perps/adrena/adrena-builder.js.map +1 -0
- package/dist/perps/adrena/adrena-constants.d.ts +163 -0
- package/dist/perps/adrena/adrena-constants.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-constants.js +122 -0
- package/dist/perps/adrena/adrena-constants.js.map +1 -0
- package/dist/perps/adrena/adrena-data-api.d.ts +180 -0
- package/dist/perps/adrena/adrena-data-api.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-data-api.js +169 -0
- package/dist/perps/adrena/adrena-data-api.js.map +1 -0
- package/dist/perps/adrena/adrena-idl.d.ts +15084 -0
- package/dist/perps/adrena/adrena-idl.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-idl.js +29903 -0
- package/dist/perps/adrena/adrena-idl.js.map +1 -0
- package/dist/perps/adrena/adrena-pda.d.ts +147 -0
- package/dist/perps/adrena/adrena-pda.d.ts.map +1 -0
- package/dist/perps/adrena/adrena-pda.js +211 -0
- package/dist/perps/adrena/adrena-pda.js.map +1 -0
- package/dist/perps/adrena/index.d.ts +12 -0
- package/dist/perps/adrena/index.d.ts.map +1 -0
- package/dist/perps/adrena/index.js +12 -0
- package/dist/perps/adrena/index.js.map +1 -0
- package/dist/perps/chart-indicators.d.ts +27 -0
- package/dist/perps/chart-indicators.d.ts.map +1 -0
- package/dist/perps/chart-indicators.js +345 -0
- package/dist/perps/chart-indicators.js.map +1 -0
- package/dist/perps/chart-tools.d.ts +45 -0
- package/dist/perps/chart-tools.d.ts.map +1 -0
- package/dist/perps/chart-tools.js +341 -0
- package/dist/perps/chart-tools.js.map +1 -0
- package/dist/perps/perp-analytics-tools.d.ts +20 -0
- package/dist/perps/perp-analytics-tools.d.ts.map +1 -0
- package/dist/perps/perp-analytics-tools.js +868 -0
- package/dist/perps/perp-analytics-tools.js.map +1 -0
- package/dist/perps/perp-constants.d.ts +362 -0
- package/dist/perps/perp-constants.d.ts.map +1 -0
- package/dist/perps/perp-constants.js +210 -0
- package/dist/perps/perp-constants.js.map +1 -0
- package/dist/perps/perp-decoders.d.ts +47 -0
- package/dist/perps/perp-decoders.d.ts.map +1 -0
- package/dist/perps/perp-decoders.js +332 -0
- package/dist/perps/perp-decoders.js.map +1 -0
- package/dist/policy/policy-engine.d.ts +42 -0
- package/dist/policy/policy-engine.d.ts.map +1 -1
- package/dist/policy/policy-engine.js +43 -0
- package/dist/policy/policy-engine.js.map +1 -1
- package/dist/premium/activation-manager.d.ts.map +1 -1
- package/dist/premium/activation-manager.js +20 -2
- package/dist/premium/activation-manager.js.map +1 -1
- package/dist/prompts/context/sap-agent-context.prompt.js +15 -1
- package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
- package/dist/server/server-metadata.d.ts +15 -0
- package/dist/server/server-metadata.d.ts.map +1 -1
- package/dist/server/server-metadata.js +16 -1
- package/dist/server/server-metadata.js.map +1 -1
- package/dist/solana/ata-utils.d.ts +39 -0
- package/dist/solana/ata-utils.d.ts.map +1 -0
- package/dist/solana/ata-utils.js +49 -0
- package/dist/solana/ata-utils.js.map +1 -0
- package/dist/strategies/trade-journal.d.ts +78 -0
- package/dist/strategies/trade-journal.d.ts.map +1 -0
- package/dist/strategies/trade-journal.js +92 -0
- package/dist/strategies/trade-journal.js.map +1 -0
- package/dist/tools/adrena/adrena-commodity-tools.d.ts +15 -0
- package/dist/tools/adrena/adrena-commodity-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-commodity-tools.js +137 -0
- package/dist/tools/adrena/adrena-commodity-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-data-tools.d.ts +23 -0
- package/dist/tools/adrena/adrena-data-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-data-tools.js +437 -0
- package/dist/tools/adrena/adrena-data-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-helpers.d.ts +46 -0
- package/dist/tools/adrena/adrena-helpers.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-helpers.js +41 -0
- package/dist/tools/adrena/adrena-helpers.js.map +1 -0
- package/dist/tools/adrena/adrena-index.d.ts +20 -0
- package/dist/tools/adrena/adrena-index.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-index.js +75 -0
- package/dist/tools/adrena/adrena-index.js.map +1 -0
- package/dist/tools/adrena/adrena-limit-order-tools.d.ts +22 -0
- package/dist/tools/adrena/adrena-limit-order-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-limit-order-tools.js +91 -0
- package/dist/tools/adrena/adrena-limit-order-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-liquidity-tools.d.ts +15 -0
- package/dist/tools/adrena/adrena-liquidity-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-liquidity-tools.js +108 -0
- package/dist/tools/adrena/adrena-liquidity-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-snapshot-tools.d.ts +42 -0
- package/dist/tools/adrena/adrena-snapshot-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-snapshot-tools.js +124 -0
- package/dist/tools/adrena/adrena-snapshot-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-staking-tools.d.ts +15 -0
- package/dist/tools/adrena/adrena-staking-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-staking-tools.js +139 -0
- package/dist/tools/adrena/adrena-staking-tools.js.map +1 -0
- package/dist/tools/adrena/adrena-trading-tools.d.ts +78 -0
- package/dist/tools/adrena/adrena-trading-tools.d.ts.map +1 -0
- package/dist/tools/adrena/adrena-trading-tools.js +556 -0
- package/dist/tools/adrena/adrena-trading-tools.js.map +1 -0
- package/dist/tools/adrena-tools.d.ts +2 -0
- package/dist/tools/adrena-tools.d.ts.map +1 -0
- package/dist/tools/adrena-tools.js +2 -0
- package/dist/tools/adrena-tools.js.map +1 -0
- package/dist/tools/client-sdk-tools.d.ts +8 -0
- package/dist/tools/client-sdk-tools.d.ts.map +1 -1
- package/dist/tools/client-sdk-tools.js +32 -7
- package/dist/tools/client-sdk-tools.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +170 -0
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/perp-tools.d.ts +6 -31
- package/dist/tools/perp-tools.d.ts.map +1 -1
- package/dist/tools/perp-tools.js +6 -1141
- 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 +123 -5
- package/dist/tools/premium-tools.js.map +1 -1
- package/dist/tools/quick-context-tool.js +1 -0
- 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 +4 -0
- package/dist/tools/register-tools.js.map +1 -1
- package/dist/tools/transaction-tools.d.ts +21 -1
- package/dist/tools/transaction-tools.d.ts.map +1 -1
- package/dist/tools/transaction-tools.js +114 -30
- 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 +288 -0
- package/dist/tools/x402-paid-call-tool.js.map +1 -1
- package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
- package/docs/10_OPERATIONS_RELEASE_AND_PM2.md +54 -6
- package/docs/14_DESKTOP_WIZARD_RELEASE.md +3 -3
- package/docs/BRANCH_AND_CI.md +2 -2
- package/package.json +3 -2
- package/server.json +3 -3
- package/skills/sap-defi/SKILL.md +93 -22
- package/skills/sap-mcp/TOOL_REFERENCE.md +39 -4
package/dist/tools/perp-tools.js
CHANGED
|
@@ -1,1147 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name tools/perp-tools
|
|
3
|
-
* @description
|
|
4
|
-
*
|
|
5
|
-
* Provides 8 tools across two categories:
|
|
6
|
-
*
|
|
7
|
-
* Read-only tools (7):
|
|
8
|
-
* - sap_perp_markets — List Adrena perp markets with mark price, funding, OI.
|
|
9
|
-
* - sap_perp_position_info — Read on-chain perp positions for a wallet.
|
|
10
|
-
* - sap_perp_funding_history — Compute funding rate from on-chain custody account.
|
|
11
|
-
* - sap_chart_ohlc — OHLC candlestick data for any Solana token.
|
|
12
|
-
* - sap_chart_long_term — Long-term price history + protocol TVL.
|
|
13
|
-
* - sap_chart_volume_profile — Volume profile analysis (POC, VAH, VAL).
|
|
14
|
-
* - sap_perp_liquidation_zones — Compute liquidation zones for open positions.
|
|
15
|
-
*
|
|
16
|
-
* Professional planning tools (1):
|
|
17
|
-
* - sap_perp_trade_plan — Build a trader-grade risk, route, and execution checklist.
|
|
18
|
-
*
|
|
19
|
-
* All read-only tools use free APIs (DexScreener, DeFiLlama) and Solana RPC
|
|
20
|
-
* (Triton). Execution builders are intentionally not exposed unless the
|
|
21
|
-
* account graph is IDL-backed and safe for local finalization.
|
|
3
|
+
* @description Barrel re-export for perp tools — split into perp-constants, perp-decoders,
|
|
4
|
+
* chart-tools, and perp-analytics-tools under src/perps/.
|
|
22
5
|
*
|
|
23
6
|
* @module tools/perp-tools
|
|
24
7
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
30
|
-
* Constants
|
|
31
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
32
|
-
/** Adrena program ID on Solana mainnet. */
|
|
33
|
-
const ADRENA_PROGRAM_ID = new PublicKey('13gDzEXCdocbj8iAiqrScGo47NiSuYENGsRqi3SEAwet');
|
|
34
|
-
/** DexScreener REST API base URL (free, no API key). */
|
|
35
|
-
const DEXSCREENER_API_URL = 'https://api.dexscreener.com';
|
|
36
|
-
/** DeFiLlama REST API base URL (free, no API key). */
|
|
37
|
-
const DEFILAMA_API_URL = 'https://api.llama.fi';
|
|
38
|
-
/** Fetch timeout for external API calls (ms). */
|
|
39
|
-
const FETCH_TIMEOUT_MS = 8_000;
|
|
40
|
-
/** Default number of volume profile buckets. */
|
|
41
|
-
const DEFAULT_VP_BUCKETS = 20;
|
|
42
|
-
/** Value area percentage for volume profile (70%). */
|
|
43
|
-
const VALUE_AREA_PCT = 0.70;
|
|
44
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
45
|
-
* Adrena account discriminators (Anchor 0.31 — sha256("account:<Name>")[0..8])
|
|
46
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
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]);
|
|
53
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
54
|
-
* Helper: timed fetch
|
|
55
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
56
|
-
/**
|
|
57
|
-
* @name timedFetch
|
|
58
|
-
* @description Fetch JSON from a URL with an abort timeout.
|
|
59
|
-
*
|
|
60
|
-
* @param url — The URL to fetch.
|
|
61
|
-
* @returns Parsed JSON response, or `null` on error/timeout.
|
|
62
|
-
*
|
|
63
|
-
* @internal
|
|
64
|
-
*/
|
|
65
|
-
async function timedFetch(url) {
|
|
66
|
-
try {
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
69
|
-
const response = await fetch(url, {
|
|
70
|
-
method: 'GET',
|
|
71
|
-
headers: { 'Content-Type': 'application/json' },
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
});
|
|
74
|
-
clearTimeout(timeout);
|
|
75
|
-
if (!response.ok)
|
|
76
|
-
return null;
|
|
77
|
-
return await response.json();
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* @name discToBase58
|
|
85
|
-
* @description Convert an 8-byte discriminator Buffer to a base58 string for
|
|
86
|
-
* use in `getProgramAccounts` memcmp filters.
|
|
87
|
-
*
|
|
88
|
-
* @param disc — 8-byte discriminator Buffer.
|
|
89
|
-
* @returns Base58-encoded string.
|
|
90
|
-
*
|
|
91
|
-
* @internal
|
|
92
|
-
*/
|
|
93
|
-
function discToBase58(disc) {
|
|
94
|
-
return new PublicKey(disc).toBase58();
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* @name readU64LE
|
|
98
|
-
* @description Read a u64 little-endian value from a Buffer and return as a JS number.
|
|
99
|
-
*
|
|
100
|
-
* @param buf — Source buffer.
|
|
101
|
-
* @param offset — Read offset.
|
|
102
|
-
* @returns The value as a JavaScript number.
|
|
103
|
-
*
|
|
104
|
-
* @internal
|
|
105
|
-
*/
|
|
106
|
-
function readU64LE(buf, offset) {
|
|
107
|
-
return Number(buf.readBigUInt64LE(offset));
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* @name readSymbol
|
|
111
|
-
* @description Read a short string (Anchor `String` — 4-byte LE length + UTF-8 bytes)
|
|
112
|
-
* from an account data buffer.
|
|
113
|
-
*
|
|
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.
|
|
118
|
-
*
|
|
119
|
-
* @internal
|
|
120
|
-
*/
|
|
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
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
133
|
-
* Tool 1: sap_perp_markets
|
|
134
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
135
|
-
/**
|
|
136
|
-
* @name registerPerpMarketsTool
|
|
137
|
-
* @description Register the sap_perp_markets read-only tool.
|
|
138
|
-
*
|
|
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.
|
|
142
|
-
*
|
|
143
|
-
* @param server — MCP server instance.
|
|
144
|
-
* @param context — Runtime context with Solana RPC connection.
|
|
145
|
-
*
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
function registerPerpMarketsTool(server, context) {
|
|
149
|
-
const schema = {
|
|
150
|
-
type: 'object',
|
|
151
|
-
properties: {
|
|
152
|
-
market: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
description: 'Optional market symbol filter (e.g. "SOL", "BTC"). Empty = all markets.',
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
additionalProperties: false,
|
|
158
|
-
};
|
|
159
|
-
registerTool(server, 'sap_perp_markets', {
|
|
160
|
-
description: 'List available perpetual futures markets from Adrena Pool/Custody accounts with mark price, funding rate, open interest, and 24h volume when the configured RPC can serve the required account indexes. Read-only analysis tool. If markets are empty, treat it as data unavailable, not proof that markets do not exist. SAP MCP currently exposes no hosted Adrena execution builder; use sap_perp_trade_plan plus chart tools for planning only.',
|
|
161
|
-
inputSchema: schema,
|
|
162
|
-
}, async (args) => {
|
|
163
|
-
const marketFilter = typeof args['market'] === 'string' ? args['market'].toUpperCase() : '';
|
|
164
|
-
try {
|
|
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
|
-
],
|
|
171
|
-
commitment: 'confirmed',
|
|
172
|
-
});
|
|
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;
|
|
261
|
-
const dataAvailability = custodyAccounts.length === 0 && poolAccounts.length === 0
|
|
262
|
-
? {
|
|
263
|
-
status: 'unavailable',
|
|
264
|
-
reason: 'No Adrena Pool/Custody accounts were returned by the configured Solana RPC. This can mean the RPC does not serve the required getProgramAccounts index, the upstream account layout changed, or Adrena markets are unavailable for this route.',
|
|
265
|
-
agentAction: 'Do not infer that no perp markets exist and do not retry in a loop. Use sap_chart_ohlc/sap_chart_volume_profile for market context, then stop before execution until an IDL-backed builder or native venue route is available.',
|
|
266
|
-
}
|
|
267
|
-
: {
|
|
268
|
-
status: 'available',
|
|
269
|
-
reason: 'Adrena account scan returned on-chain accounts from the configured RPC.',
|
|
270
|
-
agentAction: 'Use custodyAddress and poolAddress only for analysis. SAP MCP does not expose a hosted Adrena execution builder yet.',
|
|
271
|
-
};
|
|
272
|
-
return createTextResponse(JSON.stringify({
|
|
273
|
-
source: 'on-chain-rpc',
|
|
274
|
-
markets: filtered,
|
|
275
|
-
count: filtered.length,
|
|
276
|
-
totalCustodies: custodyAccounts.length,
|
|
277
|
-
totalPools: poolAccounts.length,
|
|
278
|
-
dataAvailability,
|
|
279
|
-
recommendedNextTools: [
|
|
280
|
-
'sap_chart_ohlc',
|
|
281
|
-
'sap_chart_volume_profile',
|
|
282
|
-
'sap_perp_trade_plan',
|
|
283
|
-
],
|
|
284
|
-
executionStatus: 'analysis_only_no_hosted_builder',
|
|
285
|
-
note: 'Data read directly from Solana on-chain Pool/Custody accounts. Use custodyAddress and poolAddress for analysis only. Do not execute perps unless SAP MCP exposes a typed unsigned builder or a local signer tool for that exact action.',
|
|
286
|
-
}));
|
|
287
|
-
}
|
|
288
|
-
catch (err) {
|
|
289
|
-
return createTextResponse(JSON.stringify({
|
|
290
|
-
error: 'Failed to fetch perp markets from Solana RPC',
|
|
291
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
292
|
-
}), { isError: true });
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
297
|
-
* Tool 2: sap_perp_position_info
|
|
298
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
299
|
-
/**
|
|
300
|
-
* @name registerPerpPositionInfoTool
|
|
301
|
-
* @description Register the sap_perp_position_info read-only tool.
|
|
302
|
-
*
|
|
303
|
-
* Reads on-chain Adrena position accounts for a given wallet using
|
|
304
|
-
* `getProgramAccounts` with a memcmp filter on the owner field.
|
|
305
|
-
*
|
|
306
|
-
* @param server — MCP server instance.
|
|
307
|
-
* @param context — Runtime context with Solana RPC connection.
|
|
308
|
-
*
|
|
309
|
-
* @internal
|
|
310
|
-
*/
|
|
311
|
-
function registerPerpPositionInfoTool(server, context) {
|
|
312
|
-
const schema = {
|
|
313
|
-
type: 'object',
|
|
314
|
-
properties: {
|
|
315
|
-
wallet: {
|
|
316
|
-
type: 'string',
|
|
317
|
-
description: 'Wallet public key (base58) to read positions for.',
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
required: ['wallet'],
|
|
321
|
-
additionalProperties: false,
|
|
322
|
-
};
|
|
323
|
-
registerTool(server, 'sap_perp_position_info', {
|
|
324
|
-
description: 'Read all open perpetual positions on Adrena for a given wallet address. Returns position key, market, side, size, collateral, entry price, mark price, leverage, unrealized PnL, and liquidation price. Read-only — uses Solana RPC.',
|
|
325
|
-
inputSchema: schema,
|
|
326
|
-
}, async (args) => {
|
|
327
|
-
const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
|
|
328
|
-
if (!walletStr) {
|
|
329
|
-
return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
|
|
330
|
-
}
|
|
331
|
-
let walletPubkey;
|
|
332
|
-
try {
|
|
333
|
-
walletPubkey = new PublicKey(walletStr);
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
|
|
337
|
-
}
|
|
338
|
-
try {
|
|
339
|
-
// Adrena Position accounts: filter by Position discriminator + memcmp on owner.
|
|
340
|
-
// Position layout (from IDL, after 8-byte discriminator):
|
|
341
|
-
// owner(32) + pool(32) + custody(32) + side(1) + price(u64) + sizeUsd(u64) + collateralUsd(u64) + ...
|
|
342
|
-
const accounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
|
|
343
|
-
filters: [
|
|
344
|
-
{ memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
|
|
345
|
-
{ memcmp: { offset: 8, bytes: walletPubkey.toBase58() } },
|
|
346
|
-
],
|
|
347
|
-
commitment: 'confirmed',
|
|
348
|
-
});
|
|
349
|
-
const positions = accounts.map(({ pubkey, account }) => {
|
|
350
|
-
const data = account.data;
|
|
351
|
-
// Position layout (from IDL): owner(32) + pool(32) + custody(32) + side(1) + price(u64) + sizeUsd(u64) + collateralUsd(u64) + collateralAmount(u64) + ...
|
|
352
|
-
const sideByte = data[8 + 32 + 32 + 32];
|
|
353
|
-
const side = sideByte === 0 ? 'long' : 'short';
|
|
354
|
-
const price = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1));
|
|
355
|
-
const sizeUsd = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1 + 8));
|
|
356
|
-
const collateralUsd = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1 + 8 + 8));
|
|
357
|
-
const leverage = collateralUsd > 0 ? sizeUsd / collateralUsd : 0;
|
|
358
|
-
const unrealizedPnl = 0;
|
|
359
|
-
const liquidationPrice = leverage > 0 ? price * (1 - 1 / leverage) : 0;
|
|
360
|
-
return {
|
|
361
|
-
positionKey: pubkey.toBase58(),
|
|
362
|
-
market: 'unknown',
|
|
363
|
-
side,
|
|
364
|
-
size: sizeUsd,
|
|
365
|
-
collateral: collateralUsd,
|
|
366
|
-
entryPrice: price,
|
|
367
|
-
markPrice: price,
|
|
368
|
-
leverage,
|
|
369
|
-
unrealizedPnl,
|
|
370
|
-
liquidationPrice,
|
|
371
|
-
};
|
|
372
|
-
});
|
|
373
|
-
return createTextResponse(JSON.stringify({
|
|
374
|
-
wallet: walletStr,
|
|
375
|
-
positions,
|
|
376
|
-
count: positions.length,
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
|
-
catch (err) {
|
|
380
|
-
return createTextResponse(JSON.stringify({
|
|
381
|
-
error: 'Failed to read on-chain positions',
|
|
382
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
383
|
-
}), { isError: true });
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
388
|
-
* Tool 3: sap_perp_funding_history
|
|
389
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
390
|
-
/**
|
|
391
|
-
* @name registerPerpFundingHistoryTool
|
|
392
|
-
* @description Register the sap_perp_funding_history read-only tool.
|
|
393
|
-
*
|
|
394
|
-
* Computes the current funding rate from on-chain Custody account data.
|
|
395
|
-
* The agent obtains the custody address from `sap_perp_markets` and passes
|
|
396
|
-
* it here. Historical funding rate snapshots are not available on-chain
|
|
397
|
-
* (only the current rate embedded in the custody account); the tool returns
|
|
398
|
-
* the current funding state and a clear error if the custody address is
|
|
399
|
-
* missing or invalid.
|
|
400
|
-
*
|
|
401
|
-
* @param server — MCP server instance.
|
|
402
|
-
* @param context — Runtime context with Solana RPC connection.
|
|
403
|
-
*
|
|
404
|
-
* @internal
|
|
405
|
-
*/
|
|
406
|
-
function registerPerpFundingHistoryTool(server, context) {
|
|
407
|
-
const schema = {
|
|
408
|
-
type: 'object',
|
|
409
|
-
properties: {
|
|
410
|
-
custodyAddress: {
|
|
411
|
-
type: 'string',
|
|
412
|
-
description: 'Custody account public key (base58). Obtain from sap_perp_markets output — the custodyAddress field.',
|
|
413
|
-
},
|
|
414
|
-
market: {
|
|
415
|
-
type: 'string',
|
|
416
|
-
description: 'Optional market symbol (e.g. "SOL", "BTC") for display purposes.',
|
|
417
|
-
},
|
|
418
|
-
limit: {
|
|
419
|
-
type: 'number',
|
|
420
|
-
description: 'Maximum number of funding records to return (default 100). On-chain mode returns the current funding snapshot only.',
|
|
421
|
-
minimum: 1,
|
|
422
|
-
maximum: 1000,
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
required: ['custodyAddress'],
|
|
426
|
-
additionalProperties: false,
|
|
427
|
-
};
|
|
428
|
-
registerTool(server, 'sap_perp_funding_history', {
|
|
429
|
-
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.',
|
|
430
|
-
inputSchema: schema,
|
|
431
|
-
}, async (args) => {
|
|
432
|
-
const custodyAddressStr = typeof args['custodyAddress'] === 'string' ? args['custodyAddress'] : '';
|
|
433
|
-
const market = typeof args['market'] === 'string' ? args['market'] : '';
|
|
434
|
-
if (!custodyAddressStr) {
|
|
435
|
-
return createTextResponse(JSON.stringify({
|
|
436
|
-
error: 'custodyAddress is required. Use sap_perp_markets to get the custody address for the desired market.',
|
|
437
|
-
}), { isError: true });
|
|
438
|
-
}
|
|
439
|
-
let custodyPubkey;
|
|
440
|
-
try {
|
|
441
|
-
custodyPubkey = new PublicKey(custodyAddressStr);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return createTextResponse(JSON.stringify({
|
|
445
|
-
error: 'Invalid custody address',
|
|
446
|
-
custodyAddress: custodyAddressStr,
|
|
447
|
-
}), { isError: true });
|
|
448
|
-
}
|
|
449
|
-
try {
|
|
450
|
-
const accountInfo = await context.connection.getAccountInfo(custodyPubkey, 'confirmed');
|
|
451
|
-
if (!accountInfo) {
|
|
452
|
-
return createTextResponse(JSON.stringify({
|
|
453
|
-
error: 'Custody account not found on-chain',
|
|
454
|
-
custodyAddress: custodyAddressStr,
|
|
455
|
-
message: 'The custody account does not exist or has been closed. Use sap_perp_markets to find valid custody addresses.',
|
|
456
|
-
}), { isError: true });
|
|
457
|
-
}
|
|
458
|
-
const data = accountInfo.data;
|
|
459
|
-
if (data.length < 8 + 32 + 32) {
|
|
460
|
-
return createTextResponse(JSON.stringify({
|
|
461
|
-
error: 'Account data too short to be a valid Custody account',
|
|
462
|
-
custodyAddress: custodyAddressStr,
|
|
463
|
-
dataLength: data.length,
|
|
464
|
-
}), { isError: true });
|
|
465
|
-
}
|
|
466
|
-
// Verify discriminator matches Custody.
|
|
467
|
-
const disc = data.subarray(0, 8);
|
|
468
|
-
if (!disc.equals(DISC_CUSTODY)) {
|
|
469
|
-
return createTextResponse(JSON.stringify({
|
|
470
|
-
error: 'Account discriminator does not match Custody type',
|
|
471
|
-
custodyAddress: custodyAddressStr,
|
|
472
|
-
expectedDisc: Array.from(DISC_CUSTODY),
|
|
473
|
-
actualDisc: Array.from(disc),
|
|
474
|
-
message: 'The provided address is not a Custody account. Use sap_perp_markets to get the correct custodyAddress.',
|
|
475
|
-
}), { isError: true });
|
|
476
|
-
}
|
|
477
|
-
// Decode funding-related fields from the Custody account.
|
|
478
|
-
// Pool(32) + custodyMint(32) are at offset 8.
|
|
479
|
-
const poolPubkey = new PublicKey(data.subarray(8, 8 + 32));
|
|
480
|
-
const custodyMint = new PublicKey(data.subarray(8 + 32, 8 + 64));
|
|
481
|
-
// Read symbol at offset 72 (after pool + custodyMint).
|
|
482
|
-
let symbol = readSymbol(data, 8 + 64, 16);
|
|
483
|
-
if (!symbol) {
|
|
484
|
-
symbol = readSymbol(data, 8 + 32, 16);
|
|
485
|
-
}
|
|
486
|
-
// Scan for funding rate data at common offsets.
|
|
487
|
-
let markPrice = 0;
|
|
488
|
-
let fundingRate = 0;
|
|
489
|
-
let openInterestLong = 0;
|
|
490
|
-
let openInterestShort = 0;
|
|
491
|
-
let cumulativeFunding = 0;
|
|
492
|
-
for (const baseOffset of [120, 144, 160, 176, 192, 200, 208, 224, 240, 256]) {
|
|
493
|
-
if (baseOffset + 48 > data.length)
|
|
494
|
-
continue;
|
|
495
|
-
const price = readU64LE(data, baseOffset);
|
|
496
|
-
const funding = readU64LE(data, baseOffset + 8);
|
|
497
|
-
const oiLong = readU64LE(data, baseOffset + 16);
|
|
498
|
-
const oiShort = readU64LE(data, baseOffset + 24);
|
|
499
|
-
const cumFunding = readU64LE(data, baseOffset + 32);
|
|
500
|
-
if (price > 0 && price < 1e15 && Math.abs(funding) < price) {
|
|
501
|
-
markPrice = price;
|
|
502
|
-
fundingRate = funding;
|
|
503
|
-
openInterestLong = oiLong;
|
|
504
|
-
openInterestShort = oiShort;
|
|
505
|
-
cumulativeFunding = cumFunding;
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const timestamp = Date.now();
|
|
510
|
-
return createTextResponse(JSON.stringify({
|
|
511
|
-
source: 'on-chain-rpc',
|
|
512
|
-
custodyAddress: custodyAddressStr,
|
|
513
|
-
market: market || symbol || 'UNKNOWN',
|
|
514
|
-
symbol: symbol || 'UNKNOWN',
|
|
515
|
-
poolAddress: poolPubkey.toBase58(),
|
|
516
|
-
custodyMint: custodyMint.toBase58(),
|
|
517
|
-
currentFunding: {
|
|
518
|
-
timestamp,
|
|
519
|
-
fundingRate,
|
|
520
|
-
cumulativeFunding,
|
|
521
|
-
markPrice,
|
|
522
|
-
openInterestLong,
|
|
523
|
-
openInterestShort,
|
|
524
|
-
},
|
|
525
|
-
records: [{
|
|
526
|
-
timestamp,
|
|
527
|
-
fundingRate,
|
|
528
|
-
cumulativeFunding,
|
|
529
|
-
}],
|
|
530
|
-
count: 1,
|
|
531
|
-
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.',
|
|
532
|
-
}));
|
|
533
|
-
}
|
|
534
|
-
catch (err) {
|
|
535
|
-
return createTextResponse(JSON.stringify({
|
|
536
|
-
error: 'Failed to read funding data from on-chain custody account',
|
|
537
|
-
custodyAddress: custodyAddressStr,
|
|
538
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
539
|
-
}), { isError: true });
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
544
|
-
* Tool 4: sap_chart_ohlc
|
|
545
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
546
|
-
/**
|
|
547
|
-
* @name registerChartOhlcTool
|
|
548
|
-
* @description Register the sap_chart_ohlc read-only tool.
|
|
549
|
-
*
|
|
550
|
-
* Fetches OHLC candlestick data for any Solana token from DexScreener's
|
|
551
|
-
* free REST API.
|
|
552
|
-
*
|
|
553
|
-
* @param server — MCP server instance.
|
|
554
|
-
* @param context — Runtime context (unused — pure REST call).
|
|
555
|
-
*
|
|
556
|
-
* @internal
|
|
557
|
-
*/
|
|
558
|
-
function registerChartOhlcTool(server, _context) {
|
|
559
|
-
const schema = {
|
|
560
|
-
type: 'object',
|
|
561
|
-
properties: {
|
|
562
|
-
mint: {
|
|
563
|
-
type: 'string',
|
|
564
|
-
description: 'Token mint address (base58) to fetch OHLC data for.',
|
|
565
|
-
},
|
|
566
|
-
resolution: {
|
|
567
|
-
type: 'string',
|
|
568
|
-
description: 'Chart resolution: "5m", "1h", "6h", "24h" (default "1h").',
|
|
569
|
-
enum: ['5m', '1h', '6h', '24h'],
|
|
570
|
-
},
|
|
571
|
-
},
|
|
572
|
-
required: ['mint'],
|
|
573
|
-
additionalProperties: false,
|
|
574
|
-
};
|
|
575
|
-
registerTool(server, 'sap_chart_ohlc', {
|
|
576
|
-
description: 'Fetch OHLC candlestick data for any Solana token from DexScreener. Returns open, high, low, close, volume, liquidity, and FDV. Read-only — uses free DexScreener REST API.',
|
|
577
|
-
inputSchema: schema,
|
|
578
|
-
}, async (args) => {
|
|
579
|
-
const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
|
|
580
|
-
const resolution = typeof args['resolution'] === 'string' ? args['resolution'] : '1h';
|
|
581
|
-
if (!mint) {
|
|
582
|
-
return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
|
|
583
|
-
}
|
|
584
|
-
const url = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
|
|
585
|
-
const pairs = await timedFetch(url);
|
|
586
|
-
if (!pairs || pairs.length === 0) {
|
|
587
|
-
return createTextResponse(JSON.stringify({
|
|
588
|
-
error: 'No DexScreener pairs found for this mint',
|
|
589
|
-
mint,
|
|
590
|
-
}), { isError: true });
|
|
591
|
-
}
|
|
592
|
-
const pair = pairs[0];
|
|
593
|
-
const priceUsd = pair.priceUsd ? parseFloat(pair.priceUsd) : 0;
|
|
594
|
-
if (priceUsd <= 0) {
|
|
595
|
-
return createTextResponse(JSON.stringify({
|
|
596
|
-
error: 'No valid price data from DexScreener',
|
|
597
|
-
mint,
|
|
598
|
-
}), { isError: true });
|
|
599
|
-
}
|
|
600
|
-
const priceChange = pair.priceChange ?? {};
|
|
601
|
-
const volume = pair.volume ?? {};
|
|
602
|
-
const liquidity = pair.liquidity?.usd ?? 0;
|
|
603
|
-
const fdv = pair.fdv ?? 0;
|
|
604
|
-
// Derive OHLC from price change percentages for the requested resolution.
|
|
605
|
-
const changeMap = {
|
|
606
|
-
'5m': priceChange['m5'] ?? 0,
|
|
607
|
-
'1h': priceChange['h1'] ?? 0,
|
|
608
|
-
'6h': priceChange['h6'] ?? 0,
|
|
609
|
-
'24h': priceChange['h24'] ?? 0,
|
|
610
|
-
};
|
|
611
|
-
const changePct = changeMap[resolution] ?? changeMap['1h'];
|
|
612
|
-
const open = priceUsd / (1 + changePct / 100);
|
|
613
|
-
const close = priceUsd;
|
|
614
|
-
const high = Math.max(open, close) * 1.001;
|
|
615
|
-
const low = Math.min(open, close) * 0.999;
|
|
616
|
-
const volMap = {
|
|
617
|
-
'5m': (volume['m5'] ?? 0),
|
|
618
|
-
'1h': (volume['h1'] ?? 0),
|
|
619
|
-
'6h': (volume['h6'] ?? 0),
|
|
620
|
-
'24h': (volume['h24'] ?? 0),
|
|
621
|
-
};
|
|
622
|
-
const vol = volMap[resolution] ?? volMap['1h'];
|
|
623
|
-
const candle = {
|
|
624
|
-
timestamp: Date.now(),
|
|
625
|
-
open,
|
|
626
|
-
high,
|
|
627
|
-
low,
|
|
628
|
-
close,
|
|
629
|
-
volume: vol,
|
|
630
|
-
};
|
|
631
|
-
return createTextResponse(JSON.stringify({
|
|
632
|
-
mint,
|
|
633
|
-
symbol: pair.baseToken?.symbol ?? 'unknown',
|
|
634
|
-
resolution,
|
|
635
|
-
candle,
|
|
636
|
-
liquidityUsd: liquidity,
|
|
637
|
-
fdv,
|
|
638
|
-
priceUsd,
|
|
639
|
-
}));
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
643
|
-
* Tool 5: sap_chart_long_term
|
|
644
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
645
|
-
/**
|
|
646
|
-
* @name registerChartLongTermTool
|
|
647
|
-
* @description Register the sap_chart_long_term read-only tool.
|
|
648
|
-
*
|
|
649
|
-
* Fetches long-term price history for a Solana token from DexScreener
|
|
650
|
-
* and optional protocol TVL data from DeFiLlama.
|
|
651
|
-
*
|
|
652
|
-
* @param server — MCP server instance.
|
|
653
|
-
* @param context — Runtime context (unused — pure REST call).
|
|
654
|
-
*
|
|
655
|
-
* @internal
|
|
656
|
-
*/
|
|
657
|
-
function registerChartLongTermTool(server, _context) {
|
|
658
|
-
const schema = {
|
|
659
|
-
type: 'object',
|
|
660
|
-
properties: {
|
|
661
|
-
mint: {
|
|
662
|
-
type: 'string',
|
|
663
|
-
description: 'Token mint address (base58) for price history.',
|
|
664
|
-
},
|
|
665
|
-
protocol: {
|
|
666
|
-
type: 'string',
|
|
667
|
-
description: 'Optional DeFiLlama protocol slug for TVL history (e.g. "jupiter", "raydium").',
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
required: ['mint'],
|
|
671
|
-
additionalProperties: false,
|
|
672
|
-
};
|
|
673
|
-
registerTool(server, 'sap_chart_long_term', {
|
|
674
|
-
description: 'Fetch long-term price history for a Solana token from DexScreener and optional protocol TVL data from DeFiLlama. Returns price changes, volume, liquidity, FDV, and TVL history. Read-only — uses free REST APIs.',
|
|
675
|
-
inputSchema: schema,
|
|
676
|
-
}, async (args) => {
|
|
677
|
-
const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
|
|
678
|
-
const protocol = typeof args['protocol'] === 'string' ? args['protocol'] : '';
|
|
679
|
-
if (!mint) {
|
|
680
|
-
return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
|
|
681
|
-
}
|
|
682
|
-
// Fetch token price data from DexScreener.
|
|
683
|
-
const dexUrl = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
|
|
684
|
-
const pairs = await timedFetch(dexUrl);
|
|
685
|
-
let priceData = {};
|
|
686
|
-
if (pairs && pairs.length > 0) {
|
|
687
|
-
const pair = pairs[0];
|
|
688
|
-
priceData = {
|
|
689
|
-
symbol: pair.baseToken?.symbol ?? 'unknown',
|
|
690
|
-
priceUsd: pair.priceUsd ?? '0',
|
|
691
|
-
priceChange: pair.priceChange ?? {},
|
|
692
|
-
volume: pair.volume ?? {},
|
|
693
|
-
liquidityUsd: pair.liquidity?.usd ?? 0,
|
|
694
|
-
fdv: pair.fdv ?? 0,
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
// Fetch protocol TVL from DeFiLlama if requested.
|
|
698
|
-
let tvlData = {};
|
|
699
|
-
if (protocol) {
|
|
700
|
-
const llamaUrl = `${DEFILAMA_API_URL}/protocol/${encodeURIComponent(protocol)}`;
|
|
701
|
-
const protoData = await timedFetch(llamaUrl);
|
|
702
|
-
if (protoData) {
|
|
703
|
-
tvlData = {
|
|
704
|
-
name: protoData.name,
|
|
705
|
-
currentTvl: protoData.tvl,
|
|
706
|
-
change1d: protoData.change_1d,
|
|
707
|
-
change7d: protoData.change_7d,
|
|
708
|
-
marketcap: protoData.marketcap,
|
|
709
|
-
chain: protoData.chain,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (Object.keys(priceData).length === 0 && Object.keys(tvlData).length === 0) {
|
|
714
|
-
return createTextResponse(JSON.stringify({
|
|
715
|
-
error: 'No data available from DexScreener or DeFiLlama',
|
|
716
|
-
mint,
|
|
717
|
-
protocol,
|
|
718
|
-
}), { isError: true });
|
|
719
|
-
}
|
|
720
|
-
return createTextResponse(JSON.stringify({
|
|
721
|
-
mint,
|
|
722
|
-
token: priceData,
|
|
723
|
-
protocolTvl: tvlData,
|
|
724
|
-
}));
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
728
|
-
* Tool 6: sap_chart_volume_profile
|
|
729
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
730
|
-
/**
|
|
731
|
-
* @name computeVolumeProfile
|
|
732
|
-
* @description Compute volume profile (POC, VAH, VAL) from price and volume arrays.
|
|
733
|
-
*
|
|
734
|
-
* Pure TypeScript implementation — no external indicator library.
|
|
735
|
-
*
|
|
736
|
-
* @param prices — Array of price points.
|
|
737
|
-
* @param volumes — Array of volume values (same length as prices).
|
|
738
|
-
* @param bucketCount — Number of price buckets (default 20).
|
|
739
|
-
* @returns Volume profile result with POC, VAH, VAL, and buckets.
|
|
740
|
-
*
|
|
741
|
-
* @internal
|
|
742
|
-
*/
|
|
743
|
-
function computeVolumeProfile(prices, volumes, bucketCount = DEFAULT_VP_BUCKETS) {
|
|
744
|
-
if (prices.length === 0 || volumes.length === 0) {
|
|
745
|
-
return { poc: 0, vah: 0, val: 0, buckets: [] };
|
|
746
|
-
}
|
|
747
|
-
const minPrice = Math.min(...prices);
|
|
748
|
-
const maxPrice = Math.max(...prices);
|
|
749
|
-
const range = maxPrice - minPrice;
|
|
750
|
-
if (range === 0) {
|
|
751
|
-
return { poc: minPrice, vah: minPrice, val: minPrice, buckets: [{ priceLow: minPrice, priceHigh: minPrice, volume: volumes.reduce((a, b) => a + b, 0), pctOfTotal: 1 }] };
|
|
752
|
-
}
|
|
753
|
-
const bucketSize = range / bucketCount;
|
|
754
|
-
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
|
755
|
-
priceLow: minPrice + i * bucketSize,
|
|
756
|
-
priceHigh: minPrice + (i + 1) * bucketSize,
|
|
757
|
-
volume: 0,
|
|
758
|
-
pctOfTotal: 0,
|
|
759
|
-
}));
|
|
760
|
-
const totalVolume = volumes.reduce((a, b) => a + b, 0);
|
|
761
|
-
if (totalVolume === 0) {
|
|
762
|
-
return { poc: minPrice, vah: maxPrice, val: minPrice, buckets };
|
|
763
|
-
}
|
|
764
|
-
for (let i = 0; i < prices.length; i++) {
|
|
765
|
-
const bucketIdx = Math.min(Math.floor((prices[i] - minPrice) / bucketSize), bucketCount - 1);
|
|
766
|
-
buckets[bucketIdx].volume += volumes[i];
|
|
767
|
-
}
|
|
768
|
-
for (const b of buckets) {
|
|
769
|
-
b.pctOfTotal = b.volume / totalVolume;
|
|
770
|
-
}
|
|
771
|
-
// POC = bucket with highest volume.
|
|
772
|
-
let pocIdx = 0;
|
|
773
|
-
for (let i = 1; i < buckets.length; i++) {
|
|
774
|
-
if (buckets[i].volume > buckets[pocIdx].volume)
|
|
775
|
-
pocIdx = i;
|
|
776
|
-
}
|
|
777
|
-
const poc = (buckets[pocIdx].priceLow + buckets[pocIdx].priceHigh) / 2;
|
|
778
|
-
// Value area: 70% of volume around POC.
|
|
779
|
-
let volAccum = buckets[pocIdx].volume;
|
|
780
|
-
let lowIdx = pocIdx - 1;
|
|
781
|
-
let highIdx = pocIdx + 1;
|
|
782
|
-
while (volAccum / totalVolume < VALUE_AREA_PCT && (lowIdx >= 0 || highIdx < buckets.length)) {
|
|
783
|
-
const lowVol = lowIdx >= 0 ? buckets[lowIdx].volume : 0;
|
|
784
|
-
const highVol = highIdx < buckets.length ? buckets[highIdx].volume : 0;
|
|
785
|
-
if (lowVol >= highVol && lowIdx >= 0) {
|
|
786
|
-
volAccum += lowVol;
|
|
787
|
-
lowIdx--;
|
|
788
|
-
}
|
|
789
|
-
else if (highIdx < buckets.length) {
|
|
790
|
-
volAccum += highVol;
|
|
791
|
-
highIdx++;
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
const val = buckets[lowIdx + 1]?.priceLow ?? minPrice;
|
|
798
|
-
const vah = buckets[highIdx - 1]?.priceHigh ?? maxPrice;
|
|
799
|
-
return { poc, vah, val, buckets };
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* @name registerChartVolumeProfileTool
|
|
803
|
-
* @description Register the sap_chart_volume_profile read-only tool.
|
|
804
|
-
*
|
|
805
|
-
* @param server — MCP server instance.
|
|
806
|
-
* @param context — Runtime context (unused — pure REST + local math).
|
|
807
|
-
*
|
|
808
|
-
* @internal
|
|
809
|
-
*/
|
|
810
|
-
function registerChartVolumeProfileTool(server, _context) {
|
|
811
|
-
const schema = {
|
|
812
|
-
type: 'object',
|
|
813
|
-
properties: {
|
|
814
|
-
mint: {
|
|
815
|
-
type: 'string',
|
|
816
|
-
description: 'Token mint address (base58) to compute volume profile for.',
|
|
817
|
-
},
|
|
818
|
-
buckets: {
|
|
819
|
-
type: 'number',
|
|
820
|
-
description: 'Number of price buckets (default 20).',
|
|
821
|
-
minimum: 5,
|
|
822
|
-
maximum: 100,
|
|
823
|
-
},
|
|
824
|
-
},
|
|
825
|
-
required: ['mint'],
|
|
826
|
-
additionalProperties: false,
|
|
827
|
-
};
|
|
828
|
-
registerTool(server, 'sap_chart_volume_profile', {
|
|
829
|
-
description: 'Compute volume profile (POC, VAH, VAL) for any Solana token using DexScreener price and volume data. Returns point of control, value area high/low, and bucket distribution. Read-only — uses free DexScreener REST API + local computation.',
|
|
830
|
-
inputSchema: schema,
|
|
831
|
-
}, async (args) => {
|
|
832
|
-
const mint = typeof args['mint'] === 'string' ? args['mint'] : '';
|
|
833
|
-
const bucketCount = typeof args['buckets'] === 'number' ? args['buckets'] : DEFAULT_VP_BUCKETS;
|
|
834
|
-
if (!mint) {
|
|
835
|
-
return createTextResponse(JSON.stringify({ error: 'mint is required' }), { isError: true });
|
|
836
|
-
}
|
|
837
|
-
// Fetch token data from DexScreener.
|
|
838
|
-
const url = `${DEXSCREENER_API_URL}/tokens/v1/solana/${encodeURIComponent(mint)}`;
|
|
839
|
-
const pairs = await timedFetch(url);
|
|
840
|
-
if (!pairs || pairs.length === 0) {
|
|
841
|
-
return createTextResponse(JSON.stringify({ error: 'No DexScreener data for this mint' }), { isError: true });
|
|
842
|
-
}
|
|
843
|
-
const pair = pairs[0];
|
|
844
|
-
const priceUsd = pair.priceUsd ? parseFloat(pair.priceUsd) : 0;
|
|
845
|
-
if (priceUsd <= 0) {
|
|
846
|
-
return createTextResponse(JSON.stringify({ error: 'No valid price data' }), { isError: true });
|
|
847
|
-
}
|
|
848
|
-
// Derive price points from price change percentages.
|
|
849
|
-
const pc = pair.priceChange ?? {};
|
|
850
|
-
const vol = pair.volume ?? {};
|
|
851
|
-
const prices = [
|
|
852
|
-
priceUsd / (1 + (pc['m5'] ?? 0) / 100),
|
|
853
|
-
priceUsd / (1 + (pc['h1'] ?? 0) / 100),
|
|
854
|
-
priceUsd / (1 + (pc['h6'] ?? 0) / 100),
|
|
855
|
-
priceUsd / (1 + (pc['h24'] ?? 0) / 100),
|
|
856
|
-
priceUsd,
|
|
857
|
-
];
|
|
858
|
-
const volumes = [
|
|
859
|
-
vol['m5'] ?? 0,
|
|
860
|
-
vol['h1'] ?? 0,
|
|
861
|
-
vol['h6'] ?? 0,
|
|
862
|
-
vol['h24'] ?? 0,
|
|
863
|
-
vol['h24'] ?? 0,
|
|
864
|
-
];
|
|
865
|
-
const profile = computeVolumeProfile(prices, volumes, bucketCount);
|
|
866
|
-
return createTextResponse(JSON.stringify({
|
|
867
|
-
mint,
|
|
868
|
-
symbol: pair.baseToken?.symbol ?? 'unknown',
|
|
869
|
-
priceUsd,
|
|
870
|
-
volumeProfile: profile,
|
|
871
|
-
}));
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
875
|
-
* Tool 7: sap_perp_liquidation_zones
|
|
876
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
877
|
-
/**
|
|
878
|
-
* @name registerPerpLiquidationZonesTool
|
|
879
|
-
* @description Register the sap_perp_liquidation_zones read-only tool.
|
|
880
|
-
*
|
|
881
|
-
* Reads on-chain Adrena positions for a wallet and computes liquidation
|
|
882
|
-
* zones based on leverage, collateral, and current mark price.
|
|
883
|
-
*
|
|
884
|
-
* @param server — MCP server instance.
|
|
885
|
-
* @param context — Runtime context with Solana RPC connection.
|
|
886
|
-
*
|
|
887
|
-
* @internal
|
|
888
|
-
*/
|
|
889
|
-
function registerPerpLiquidationZonesTool(server, context) {
|
|
890
|
-
const schema = {
|
|
891
|
-
type: 'object',
|
|
892
|
-
properties: {
|
|
893
|
-
wallet: {
|
|
894
|
-
type: 'string',
|
|
895
|
-
description: 'Wallet public key (base58) to compute liquidation zones for.',
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
|
-
required: ['wallet'],
|
|
899
|
-
additionalProperties: false,
|
|
900
|
-
};
|
|
901
|
-
registerTool(server, 'sap_perp_liquidation_zones', {
|
|
902
|
-
description: 'Compute liquidation zones for all open Adrena perp positions of a wallet. Reads on-chain positions via Solana RPC and calculates liquidation price, current price, and distance to liquidation. Read-only.',
|
|
903
|
-
inputSchema: schema,
|
|
904
|
-
}, async (args) => {
|
|
905
|
-
const walletStr = typeof args['wallet'] === 'string' ? args['wallet'] : '';
|
|
906
|
-
if (!walletStr) {
|
|
907
|
-
return createTextResponse(JSON.stringify({ error: 'wallet is required' }), { isError: true });
|
|
908
|
-
}
|
|
909
|
-
let walletPubkey;
|
|
910
|
-
try {
|
|
911
|
-
walletPubkey = new PublicKey(walletStr);
|
|
912
|
-
}
|
|
913
|
-
catch {
|
|
914
|
-
return createTextResponse(JSON.stringify({ error: 'Invalid wallet address' }), { isError: true });
|
|
915
|
-
}
|
|
916
|
-
try {
|
|
917
|
-
const accounts = await context.connection.getProgramAccounts(ADRENA_PROGRAM_ID, {
|
|
918
|
-
filters: [
|
|
919
|
-
{ memcmp: { offset: 0, bytes: discToBase58(DISC_POSITION) } },
|
|
920
|
-
{ memcmp: { offset: 8, bytes: walletPubkey.toBase58() } },
|
|
921
|
-
],
|
|
922
|
-
commitment: 'confirmed',
|
|
923
|
-
});
|
|
924
|
-
const zones = accounts.map(({ pubkey, account }) => {
|
|
925
|
-
const data = account.data;
|
|
926
|
-
const sideByte = data[8 + 32 + 32 + 32];
|
|
927
|
-
const side = sideByte === 0 ? 'long' : 'short';
|
|
928
|
-
const entryPrice = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1));
|
|
929
|
-
const sizeUsd = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1 + 8));
|
|
930
|
-
const collateralUsd = Number(data.readBigUInt64LE(8 + 32 + 32 + 32 + 1 + 8 + 8));
|
|
931
|
-
const leverage = collateralUsd > 0 ? sizeUsd / collateralUsd : 0;
|
|
932
|
-
// Liquidation price: for long, price drops by 1/leverage; for short, price rises.
|
|
933
|
-
const liquidationPrice = leverage > 0
|
|
934
|
-
? side === 'long'
|
|
935
|
-
? entryPrice * (1 - 1 / leverage)
|
|
936
|
-
: entryPrice * (1 + 1 / leverage)
|
|
937
|
-
: 0;
|
|
938
|
-
const currentPrice = entryPrice;
|
|
939
|
-
const distanceToLiquidationPct = liquidationPrice > 0
|
|
940
|
-
? Math.abs((currentPrice - liquidationPrice) / currentPrice) * 100
|
|
941
|
-
: 0;
|
|
942
|
-
return {
|
|
943
|
-
positionKey: pubkey.toBase58(),
|
|
944
|
-
market: 'unknown',
|
|
945
|
-
side,
|
|
946
|
-
liquidationPrice,
|
|
947
|
-
currentPrice,
|
|
948
|
-
distanceToLiquidationPct,
|
|
949
|
-
leverage,
|
|
950
|
-
};
|
|
951
|
-
});
|
|
952
|
-
return createTextResponse(JSON.stringify({
|
|
953
|
-
wallet: walletStr,
|
|
954
|
-
zones,
|
|
955
|
-
count: zones.length,
|
|
956
|
-
}));
|
|
957
|
-
}
|
|
958
|
-
catch (err) {
|
|
959
|
-
return createTextResponse(JSON.stringify({
|
|
960
|
-
error: 'Failed to read on-chain positions for liquidation zones',
|
|
961
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
962
|
-
}), { isError: true });
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
967
|
-
* Tool 8: sap_perp_trade_plan
|
|
968
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
969
|
-
/**
|
|
970
|
-
* @name registerPerpTradePlanTool
|
|
971
|
-
* @description Register the professional perps planning tool.
|
|
972
|
-
*
|
|
973
|
-
* This tool does not build a transaction. It turns an intent into a compact
|
|
974
|
-
* risk, sizing, and execution checklist so agents can act like traders while
|
|
975
|
-
* avoiding fake or incomplete unsigned transaction builders.
|
|
976
|
-
*
|
|
977
|
-
* @param server — MCP server instance.
|
|
978
|
-
*
|
|
979
|
-
* @internal
|
|
980
|
-
*/
|
|
981
|
-
function registerPerpTradePlanTool(server) {
|
|
982
|
-
const schema = {
|
|
983
|
-
type: 'object',
|
|
984
|
-
properties: {
|
|
985
|
-
market: {
|
|
986
|
-
type: 'string',
|
|
987
|
-
description: 'Perp market symbol or pair, for example SOL-PERP, BTC-PERP, or ETH-PERP.',
|
|
988
|
-
},
|
|
989
|
-
side: {
|
|
990
|
-
type: 'string',
|
|
991
|
-
description: 'Intended direction for the trade.',
|
|
992
|
-
enum: ['long', 'short'],
|
|
993
|
-
},
|
|
994
|
-
collateralAmountUsd: {
|
|
995
|
-
type: 'number',
|
|
996
|
-
description: 'Collateral to allocate in USD. This is the margin budget, not notional size.',
|
|
997
|
-
minimum: 0,
|
|
998
|
-
},
|
|
999
|
-
leverage: {
|
|
1000
|
-
type: 'number',
|
|
1001
|
-
description: 'Requested leverage multiplier. Keep conservative unless user policy explicitly allows more.',
|
|
1002
|
-
minimum: 1,
|
|
1003
|
-
maximum: 100,
|
|
1004
|
-
},
|
|
1005
|
-
entryPrice: {
|
|
1006
|
-
type: 'number',
|
|
1007
|
-
description: 'Reference entry price in USD used for risk math.',
|
|
1008
|
-
minimum: 0,
|
|
1009
|
-
},
|
|
1010
|
-
stopLossPrice: {
|
|
1011
|
-
type: 'number',
|
|
1012
|
-
description: 'Optional stop loss price in USD. Strongly recommended before execution.',
|
|
1013
|
-
minimum: 0,
|
|
1014
|
-
},
|
|
1015
|
-
takeProfitPrice: {
|
|
1016
|
-
type: 'number',
|
|
1017
|
-
description: 'Optional take profit price in USD used to compute reward/risk.',
|
|
1018
|
-
minimum: 0,
|
|
1019
|
-
},
|
|
1020
|
-
maxAccountRiskPct: {
|
|
1021
|
-
type: 'number',
|
|
1022
|
-
description: 'Maximum account risk percentage allowed by local policy. Default 1%.',
|
|
1023
|
-
minimum: 0,
|
|
1024
|
-
maximum: 100,
|
|
1025
|
-
},
|
|
1026
|
-
maxSlippageBps: {
|
|
1027
|
-
type: 'number',
|
|
1028
|
-
description: 'Maximum execution slippage in basis points. Default 50 bps.',
|
|
1029
|
-
minimum: 0,
|
|
1030
|
-
maximum: 10_000,
|
|
1031
|
-
},
|
|
1032
|
-
timeframe: {
|
|
1033
|
-
type: 'string',
|
|
1034
|
-
description: 'Trading horizon such as scalp, intraday, swing, or hedge.',
|
|
1035
|
-
},
|
|
1036
|
-
notes: {
|
|
1037
|
-
type: 'string',
|
|
1038
|
-
description: 'Optional user notes, catalyst, invalidation thesis, or strategy context.',
|
|
1039
|
-
},
|
|
1040
|
-
},
|
|
1041
|
-
required: ['market', 'side', 'collateralAmountUsd', 'leverage', 'entryPrice'],
|
|
1042
|
-
additionalProperties: false,
|
|
1043
|
-
};
|
|
1044
|
-
registerTool(server, 'sap_perp_trade_plan', {
|
|
1045
|
-
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.',
|
|
1046
|
-
inputSchema: schema,
|
|
1047
|
-
}, async (args) => {
|
|
1048
|
-
const market = String(args['market'] ?? '').trim().toUpperCase();
|
|
1049
|
-
const side = args['side'] === 'short' ? 'short' : 'long';
|
|
1050
|
-
const collateralAmountUsd = typeof args['collateralAmountUsd'] === 'number' ? args['collateralAmountUsd'] : 0;
|
|
1051
|
-
const leverage = typeof args['leverage'] === 'number' ? args['leverage'] : 1;
|
|
1052
|
-
const entryPrice = typeof args['entryPrice'] === 'number' ? args['entryPrice'] : 0;
|
|
1053
|
-
const stopLossPrice = typeof args['stopLossPrice'] === 'number' ? args['stopLossPrice'] : null;
|
|
1054
|
-
const takeProfitPrice = typeof args['takeProfitPrice'] === 'number' ? args['takeProfitPrice'] : null;
|
|
1055
|
-
const maxAccountRiskPct = typeof args['maxAccountRiskPct'] === 'number' ? args['maxAccountRiskPct'] : 1;
|
|
1056
|
-
const maxSlippageBps = typeof args['maxSlippageBps'] === 'number' ? args['maxSlippageBps'] : 50;
|
|
1057
|
-
if (!market || collateralAmountUsd <= 0 || leverage <= 0 || entryPrice <= 0) {
|
|
1058
|
-
return createTextResponse(JSON.stringify({
|
|
1059
|
-
error: 'market, collateralAmountUsd, leverage, and entryPrice are required and must be positive.',
|
|
1060
|
-
}), { isError: true });
|
|
1061
|
-
}
|
|
1062
|
-
const notionalUsd = collateralAmountUsd * leverage;
|
|
1063
|
-
const stopMovePct = stopLossPrice && stopLossPrice > 0
|
|
1064
|
-
? Math.abs((entryPrice - stopLossPrice) / entryPrice) * 100
|
|
1065
|
-
: null;
|
|
1066
|
-
const takeProfitMovePct = takeProfitPrice && takeProfitPrice > 0
|
|
1067
|
-
? Math.abs((takeProfitPrice - entryPrice) / entryPrice) * 100
|
|
1068
|
-
: null;
|
|
1069
|
-
const estimatedStopRiskUsd = stopMovePct === null ? null : notionalUsd * (stopMovePct / 100);
|
|
1070
|
-
const estimatedRewardUsd = takeProfitMovePct === null ? null : notionalUsd * (takeProfitMovePct / 100);
|
|
1071
|
-
const rewardRisk = estimatedStopRiskUsd && estimatedRewardUsd
|
|
1072
|
-
? estimatedRewardUsd / estimatedStopRiskUsd
|
|
1073
|
-
: null;
|
|
1074
|
-
const liquidationEstimate = side === 'long'
|
|
1075
|
-
? entryPrice * (1 - (1 / leverage))
|
|
1076
|
-
: entryPrice * (1 + (1 / leverage));
|
|
1077
|
-
const riskFlags = [];
|
|
1078
|
-
if (!stopLossPrice)
|
|
1079
|
-
riskFlags.push('missing_stop_loss');
|
|
1080
|
-
if (estimatedStopRiskUsd !== null && estimatedStopRiskUsd > collateralAmountUsd * (maxAccountRiskPct / 100)) {
|
|
1081
|
-
riskFlags.push('stop_risk_exceeds_policy');
|
|
1082
|
-
}
|
|
1083
|
-
if (leverage > 10)
|
|
1084
|
-
riskFlags.push('high_leverage_requires_explicit_user_confirmation');
|
|
1085
|
-
if (maxSlippageBps > 100)
|
|
1086
|
-
riskFlags.push('slippage_above_1_percent');
|
|
1087
|
-
return createTextResponse(JSON.stringify({
|
|
1088
|
-
market,
|
|
1089
|
-
side,
|
|
1090
|
-
executionStatus: 'analysis_only',
|
|
1091
|
-
collateralAmountUsd,
|
|
1092
|
-
leverage,
|
|
1093
|
-
notionalUsd,
|
|
1094
|
-
entryPrice,
|
|
1095
|
-
stopLossPrice,
|
|
1096
|
-
takeProfitPrice,
|
|
1097
|
-
maxAccountRiskPct,
|
|
1098
|
-
maxSlippageBps,
|
|
1099
|
-
liquidationEstimate,
|
|
1100
|
-
estimatedStopRiskUsd,
|
|
1101
|
-
estimatedRewardUsd,
|
|
1102
|
-
rewardRisk,
|
|
1103
|
-
riskFlags,
|
|
1104
|
-
professionalChecklist: [
|
|
1105
|
-
'Call sap_perp_markets for current market/custody data before execution.',
|
|
1106
|
-
'Call sap_chart_ohlc and sap_chart_volume_profile to validate trend, liquidity, POC, VAH, and VAL.',
|
|
1107
|
-
'Call sap_perp_position_info and sap_perp_liquidation_zones for the user wallet before increasing exposure.',
|
|
1108
|
-
'Show the user one compact preview: side, notional, margin, leverage, stop, take profit, liquidation estimate, slippage, and risk flags.',
|
|
1109
|
-
'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.',
|
|
1110
|
-
],
|
|
1111
|
-
recommendedReadTools: [
|
|
1112
|
-
'sap_perp_markets',
|
|
1113
|
-
'sap_chart_ohlc',
|
|
1114
|
-
'sap_chart_volume_profile',
|
|
1115
|
-
'sap_perp_position_info',
|
|
1116
|
-
'sap_perp_liquidation_zones',
|
|
1117
|
-
],
|
|
1118
|
-
executionWarning: 'SAP MCP intentionally does not expose manual Adrena execution builders. Execution must use a complete IDL-backed route before local finalization. If a hosted direct Adrena signer tool returns hosted_local_signer_required, do not route it through sap_payments_call_paid_tool; no x402 fee should be charged and there is no unsigned transaction to finalize.',
|
|
1119
|
-
notes: typeof args['notes'] === 'string' ? args['notes'] : undefined,
|
|
1120
|
-
}, null, 2));
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
1124
|
-
* Main registration function
|
|
1125
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
1126
|
-
/**
|
|
1127
|
-
* @name registerPerpTools
|
|
1128
|
-
* @description Register all 8 perp trading analytics and planning MCP tools.
|
|
1129
|
-
*
|
|
1130
|
-
* @param server — MCP server instance.
|
|
1131
|
-
* @param context — Shared runtime context with SAP client, connection, and config.
|
|
1132
|
-
*
|
|
1133
|
-
* @usedBy `register-tools.ts`
|
|
1134
|
-
*/
|
|
1135
|
-
export function registerPerpTools(server, context) {
|
|
1136
|
-
logger.debug('Registering perp trading analytics and planning tools');
|
|
1137
|
-
registerPerpMarketsTool(server, context);
|
|
1138
|
-
registerPerpPositionInfoTool(server, context);
|
|
1139
|
-
registerPerpFundingHistoryTool(server, context);
|
|
1140
|
-
registerChartOhlcTool(server, context);
|
|
1141
|
-
registerChartLongTermTool(server, context);
|
|
1142
|
-
registerChartVolumeProfileTool(server, context);
|
|
1143
|
-
registerPerpLiquidationZonesTool(server, context);
|
|
1144
|
-
registerPerpTradePlanTool(server);
|
|
1145
|
-
logger.debug('Perp tools registered', { count: 8 });
|
|
1146
|
-
}
|
|
8
|
+
export { registerPerpTools } from '../perps/perp-analytics-tools.js';
|
|
9
|
+
export { ADRENA_POSITION_OWNER_MEMCMP_OFFSET } from '../perps/perp-constants.js';
|
|
10
|
+
export { decodeAdrenaCustodyAccount, decodeAdrenaPositionAccount, discToBase58, readAdrenaLimitedString } from '../perps/perp-decoders.js';
|
|
11
|
+
export { normalizePerpProviderMarkets } from '../perps/perp-constants.js';
|
|
1147
12
|
//# sourceMappingURL=perp-tools.js.map
|