@usenami/signer-mcp 0.1.0
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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +129 -0
- package/dist/lib.js +281 -0
- package/dist/lib.js.map +1 -0
- package/dist/parsers/asterdex.d.ts +35 -0
- package/dist/parsers/asterdex.js +79 -0
- package/dist/parsers/asterdex.js.map +1 -0
- package/dist/parsers/binance.d.ts +32 -0
- package/dist/parsers/binance.js +72 -0
- package/dist/parsers/binance.js.map +1 -0
- package/dist/parsers/index.d.ts +16 -0
- package/dist/parsers/index.js +25 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/okx.d.ts +34 -0
- package/dist/parsers/okx.js +104 -0
- package/dist/parsers/okx.js.map +1 -0
- package/dist/parsers/types.d.ts +62 -0
- package/dist/parsers/types.js +10 -0
- package/dist/parsers/types.js.map +1 -0
- package/package.json +65 -0
- package/server.json +62 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKX v5 account parser.
|
|
3
|
+
*
|
|
4
|
+
* OKX splits balance and positions across TWO endpoints:
|
|
5
|
+
* GET /api/v5/account/balance → equity + free margin
|
|
6
|
+
* GET /api/v5/account/positions → open positions
|
|
7
|
+
*
|
|
8
|
+
* For the Option-A architecture this means signer's `/account/<venue>` for
|
|
9
|
+
* OKX may need to return TWO signed requests (or one composite). For v0 we
|
|
10
|
+
* accept a combined raw payload:
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* "balance": { ...OKX /balance response... },
|
|
14
|
+
* "positions": { ...OKX /positions response... }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* If only `balance` is present (positions response missing), we still emit
|
|
18
|
+
* a valid NormalizedAccount with empty positions — the agent can decide.
|
|
19
|
+
*
|
|
20
|
+
* OKX response wrapper: { "code": "0", "msg": "", "data": [ {...} ] }
|
|
21
|
+
* Single-element data arrays are typical for these endpoints.
|
|
22
|
+
*
|
|
23
|
+
* Docs:
|
|
24
|
+
* https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance
|
|
25
|
+
* https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions
|
|
26
|
+
*
|
|
27
|
+
* Quirks:
|
|
28
|
+
* - OKX returns numbers as strings (like Binance).
|
|
29
|
+
* - Empty equity field returns "" not null.
|
|
30
|
+
* - Positions array can include zeroed-out rows after closes; filter them.
|
|
31
|
+
* - `instId` is OKX's symbol field (e.g. "BTC-USDT-SWAP").
|
|
32
|
+
*/
|
|
33
|
+
function toNum(v, fallback = 0) {
|
|
34
|
+
if (typeof v === "number")
|
|
35
|
+
return Number.isFinite(v) ? v : fallback;
|
|
36
|
+
if (typeof v === "string" && v.length > 0) {
|
|
37
|
+
const n = parseFloat(v);
|
|
38
|
+
return Number.isFinite(n) ? n : fallback;
|
|
39
|
+
}
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
function unwrapOkxArray(payload) {
|
|
43
|
+
// Standard shape: { code: "0", data: [ {...} ] }
|
|
44
|
+
const obj = payload;
|
|
45
|
+
if (!obj || !Array.isArray(obj.data))
|
|
46
|
+
return null;
|
|
47
|
+
const first = obj.data[0];
|
|
48
|
+
return first ?? null;
|
|
49
|
+
}
|
|
50
|
+
export const parseOkxAccount = (raw) => {
|
|
51
|
+
const wrap = raw ?? {};
|
|
52
|
+
const balanceWrap = unwrapOkxArray(wrap.balance);
|
|
53
|
+
const positionsWrap = wrap.positions;
|
|
54
|
+
// Equity + free margin from /balance.
|
|
55
|
+
// OKX returns multi-currency `details: [...]` per account; we sum USDT/USDC
|
|
56
|
+
// available balances and use totalEq (already USD-normalized).
|
|
57
|
+
const equity = toNum(balanceWrap?.totalEq);
|
|
58
|
+
let freeMargin = 0;
|
|
59
|
+
const details = Array.isArray(balanceWrap?.details) ? balanceWrap?.details : [];
|
|
60
|
+
for (const d of details) {
|
|
61
|
+
const ccy = String(d.ccy ?? "");
|
|
62
|
+
if (ccy === "USDT" || ccy === "USDC" || ccy === "USD") {
|
|
63
|
+
// availBal is the spendable balance in that currency. Sum across stable
|
|
64
|
+
// collateral types — close enough for v0 USD-equivalent.
|
|
65
|
+
freeMargin += toNum(d.availBal);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Fallback: if no stable collateral row, use adjEq as a rough proxy.
|
|
69
|
+
if (freeMargin === 0) {
|
|
70
|
+
freeMargin = toNum(balanceWrap?.adjEq);
|
|
71
|
+
}
|
|
72
|
+
// Positions from /positions.
|
|
73
|
+
const positions = [];
|
|
74
|
+
if (positionsWrap && Array.isArray(positionsWrap.data)) {
|
|
75
|
+
for (const p of positionsWrap.data) {
|
|
76
|
+
const qty = toNum(p.pos);
|
|
77
|
+
if (qty === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const symbol = String(p.instId ?? "");
|
|
80
|
+
if (symbol === "")
|
|
81
|
+
continue;
|
|
82
|
+
positions.push({
|
|
83
|
+
symbol,
|
|
84
|
+
qty,
|
|
85
|
+
entry_price: toNum(p.avgPx),
|
|
86
|
+
unrealized_pnl: toNum(p.upl),
|
|
87
|
+
mark_price: p.markPx !== undefined ? toNum(p.markPx) : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// OKX gives uTime / cTime (timestamps in ms) — use balance.uTime when present.
|
|
92
|
+
const ts = balanceWrap?.uTime;
|
|
93
|
+
const updated_at = typeof ts === "string" && ts.length > 0
|
|
94
|
+
? new Date(parseInt(ts, 10)).toISOString()
|
|
95
|
+
: new Date(0).toISOString();
|
|
96
|
+
return {
|
|
97
|
+
venue: "okx",
|
|
98
|
+
equity_usd: equity,
|
|
99
|
+
free_margin_usd: freeMargin,
|
|
100
|
+
positions,
|
|
101
|
+
updated_at,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=okx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"okx.js","sourceRoot":"","sources":["../../src/parsers/okx.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common shapes for venue parsers under the Option-A architecture (signer
|
|
3
|
+
* returns signed read request, signer-mcp executes it + parses).
|
|
4
|
+
*
|
|
5
|
+
* Per signer's 2026-05-31T2150 schema-coordination report. The MCP tool
|
|
6
|
+
* contract returns this normalized shape — venue-specific quirks live
|
|
7
|
+
* inside each parser.
|
|
8
|
+
*/
|
|
9
|
+
export interface NormalizedAccount {
|
|
10
|
+
/** Venue id (binance, okx, asterdex) — echoed for agent introspection. */
|
|
11
|
+
venue: string;
|
|
12
|
+
/**
|
|
13
|
+
* Total margin balance in USD-equivalent. Some venues return USDT-collat;
|
|
14
|
+
* we treat USDT as USD for v0 (acceptable for the demo, real conversion
|
|
15
|
+
* via mark price comes in v0.1).
|
|
16
|
+
*/
|
|
17
|
+
equity_usd: number;
|
|
18
|
+
/**
|
|
19
|
+
* Free margin = equity - used by open positions. Used for `place_order`
|
|
20
|
+
* pre-checks ("do I have margin?").
|
|
21
|
+
*/
|
|
22
|
+
free_margin_usd: number;
|
|
23
|
+
/** Per-symbol open positions. Empty array when flat. */
|
|
24
|
+
positions: NormalizedPosition[];
|
|
25
|
+
/**
|
|
26
|
+
* Server-side timestamp from the venue. ISO 8601 if the venue gives one,
|
|
27
|
+
* otherwise a normalized fetch timestamp (parser sets it).
|
|
28
|
+
*/
|
|
29
|
+
updated_at: string;
|
|
30
|
+
}
|
|
31
|
+
export interface NormalizedPosition {
|
|
32
|
+
symbol: string;
|
|
33
|
+
/**
|
|
34
|
+
* Signed quantity in base asset. Positive = long, negative = short. Zero
|
|
35
|
+
* positions are filtered out before returning.
|
|
36
|
+
*/
|
|
37
|
+
qty: number;
|
|
38
|
+
/** Volume-weighted average entry price. */
|
|
39
|
+
entry_price: number;
|
|
40
|
+
/** Unrealized PnL in venue's quote/collat currency. */
|
|
41
|
+
unrealized_pnl: number;
|
|
42
|
+
/** Mark price if the venue provides it (Binance does, OKX does, Asterdex partial). */
|
|
43
|
+
mark_price?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface SignedRequest {
|
|
46
|
+
venue: string;
|
|
47
|
+
method: "GET" | "POST" | "DELETE";
|
|
48
|
+
url: string;
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
body?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A parser turns a venue's raw account-endpoint response into our normalized
|
|
54
|
+
* shape. Each parser knows ONE venue's quirks; the dispatcher in
|
|
55
|
+
* parsers/index.ts picks the right one based on venue id.
|
|
56
|
+
*
|
|
57
|
+
* Parsers MUST be tolerant: missing fields → return 0 or [], never throw.
|
|
58
|
+
* Throwing in a parser cascades to the tool error path, which kills the
|
|
59
|
+
* agent's ability to call place_order even when the account read partially
|
|
60
|
+
* worked.
|
|
61
|
+
*/
|
|
62
|
+
export type AccountParser = (raw: unknown) => NormalizedAccount;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common shapes for venue parsers under the Option-A architecture (signer
|
|
3
|
+
* returns signed read request, signer-mcp executes it + parses).
|
|
4
|
+
*
|
|
5
|
+
* Per signer's 2026-05-31T2150 schema-coordination report. The MCP tool
|
|
6
|
+
* contract returns this normalized shape — venue-specific quirks live
|
|
7
|
+
* inside each parser.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/parsers/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usenami/signer-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sign CEX orders from any MCP-aware AI agent (Claude Desktop, Cursor, ElizaOS) with keys that never leave an AWS Nitro Enclave.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"signer-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"server.json"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepare": "npm run build",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "tsx src/index.ts",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"smoke": "npm run build && tsx scripts/smoke-test.ts"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"trading",
|
|
29
|
+
"binance",
|
|
30
|
+
"okx",
|
|
31
|
+
"perpetuals",
|
|
32
|
+
"attested-compute",
|
|
33
|
+
"aws-nitro-enclaves",
|
|
34
|
+
"claude",
|
|
35
|
+
"cursor",
|
|
36
|
+
"elizaos",
|
|
37
|
+
"ai-agents",
|
|
38
|
+
"usenami",
|
|
39
|
+
"signer"
|
|
40
|
+
],
|
|
41
|
+
"author": "Usenami",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://usenami.io/signer",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/namixai/namixai-terminal.git",
|
|
47
|
+
"directory": "signer-mcp"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/namixai/namixai-terminal/issues"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
57
|
+
"zod": "^3.23.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.19.19",
|
|
61
|
+
"tsx": "^4.19.0",
|
|
62
|
+
"typescript": "^5.6.0",
|
|
63
|
+
"vitest": "^4.1.7"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://registry.modelcontextprotocol.io/schemas/server.json",
|
|
3
|
+
"name": "@usenami/signer-mcp",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Sign CEX orders (Binance, OKX, Asterdex) from any MCP-aware AI agent with keys that never leave an AWS Nitro Enclave.",
|
|
6
|
+
"homepage": "https://usenami.io/signer",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"tags": [
|
|
9
|
+
"trading",
|
|
10
|
+
"binance",
|
|
11
|
+
"okx",
|
|
12
|
+
"perpetuals",
|
|
13
|
+
"attested-compute",
|
|
14
|
+
"aws-nitro-enclaves",
|
|
15
|
+
"signer",
|
|
16
|
+
"ai-agents"
|
|
17
|
+
],
|
|
18
|
+
"categories": ["trading", "finance", "security"],
|
|
19
|
+
"transport": {
|
|
20
|
+
"type": "stdio",
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@usenami/signer-mcp"]
|
|
23
|
+
},
|
|
24
|
+
"env_schema": {
|
|
25
|
+
"SIGNER_GATEWAY_URL": {
|
|
26
|
+
"description": "Override the Signer gateway URL (default: https://signer.usenami.io)",
|
|
27
|
+
"required": false
|
|
28
|
+
},
|
|
29
|
+
"SIGNER_API_TOKEN": {
|
|
30
|
+
"description": "Bearer token issued by usenami.io/signer. Required for everything except list_venues.",
|
|
31
|
+
"required": false,
|
|
32
|
+
"secret": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"tools": [
|
|
36
|
+
{
|
|
37
|
+
"name": "list_venues",
|
|
38
|
+
"description": "List the venues supported by this Signer. Read-only static manifest — no gateway call.",
|
|
39
|
+
"readOnly": true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "get_attestation",
|
|
43
|
+
"description": "Return the AWS Nitro attestation document (PCR0+sig) for the running enclave.",
|
|
44
|
+
"readOnly": true
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "get_account",
|
|
48
|
+
"description": "Return equity, free margin, and open positions for a venue.",
|
|
49
|
+
"readOnly": true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "place_order",
|
|
53
|
+
"description": "Sign and submit a single market or limit order. Enclave enforces policy caps.",
|
|
54
|
+
"readOnly": false
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "cancel_order",
|
|
58
|
+
"description": "Cancel an outstanding order by venue order_id. Idempotent.",
|
|
59
|
+
"readOnly": false
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|