@stratabook/mcp 0.2.16 → 0.2.18
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 +23 -0
- package/README.md +43 -14
- package/dist/src/autonomy.d.ts +9 -0
- package/dist/src/autonomy.js +38 -0
- package/dist/src/cli.js +26 -6
- package/dist/src/pairing.d.ts +2 -0
- package/dist/src/pairing.js +38 -14
- package/dist/src/server.d.ts +16 -1
- package/dist/src/server.js +559 -34
- package/dist/src/usability.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to the Strata SDKs (`@stratabook/sdk`, `@stratabook/mcp`,
|
|
4
4
|
and the `strata-sdk` Rust crate) are recorded here. Versions move together.
|
|
5
5
|
|
|
6
|
+
## 0.2.18
|
|
7
|
+
|
|
8
|
+
- Hot-reload locally paired session credentials inside a running MCP process;
|
|
9
|
+
`connect`, session replacement, and disconnect no longer require a client
|
|
10
|
+
restart.
|
|
11
|
+
- Expose `strata_order_execute` in simple mode with human limit-order intent:
|
|
12
|
+
market label, side, percentage of available balance, and signed mark offset.
|
|
13
|
+
Strata resolves exact asset atoms, decimals, mark and tick alignment.
|
|
14
|
+
- Keep authenticated order channels warm for durable dead-man heartbeats. A
|
|
15
|
+
placement whose guard cannot be proven is cancelled fail-closed.
|
|
16
|
+
- Expand `strata_autonomy` into a public-key-only runtime handshake covering
|
|
17
|
+
package version, mode, credential source, loaded/file/on-chain session
|
|
18
|
+
consistency, execution readiness, expiry, and clock skew.
|
|
19
|
+
- Preserve exact `retry_after_ms` values instead of retrying every transient
|
|
20
|
+
read after a fixed 150ms.
|
|
21
|
+
|
|
22
|
+
## 0.2.17
|
|
23
|
+
|
|
24
|
+
- Consume the matching strategy-timed TypeScript SDK and allow an optional
|
|
25
|
+
zero cadence floor in advanced setup calls.
|
|
26
|
+
- Keep the local pairing listener alive longer and close its helper tab after
|
|
27
|
+
credentials are saved, with a branded Strata return page as the fallback.
|
|
28
|
+
|
|
6
29
|
## 0.2.16
|
|
7
30
|
|
|
8
31
|
- Consume the matching TypeScript SDK 0.2.16 packaging-parity release. MCP
|
package/README.md
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://stratabook.app">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/alsk1992/strata-mcp/main/assets/readme-hero.svg?v=20260826-2" alt="Strata MCP — The deepest book in DeFi" width="100%" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://stratabook.app">Trade</a> ·
|
|
9
|
+
<a href="https://stratabook.org/docs/agent-mcp">Docs</a> ·
|
|
10
|
+
<a href="https://www.npmjs.com/package/@stratabook/mcp">npm</a> ·
|
|
11
|
+
<a href="https://api.stratabook.app/mcp">Hosted MCP</a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
1
14
|
# Strata MCP
|
|
2
15
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
16
|
+
Give any MCP-compatible agent direct access to Strata markets and Sonar. It
|
|
17
|
+
works across Claude Desktop, Cursor, Windsurf, Codex and any standards-based
|
|
18
|
+
MCP host.
|
|
19
|
+
|
|
20
|
+
Read-only access works immediately. No wallet, API key, session key, environment
|
|
21
|
+
variable or settings screen is required to explore markets, books, prices,
|
|
22
|
+
candles, trades, quotes and public maker data.
|
|
23
|
+
|
|
24
|
+
| How you connect | Best for | What stays private |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Hosted: `https://api.stratabook.app/mcp` | Instant reads, Sonar quotes and externally signed prepare/submit flows | The shared server never receives a session secret |
|
|
27
|
+
| Local: `npx -y @stratabook/mcp` | Reads now; optional one-call trading after `connect` | The session secret remains in a mode-0600 file on the user's machine |
|
|
28
|
+
|
|
29
|
+
Intent, Strand and Current controls are live. A connected local MCP can execute
|
|
30
|
+
session-backed trades, orders, TWAPs and Intent updates in one call. The hosted
|
|
31
|
+
shared MCP exposes Intent prepare/submit instead because it deliberately never
|
|
32
|
+
stores a user's session secret. Current tracks Strata's live mark automatically.
|
|
6
33
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
operation live. It does **not** mean quotes need activation or user setup.
|
|
34
|
+
Tool availability follows Strata's live safety policy. If an operation is
|
|
35
|
+
paused, its tool disappears immediately; everyday reads and quotes require no
|
|
36
|
+
activation.
|
|
11
37
|
|
|
12
38
|
The default tool mode is deliberately compact. Agents call the requested tool
|
|
13
39
|
directly instead of burning discovery calls before a quote. Use
|
|
14
40
|
`--mode advanced` (or `STRATA_MCP_MODE=advanced`) only when an integration
|
|
15
41
|
needs the explicit challenge / prepare / submit protocol tools.
|
|
16
42
|
|
|
17
|
-
##
|
|
43
|
+
## Start locally
|
|
18
44
|
|
|
19
45
|
Read-only use needs no wallet, key, autonomy setting, or environment variable:
|
|
20
46
|
|
|
@@ -36,9 +62,11 @@ returns to the live Control Center. Run the same command again to replace the
|
|
|
36
62
|
old key atomically; the old key is revoked in the same signed transaction.
|
|
37
63
|
|
|
38
64
|
There is no secret to paste into chat, no environment-variable screen, and no
|
|
39
|
-
client config to edit after the read-only server is installed.
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
client config to edit after the read-only server is installed. A running MCP
|
|
66
|
+
process detects the new, replaced, or removed local credential automatically;
|
|
67
|
+
no client restart is required for credential changes. A package-version upgrade
|
|
68
|
+
still requires the MCP host to launch the new process. Revoke the exact session
|
|
69
|
+
and delete its local credential with:
|
|
42
70
|
|
|
43
71
|
```sh
|
|
44
72
|
npx -y @stratabook/mcp disconnect
|
|
@@ -80,8 +108,9 @@ The compact default exposes the tools ordinary users need:
|
|
|
80
108
|
- `strata_quote` — accepts `0.1 SOL`, `20 USDC`, or `$20`; token atoms remain optional
|
|
81
109
|
- `strata_portfolio` and `strata_market_making_status`
|
|
82
110
|
- `strata_trade` — returns a live quote when trading is not connected, with one setup link; follows the user's session limits when connected
|
|
111
|
+
- `strata_order_execute` — understands instructions such as “sell 10% of available SOL at 3% above mark”; resolves exact atoms and tick rounding, submits through a warm order channel, and maintains fail-closed dead-man protection
|
|
83
112
|
- `strata_market_making_prepare` and `strata_market_making_submit_and_wait`
|
|
84
|
-
- `strata_autonomy` — reports
|
|
113
|
+
- `strata_autonomy` — reports the user's limits plus MCP version/mode, loaded and on-chain session consistency, execution readiness, expiry, and clock skew
|
|
85
114
|
|
|
86
115
|
Advanced mode additionally exposes the complete protocol surface, including:
|
|
87
116
|
|
|
@@ -167,8 +196,8 @@ original confirmed signature while the packet remains live.
|
|
|
167
196
|
For maker funding, initialize the market Vault if needed, activate the Strand
|
|
168
197
|
or Current, then deposit with `strata_vault_deposit`. The market keeps that
|
|
169
198
|
available collateral while a control is live and returns it after the final
|
|
170
|
-
control is disabled, exhausted, expired, or cancelled. Current
|
|
171
|
-
live mark
|
|
199
|
+
control is disabled, exhausted, expired, or cancelled. Current tracks Strata's
|
|
200
|
+
live mark automatically.
|
|
172
201
|
|
|
173
202
|
The matching TypeScript package ships `strata-maker-conformance`. Its default
|
|
174
203
|
`safe` mode exercises this hosted MCP's discovery, public maker reads, and
|
package/dist/src/autonomy.d.ts
CHANGED
|
@@ -42,6 +42,15 @@ export interface SessionAutonomy {
|
|
|
42
42
|
readonly config: AutonomyConfig;
|
|
43
43
|
readonly dailyBudget: DailyUsdBudget;
|
|
44
44
|
}
|
|
45
|
+
export type SessionAutonomyProvider = () => Promise<SessionAutonomy | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Build a live session source for long-running MCP hosts. The credential file
|
|
48
|
+
* may be replaced by `strata-mcp connect` while the stdio process remains
|
|
49
|
+
* alive, so callers must not close over the first environment snapshot. Daily
|
|
50
|
+
* spend remains process-local and survives policy/file refreshes for the same
|
|
51
|
+
* owner + session pair.
|
|
52
|
+
*/
|
|
53
|
+
export declare function liveSessionAutonomyProvider(loadEnvironment: () => Promise<Record<string, string | undefined>>): SessionAutonomyProvider;
|
|
45
54
|
/** Parse the autonomy slider from a plain env bag; defaults to the calm `ask`. */
|
|
46
55
|
export declare function parseAutonomyConfig(env: Record<string, string | undefined>): AutonomyConfig;
|
|
47
56
|
/** Build the session-autonomy context from env, or null when no session key is set. */
|
package/dist/src/autonomy.js
CHANGED
|
@@ -26,6 +26,44 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { StrataContractError, sessionSignerFromSecretKey, } from "@stratabook/sdk";
|
|
28
28
|
export const AUTONOMY_LEVELS = ["ask", "limits", "instant"];
|
|
29
|
+
/**
|
|
30
|
+
* Build a live session source for long-running MCP hosts. The credential file
|
|
31
|
+
* may be replaced by `strata-mcp connect` while the stdio process remains
|
|
32
|
+
* alive, so callers must not close over the first environment snapshot. Daily
|
|
33
|
+
* spend remains process-local and survives policy/file refreshes for the same
|
|
34
|
+
* owner + session pair.
|
|
35
|
+
*/
|
|
36
|
+
export function liveSessionAutonomyProvider(loadEnvironment) {
|
|
37
|
+
let fingerprint = null;
|
|
38
|
+
let current = null;
|
|
39
|
+
const budgets = new Map();
|
|
40
|
+
return async () => {
|
|
41
|
+
const env = await loadEnvironment();
|
|
42
|
+
const nextFingerprint = [
|
|
43
|
+
env.STRATA_OWNER_WALLET ?? "",
|
|
44
|
+
env.STRATA_SESSION_PUBLIC_KEY ?? "",
|
|
45
|
+
env.STRATA_SESSION_SECRET_KEY ?? "",
|
|
46
|
+
env.STRATA_AUTONOMY ?? "",
|
|
47
|
+
env.STRATA_AUTONOMY_MAX_USD_PER_TRADE ?? "",
|
|
48
|
+
env.STRATA_AUTONOMY_MAX_USD_PER_DAY ?? "",
|
|
49
|
+
env.STRATA_AUTONOMY_MARKETS ?? "",
|
|
50
|
+
].join("\0");
|
|
51
|
+
if (fingerprint === nextFingerprint)
|
|
52
|
+
return current;
|
|
53
|
+
const parsed = await sessionAutonomyFromEnv(env);
|
|
54
|
+
if (parsed === null) {
|
|
55
|
+
fingerprint = nextFingerprint;
|
|
56
|
+
current = null;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const budgetKey = `${parsed.ownerWallet}:${parsed.signer.publicKey}`;
|
|
60
|
+
const dailyBudget = budgets.get(budgetKey) ?? parsed.dailyBudget;
|
|
61
|
+
budgets.set(budgetKey, dailyBudget);
|
|
62
|
+
current = { ...parsed, dailyBudget };
|
|
63
|
+
fingerprint = nextFingerprint;
|
|
64
|
+
return current;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
29
67
|
function positiveNumber(raw) {
|
|
30
68
|
if (raw === undefined)
|
|
31
69
|
return undefined;
|
package/dist/src/cli.js
CHANGED
|
@@ -4,11 +4,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
5
|
import { DEFAULT_API_BASE, StrataApiError, StrataClient } from "@stratabook/sdk";
|
|
6
6
|
import { STRATA_ACTION_GRAPH, STRATA_AGENT_HARNESS, } from "./generated-harness.js";
|
|
7
|
-
import { createStrataMcpServer, probeStrataMcpReadiness } from "./server.js";
|
|
8
|
-
import {
|
|
7
|
+
import { createStrataMcpServer, probeStrataMcpReadiness, } from "./server.js";
|
|
8
|
+
import { liveSessionAutonomyProvider, } from "./autonomy.js";
|
|
9
9
|
import { SERVER_VERSION } from "./version.js";
|
|
10
10
|
import { friendlyApiError, humanQuoteAmount, parseToolMode } from "./usability.js";
|
|
11
|
-
import { DEFAULT_PAIRING_WEB_BASE, loadTradingEnvironment, runLocalPairing, tradingCredentialsPath, } from "./pairing.js";
|
|
11
|
+
import { DEFAULT_PAIRING_WEB_BASE, loadTradingEnvironment, readTradingConnection, runLocalPairing, tradingCredentialsPath, } from "./pairing.js";
|
|
12
12
|
function parse(argv) {
|
|
13
13
|
const knownCommands = new Set(["doctor", "connect", "disconnect"]);
|
|
14
14
|
const first = argv[0];
|
|
@@ -233,13 +233,33 @@ async function main() {
|
|
|
233
233
|
});
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
|
-
const
|
|
236
|
+
const runtimeEnvironment = options.credentialsFile === undefined
|
|
237
|
+
? { ...process.env }
|
|
238
|
+
: { ...process.env, STRATA_MCP_CREDENTIALS_FILE: options.credentialsFile };
|
|
239
|
+
const sessionEnv = await loadTradingEnvironment(runtimeEnvironment);
|
|
237
240
|
if (options.command === "doctor") {
|
|
238
241
|
await runDoctor(options, sessionEnv);
|
|
239
242
|
return;
|
|
240
243
|
}
|
|
241
|
-
const
|
|
242
|
-
const
|
|
244
|
+
const sessionAutonomyProvider = liveSessionAutonomyProvider(() => loadTradingEnvironment(runtimeEnvironment));
|
|
245
|
+
const sessionAutonomy = await sessionAutonomyProvider();
|
|
246
|
+
const sessionCredentialDiagnostics = async () => {
|
|
247
|
+
const path = tradingCredentialsPath(runtimeEnvironment);
|
|
248
|
+
const stored = await readTradingConnection(path);
|
|
249
|
+
const explicit = Boolean(runtimeEnvironment.STRATA_SESSION_SECRET_KEY || runtimeEnvironment.STRATA_OWNER_WALLET);
|
|
250
|
+
return {
|
|
251
|
+
source: explicit ? "environment" : stored === null ? "none" : "file",
|
|
252
|
+
credentials_file: path,
|
|
253
|
+
credential_file_session_public_key: stored?.session_public_key ?? null,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const withSession = {
|
|
257
|
+
...options,
|
|
258
|
+
sessionAutonomyProvider,
|
|
259
|
+
sessionCredentialDiagnostics,
|
|
260
|
+
persistentOrderGuards: options.transport === "stdio",
|
|
261
|
+
...(sessionAutonomy === null ? {} : { sessionAutonomy }),
|
|
262
|
+
};
|
|
243
263
|
if (sessionAutonomy) {
|
|
244
264
|
process.stderr.write(`[strata-mcp] session autonomy: ${sessionAutonomy.config.level} `
|
|
245
265
|
+ `(wallet ${sessionAutonomy.ownerWallet.slice(0, 6)}…, session ${sessionAutonomy.signer.publicKey.slice(0, 6)}…, `
|
package/dist/src/pairing.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface PairingOptions {
|
|
|
17
17
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
18
18
|
readonly nowMs?: () => number;
|
|
19
19
|
}
|
|
20
|
+
/** Local success page: close the helper tab when allowed, otherwise return to Strata. */
|
|
21
|
+
export declare function pairingCompletionDocument(returnUrl: string): string;
|
|
20
22
|
export declare function tradingCredentialsPath(env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, home?: string): string;
|
|
21
23
|
export declare function readTradingConnection(path?: string): Promise<StoredTradingConnection | null>;
|
|
22
24
|
export declare function writeTradingConnection(connection: StoredTradingConnection, path?: string): Promise<void>;
|
package/dist/src/pairing.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { DEFAULT_API_BASE, generateSessionKeypair, } from "@stratabook/sdk";
|
|
8
8
|
const PUBLIC_KEY_PATTERN = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
9
9
|
const SECRET_KEY_PATTERN = /^[1-9A-HJ-NP-Za-km-z]{80,90}$/;
|
|
10
|
-
const PAIRING_TIMEOUT_MS =
|
|
10
|
+
const PAIRING_TIMEOUT_MS = 30 * 60_000;
|
|
11
11
|
export const DEFAULT_PAIRING_WEB_BASE = "https://stratabook.app";
|
|
12
|
+
/** Local success page: close the helper tab when allowed, otherwise return to Strata. */
|
|
13
|
+
export function pairingCompletionDocument(returnUrl) {
|
|
14
|
+
const destination = JSON.stringify(returnUrl).replaceAll("<", "\\u003c");
|
|
15
|
+
return `<!doctype html>
|
|
16
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
17
|
+
<title>Agent connected · Strata</title><style>
|
|
18
|
+
:root{color-scheme:dark}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#090b10;color:#f4f7fa;font:16px/1.5 system-ui,sans-serif}.card{max-width:34rem;margin:2rem;padding:2rem;border:1px solid #1b8596;border-radius:18px;background:linear-gradient(135deg,#0c171d,#100d1e)}h1{margin:.25rem 0 .5rem;font-size:1.7rem}p{margin:0;color:#aeb8c5}.mark{color:#34d6c4;font-weight:700;letter-spacing:.08em;text-transform:uppercase;font-size:.75rem}
|
|
19
|
+
</style></head><body><main class="card"><div class="mark">Strata</div><h1>Agent connected</h1><p>Your local credential is saved. You can close this tab.</p></main>
|
|
20
|
+
<script>const destination=${destination};window.close();setTimeout(()=>location.replace(destination),700);</script></body></html>`;
|
|
21
|
+
}
|
|
12
22
|
function validPublicKey(value) {
|
|
13
23
|
return typeof value === "string" && PUBLIC_KEY_PATTERN.test(value);
|
|
14
24
|
}
|
|
@@ -60,13 +70,23 @@ export async function readTradingConnection(path = tradingCredentialsPath()) {
|
|
|
60
70
|
}
|
|
61
71
|
export async function writeTradingConnection(connection, path = tradingCredentialsPath()) {
|
|
62
72
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
74
|
+
try {
|
|
75
|
+
await writeFile(temporary, `${JSON.stringify(connection, null, 2)}\n`, {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
mode: 0o600,
|
|
78
|
+
});
|
|
79
|
+
// `mode` is honored on creation. chmod also protects unusual umasks.
|
|
80
|
+
if (process.platform !== "win32")
|
|
81
|
+
await chmod(temporary, 0o600);
|
|
82
|
+
// Same-directory rename means the live MCP sees either the old complete
|
|
83
|
+
// credential or the new complete credential, never a partially written file.
|
|
84
|
+
await rename(temporary, path);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await unlink(temporary).catch(() => undefined);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
70
90
|
}
|
|
71
91
|
export async function removeTradingConnection(path = tradingCredentialsPath()) {
|
|
72
92
|
try {
|
|
@@ -198,13 +218,17 @@ async function waitForPairingCallback(sessionPublicKey, state, returnUrl, onComp
|
|
|
198
218
|
finished = true;
|
|
199
219
|
try {
|
|
200
220
|
await onComplete(ownerWallet);
|
|
201
|
-
|
|
202
|
-
|
|
221
|
+
const document = pairingCompletionDocument(returnUrl);
|
|
222
|
+
response.writeHead(200, {
|
|
223
|
+
"content-type": "text/html; charset=utf-8",
|
|
224
|
+
"content-length": Buffer.byteLength(document),
|
|
203
225
|
"cache-control": "no-store",
|
|
226
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
227
|
+
"referrer-policy": "no-referrer",
|
|
204
228
|
"x-content-type-options": "nosniff",
|
|
205
229
|
"x-frame-options": "DENY",
|
|
206
230
|
});
|
|
207
|
-
response.end();
|
|
231
|
+
response.end(document);
|
|
208
232
|
settle(ownerWallet);
|
|
209
233
|
}
|
|
210
234
|
catch (error) {
|
|
@@ -234,7 +258,7 @@ async function waitForPairingCallback(sessionPublicKey, state, returnUrl, onComp
|
|
|
234
258
|
return;
|
|
235
259
|
finished = true;
|
|
236
260
|
server.close();
|
|
237
|
-
reject(new Error("Strata pairing timed out after
|
|
261
|
+
reject(new Error("Strata pairing timed out after 30 minutes"));
|
|
238
262
|
}, PAIRING_TIMEOUT_MS);
|
|
239
263
|
timeout.unref();
|
|
240
264
|
completion.finally(() => clearTimeout(timeout)).catch(() => undefined);
|
|
@@ -295,6 +319,6 @@ export async function runLocalPairing(options) {
|
|
|
295
319
|
process.stdout.write("Waiting for the owner-wallet signature…\n");
|
|
296
320
|
const ownerWallet = await callback.completion;
|
|
297
321
|
process.stdout.write(options.action === "connect"
|
|
298
|
-
? `✓ Trading connected for ${ownerWallet}. Credentials saved privately at ${path}.\
|
|
322
|
+
? `✓ Trading connected for ${ownerWallet}. Credentials saved privately at ${path}.\nThis MCP release and newer pick it up automatically; restart only older clients.\n`
|
|
299
323
|
: `✓ Session revoked for ${ownerWallet}. Local trading credentials removed.\n`);
|
|
300
324
|
}
|
package/dist/src/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StrataClient, StrataPlatformClient, type CapabilityCatalog } from "@stratabook/sdk";
|
|
3
|
-
import { type SessionAutonomy } from "./autonomy.js";
|
|
3
|
+
import { type SessionAutonomy, type SessionAutonomyProvider } from "./autonomy.js";
|
|
4
4
|
import { type StrataMcpToolMode } from "./usability.js";
|
|
5
5
|
export interface StrataMcpOptions {
|
|
6
6
|
apiBase?: string;
|
|
@@ -15,6 +15,21 @@ export interface StrataMcpOptions {
|
|
|
15
15
|
* every trade is prepared for a human to sign. Built from env in the CLI.
|
|
16
16
|
*/
|
|
17
17
|
sessionAutonomy?: SessionAutonomy;
|
|
18
|
+
/**
|
|
19
|
+
* Live source used by long-running hosts. When present it is consulted for
|
|
20
|
+
* every trading call, allowing `strata-mcp connect` to rotate the private
|
|
21
|
+
* credential file without restarting the MCP process.
|
|
22
|
+
*/
|
|
23
|
+
sessionAutonomyProvider?: SessionAutonomyProvider;
|
|
24
|
+
/** Public-only credential metadata for the runtime health handshake. */
|
|
25
|
+
sessionCredentialDiagnostics?: () => Promise<SessionCredentialDiagnostics>;
|
|
26
|
+
/** False for stateless HTTP runtimes that cannot heartbeat a durable guard. */
|
|
27
|
+
persistentOrderGuards?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SessionCredentialDiagnostics {
|
|
30
|
+
readonly source: "environment" | "file" | "none";
|
|
31
|
+
readonly credentials_file: string | null;
|
|
32
|
+
readonly credential_file_session_public_key: string | null;
|
|
18
33
|
}
|
|
19
34
|
export interface StrataMcpRuntime {
|
|
20
35
|
server: McpServer;
|
package/dist/src/server.js
CHANGED
|
@@ -369,6 +369,315 @@ function orderOperationFromArgs(args) {
|
|
|
369
369
|
}
|
|
370
370
|
const PLATFORM_MARKET_PAGE_LIMIT = 100;
|
|
371
371
|
const PLATFORM_MARKET_MAX_PAGES = 20;
|
|
372
|
+
/**
|
|
373
|
+
* Keeps authenticated order channels warm so a durable dead-man ticket can be
|
|
374
|
+
* heartbeated after the one MCP call returns. A session rotation closes every
|
|
375
|
+
* old channel; its already-armed ticket then fails closed instead of leaving
|
|
376
|
+
* orders unmanaged.
|
|
377
|
+
*/
|
|
378
|
+
class WarmOrderConnectionPool {
|
|
379
|
+
platformClient;
|
|
380
|
+
connections = new Map();
|
|
381
|
+
sessionBinding = null;
|
|
382
|
+
constructor(platformClient) {
|
|
383
|
+
this.platformClient = platformClient;
|
|
384
|
+
}
|
|
385
|
+
reconcile(autonomy) {
|
|
386
|
+
const binding = autonomy === null
|
|
387
|
+
? null
|
|
388
|
+
: `${autonomy.ownerWallet}:${autonomy.signer.publicKey}`;
|
|
389
|
+
if (binding === this.sessionBinding)
|
|
390
|
+
return;
|
|
391
|
+
this.close();
|
|
392
|
+
this.sessionBinding = binding;
|
|
393
|
+
}
|
|
394
|
+
async get(marketId, autonomy) {
|
|
395
|
+
this.reconcile(autonomy);
|
|
396
|
+
let pending = this.connections.get(marketId);
|
|
397
|
+
if (!pending) {
|
|
398
|
+
pending = this.platformClient.orders.connect(marketId, autonomy.ownerWallet, autonomy.signer, {
|
|
399
|
+
onError: (error) => {
|
|
400
|
+
process.stderr.write(`[strata-mcp] guarded order channel failed for ${marketId}: ${safeMessage(error)}\n`);
|
|
401
|
+
},
|
|
402
|
+
}).then(async (connection) => {
|
|
403
|
+
await connection.ready;
|
|
404
|
+
return connection;
|
|
405
|
+
});
|
|
406
|
+
this.connections.set(marketId, pending);
|
|
407
|
+
pending.catch(() => {
|
|
408
|
+
if (this.connections.get(marketId) === pending)
|
|
409
|
+
this.connections.delete(marketId);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return pending;
|
|
413
|
+
}
|
|
414
|
+
close() {
|
|
415
|
+
for (const pending of this.connections.values()) {
|
|
416
|
+
pending.then((connection) => connection.close()).catch(() => undefined);
|
|
417
|
+
}
|
|
418
|
+
this.connections.clear();
|
|
419
|
+
this.sessionBinding = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/** Resolve a human limit-order instruction from one coherent public snapshot. */
|
|
423
|
+
async function resolveFriendlyOrderIntent(platformClient, autonomy, args, nowMs) {
|
|
424
|
+
const percentageHundredths = Math.round(args.availablePercent * 100);
|
|
425
|
+
if (!Number.isFinite(args.availablePercent)
|
|
426
|
+
|| args.availablePercent <= 0
|
|
427
|
+
|| args.availablePercent > 100
|
|
428
|
+
|| Math.abs(args.availablePercent * 100 - percentageHundredths) > 1e-8) {
|
|
429
|
+
throw new TypeError("availablePercent must be greater than 0 and at most 100, with at most two decimals");
|
|
430
|
+
}
|
|
431
|
+
if (!Number.isSafeInteger(args.markOffsetBps) || args.markOffsetBps <= -10_000) {
|
|
432
|
+
throw new TypeError("markOffsetBps must be an integer greater than -10000");
|
|
433
|
+
}
|
|
434
|
+
const market = await platformClient.markets.resolve(args.market);
|
|
435
|
+
if (market.status !== "active" || !market.available_actions.includes("place_order")) {
|
|
436
|
+
throw new Error(`${market.label} is not currently accepting resting orders`);
|
|
437
|
+
}
|
|
438
|
+
const [mark, status, fees, portfolio, assetsPage] = await Promise.all([
|
|
439
|
+
platformClient.marketData.mark(market.market_id),
|
|
440
|
+
platformClient.books.status(market.market_id),
|
|
441
|
+
platformClient.books.fees(market.market_id),
|
|
442
|
+
platformClient.account.portfolio(autonomy.ownerWallet),
|
|
443
|
+
platformClient.assets.list({ limit: 200 }),
|
|
444
|
+
]);
|
|
445
|
+
if (mark.stale || mark.price_atoms_per_base_unit === null) {
|
|
446
|
+
throw new Error(`${market.label} does not have a fresh mark`);
|
|
447
|
+
}
|
|
448
|
+
const baseAsset = assetsPage.assets.find((asset) => asset.asset_id === market.base_asset_id)
|
|
449
|
+
?? await platformClient.assets.resolve(market.base_asset_id);
|
|
450
|
+
const quoteAsset = assetsPage.assets.find((asset) => asset.asset_id === market.quote_asset_id)
|
|
451
|
+
?? await platformClient.assets.resolve(market.quote_asset_id);
|
|
452
|
+
if (mark.quote_decimals !== quoteAsset.decimals) {
|
|
453
|
+
throw new Error(`${market.label} mark decimals do not match its quote asset`);
|
|
454
|
+
}
|
|
455
|
+
const tick = BigInt(status.tick_size_atoms);
|
|
456
|
+
const markAtoms = BigInt(mark.price_atoms_per_base_unit);
|
|
457
|
+
if (tick <= 0n || markAtoms <= 0n)
|
|
458
|
+
throw new Error(`${market.label} returned invalid price metadata`);
|
|
459
|
+
const factor = BigInt(10_000 + args.markOffsetBps);
|
|
460
|
+
const priceDenominator = 10000n * tick;
|
|
461
|
+
const priceNumerator = markAtoms * factor;
|
|
462
|
+
const priceTicks = args.side === "sell"
|
|
463
|
+
? (priceNumerator + priceDenominator - 1n) / priceDenominator
|
|
464
|
+
: priceNumerator / priceDenominator;
|
|
465
|
+
const limitPriceAtoms = priceTicks * tick;
|
|
466
|
+
if (limitPriceAtoms <= 0n)
|
|
467
|
+
throw new Error("the resolved limit price is below one tick");
|
|
468
|
+
const inputAssetId = args.side === "sell" ? market.base_asset_id : market.quote_asset_id;
|
|
469
|
+
const inputBalance = portfolio.balances.find((balance) => balance.asset_id === inputAssetId);
|
|
470
|
+
if (!inputBalance)
|
|
471
|
+
throw new Error(`no ${args.side === "sell" ? baseAsset.symbol : quoteAsset.symbol} balance is available`);
|
|
472
|
+
const availableAtoms = BigInt(inputBalance.available_atoms);
|
|
473
|
+
const budgetAtoms = availableAtoms * BigInt(percentageHundredths) / 10000n;
|
|
474
|
+
const baseUnit = 10n ** BigInt(baseAsset.decimals);
|
|
475
|
+
const positiveMakerFeeBps = BigInt(Math.max(0, fees.passive_maker_fee_bps));
|
|
476
|
+
const sizeAtoms = args.side === "sell"
|
|
477
|
+
? budgetAtoms
|
|
478
|
+
: budgetAtoms * baseUnit * 10000n
|
|
479
|
+
/ (limitPriceAtoms * (10000n + positiveMakerFeeBps));
|
|
480
|
+
if (sizeAtoms <= 0n)
|
|
481
|
+
throw new Error("the requested percentage resolves below the minimum order size");
|
|
482
|
+
if (sizeAtoms < BigInt(status.minimum_order_size_atoms)) {
|
|
483
|
+
throw new Error(`the resolved size is below ${status.minimum_order_size_atoms} base atoms`);
|
|
484
|
+
}
|
|
485
|
+
// Autonomy ceilings are risk limits, so value the order at the less
|
|
486
|
+
// favourable of its live mark and limit instead of allowing an aggressive
|
|
487
|
+
// offset to understate the amount the session may spend.
|
|
488
|
+
const riskPriceAtoms = markAtoms > limitPriceAtoms ? markAtoms : limitPriceAtoms;
|
|
489
|
+
const notionalQuoteAtoms = sizeAtoms * riskPriceAtoms / baseUnit;
|
|
490
|
+
const notionalUsd = Number(notionalQuoteAtoms) / 10 ** quoteAsset.decimals;
|
|
491
|
+
if (!Number.isFinite(notionalUsd) || notionalUsd <= 0) {
|
|
492
|
+
throw new Error("the resolved order notional is invalid");
|
|
493
|
+
}
|
|
494
|
+
const clientOrderId = args.clientOrderId
|
|
495
|
+
?? `mcp-${nowMs().toString(36)}-${autonomy.signer.publicKey.slice(0, 8)}`;
|
|
496
|
+
return {
|
|
497
|
+
marketId: market.market_id,
|
|
498
|
+
clientOrderId,
|
|
499
|
+
side: args.side,
|
|
500
|
+
orderType: args.orderType ?? "post_only",
|
|
501
|
+
limitPriceAtoms: limitPriceAtoms.toString(),
|
|
502
|
+
sizeAtoms: sizeAtoms.toString(),
|
|
503
|
+
notionalUsd,
|
|
504
|
+
resolution: {
|
|
505
|
+
market: market.label,
|
|
506
|
+
market_id: market.market_id,
|
|
507
|
+
client_order_id: clientOrderId,
|
|
508
|
+
side: args.side,
|
|
509
|
+
available_percent: args.availablePercent,
|
|
510
|
+
input_asset: args.side === "sell" ? baseAsset.symbol : quoteAsset.symbol,
|
|
511
|
+
input_available_atoms: availableAtoms.toString(),
|
|
512
|
+
input_budget_atoms: budgetAtoms.toString(),
|
|
513
|
+
size_atoms: sizeAtoms.toString(),
|
|
514
|
+
size_display: `${formatAtoms(sizeAtoms.toString(), baseAsset.decimals)} ${baseAsset.symbol}`,
|
|
515
|
+
mark_price_atoms: markAtoms.toString(),
|
|
516
|
+
mark_offset_bps: args.markOffsetBps,
|
|
517
|
+
limit_price_atoms: limitPriceAtoms.toString(),
|
|
518
|
+
risk_price_atoms: riskPriceAtoms.toString(),
|
|
519
|
+
risk_notional_usd: notionalUsd,
|
|
520
|
+
tick_size_atoms: tick.toString(),
|
|
521
|
+
portfolio_observed_slot: portfolio.observed_slot,
|
|
522
|
+
portfolio_observed_at_ms: portfolio.observed_at_ms,
|
|
523
|
+
mark_server_time_ms: mark.server_time_ms,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
async function waitForOrderSubmission(connection, orderControlId, idempotencyKey) {
|
|
528
|
+
let status = await connection.status(orderControlId, idempotencyKey);
|
|
529
|
+
for (let attempt = 0; status.status === "submitting" && attempt < 12; attempt += 1) {
|
|
530
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
531
|
+
status = await connection.status(orderControlId, idempotencyKey);
|
|
532
|
+
}
|
|
533
|
+
return status;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Existing protocol cannot pre-sign a cancel for an order that does not exist
|
|
537
|
+
* yet. Close that gap operationally: confirm the placement, arm immediately on
|
|
538
|
+
* the same warm socket, and cancel the new order if arming cannot be proven.
|
|
539
|
+
*/
|
|
540
|
+
async function executeGuardedOrder(connection, autonomy, operation, idempotencyKey, deadManTimeoutMs) {
|
|
541
|
+
const receipt = await connection.execute({ operation, idempotencyKey });
|
|
542
|
+
let status = null;
|
|
543
|
+
let statusError = null;
|
|
544
|
+
try {
|
|
545
|
+
status = await waitForOrderSubmission(connection, receipt.order_control_id, idempotencyKey);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
// The order may already be live. Continue to the guard instead of leaving
|
|
549
|
+
// it unmanaged merely because the confirmation read was throttled.
|
|
550
|
+
statusError = safeMessage(error);
|
|
551
|
+
}
|
|
552
|
+
if (status?.status === "failed") {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
code: "order_submission_failed",
|
|
556
|
+
message: `Order submission failed: ${status.failure_code ?? "unknown"}.`,
|
|
557
|
+
details: { receipt, status },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
let guardError;
|
|
561
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
562
|
+
try {
|
|
563
|
+
const deadMan = await connection.armDeadMan({
|
|
564
|
+
timeoutMs: deadManTimeoutMs,
|
|
565
|
+
idempotencyKey: `guard-${receipt.order_control_id}`,
|
|
566
|
+
});
|
|
567
|
+
return { ok: true, receipt, status, statusError, deadMan };
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
guardError = error;
|
|
571
|
+
if (attempt === 0) {
|
|
572
|
+
const delay = error instanceof StrataApiError ? error.retryAfterMs ?? 100 : 100;
|
|
573
|
+
// No guard exists yet. A long provider retry window must not leave the
|
|
574
|
+
// freshly placed order exposed while we wait; cancel it immediately.
|
|
575
|
+
if (delay > 250)
|
|
576
|
+
break;
|
|
577
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const orderId = receipt.order_ids[0];
|
|
582
|
+
if (orderId) {
|
|
583
|
+
try {
|
|
584
|
+
const cancellation = await connection.execute({
|
|
585
|
+
operation: { action: "cancel", ownerWallet: autonomy.ownerWallet, orderId },
|
|
586
|
+
idempotencyKey: `guard-failed-cancel-${receipt.order_control_id}`,
|
|
587
|
+
});
|
|
588
|
+
let cancellationStatus;
|
|
589
|
+
try {
|
|
590
|
+
cancellationStatus = await waitForOrderSubmission(connection, cancellation.order_control_id, `guard-failed-cancel-${receipt.order_control_id}`);
|
|
591
|
+
}
|
|
592
|
+
catch (cancelStatusError) {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
code: "dead_man_setup_ambiguous",
|
|
596
|
+
message: "The order guard failed and Strata submitted a cancellation, but could not prove that cancellation completed.",
|
|
597
|
+
details: {
|
|
598
|
+
receipt,
|
|
599
|
+
status,
|
|
600
|
+
status_error: statusError,
|
|
601
|
+
cancellation,
|
|
602
|
+
guard_error: safeMessage(guardError),
|
|
603
|
+
cancellation_status_error: safeMessage(cancelStatusError),
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
if (cancellationStatus.status !== "submitted") {
|
|
608
|
+
return {
|
|
609
|
+
ok: false,
|
|
610
|
+
code: "dead_man_setup_ambiguous",
|
|
611
|
+
message: "The order guard failed and its fail-closed cancellation was not confirmed.",
|
|
612
|
+
details: { receipt, status, status_error: statusError, cancellation, cancellationStatus },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
ok: false,
|
|
617
|
+
code: "dead_man_setup_failed",
|
|
618
|
+
message: "The order was submitted but its dead-man guard could not be proven, so Strata cancelled it fail-closed.",
|
|
619
|
+
details: {
|
|
620
|
+
receipt,
|
|
621
|
+
status,
|
|
622
|
+
status_error: statusError,
|
|
623
|
+
cancellation,
|
|
624
|
+
cancellation_status: cancellationStatus,
|
|
625
|
+
guard_error: safeMessage(guardError),
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
catch (cancelError) {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
code: "dead_man_setup_ambiguous",
|
|
633
|
+
message: "The order was submitted, but neither its dead-man guard nor fail-closed cancellation could be proven.",
|
|
634
|
+
details: {
|
|
635
|
+
receipt,
|
|
636
|
+
status,
|
|
637
|
+
status_error: statusError,
|
|
638
|
+
guard_error: safeMessage(guardError),
|
|
639
|
+
cancellation_error: safeMessage(cancelError),
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
ok: false,
|
|
646
|
+
code: "dead_man_setup_ambiguous",
|
|
647
|
+
message: "The order was submitted without a returned order ID, so its guard could not be proven.",
|
|
648
|
+
details: { receipt, status, status_error: statusError, guard_error: safeMessage(guardError) },
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async function observePlacedOrderLifecycle(platformClient, ownerWallet, orderIds) {
|
|
652
|
+
try {
|
|
653
|
+
const portfolio = await platformClient.account.portfolio(ownerWallet);
|
|
654
|
+
const orders = orderIds.map((orderId) => {
|
|
655
|
+
const visible = portfolio.open_orders.find((order) => order.order_id === orderId);
|
|
656
|
+
return { order_id: orderId, state: visible?.state ?? "pending_visibility" };
|
|
657
|
+
});
|
|
658
|
+
const state = orders.length > 0 && orders.every((order) => order.state === "open" || order.state === "partially_filled")
|
|
659
|
+
? orders.every((order) => order.state === "open") ? "confirmed_open" : "partially_filled"
|
|
660
|
+
: "confirmed_pending_visibility";
|
|
661
|
+
return {
|
|
662
|
+
state,
|
|
663
|
+
orders,
|
|
664
|
+
observed_at_ms: portfolio.observed_at_ms,
|
|
665
|
+
observed_slot: portfolio.observed_slot,
|
|
666
|
+
unavailable_market_ids: portfolio.unavailable_market_ids,
|
|
667
|
+
error: null,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
return {
|
|
672
|
+
state: "confirmed_pending_visibility",
|
|
673
|
+
orders: orderIds.map((orderId) => ({ order_id: orderId, state: "pending_visibility" })),
|
|
674
|
+
observed_at_ms: null,
|
|
675
|
+
observed_slot: null,
|
|
676
|
+
unavailable_market_ids: [],
|
|
677
|
+
error: safeMessage(error),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
372
681
|
/**
|
|
373
682
|
* Every live platform market keyed by label, so the tool can hand agents the
|
|
374
683
|
* opaque `market_id` (and asset ids) that every by-market tool takes. A
|
|
@@ -404,6 +713,8 @@ async function platformMarketIdentities(platformClient) {
|
|
|
404
713
|
export async function createStrataMcpServer(options = {}) {
|
|
405
714
|
const client = strataClient(options);
|
|
406
715
|
const toolMode = options.toolMode ?? "simple";
|
|
716
|
+
const autonomyForCall = options.sessionAutonomyProvider
|
|
717
|
+
?? (async () => options.sessionAutonomy ?? null);
|
|
407
718
|
const platformClient = options.platformClient ?? new StrataPlatformClient({
|
|
408
719
|
apiBase: options.apiBase,
|
|
409
720
|
timeoutMs: options.timeoutMs,
|
|
@@ -939,7 +1250,7 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
939
1250
|
.max(4)
|
|
940
1251
|
.optional(),
|
|
941
1252
|
expiresAtMs: z.number().int().positive().optional(),
|
|
942
|
-
minimumIntervalSeconds: z.number().int().min(
|
|
1253
|
+
minimumIntervalSeconds: z.number().int().min(0).max(86_400).optional(),
|
|
943
1254
|
maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
|
|
944
1255
|
},
|
|
945
1256
|
annotations: {
|
|
@@ -1444,7 +1755,8 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1444
1755
|
amountInAtoms: resolvedAtoms,
|
|
1445
1756
|
maximumToleranceBps,
|
|
1446
1757
|
}));
|
|
1447
|
-
|
|
1758
|
+
const autonomy = await autonomyForCall();
|
|
1759
|
+
if (!autonomy) {
|
|
1448
1760
|
return toolResult({
|
|
1449
1761
|
executed: false,
|
|
1450
1762
|
reason: "trading_not_connected",
|
|
@@ -1464,18 +1776,18 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1464
1776
|
: null;
|
|
1465
1777
|
const resolver = new MarketMetaResolver(platformClient, async () => sonarMarkets, () => Date.now());
|
|
1466
1778
|
const marketId = sonar ? await resolver.idForLabel(sonar.label) : null;
|
|
1467
|
-
const decision = decideAutonomy(
|
|
1779
|
+
const decision = decideAutonomy(autonomy, marketId ?? "", notional, Date.now());
|
|
1468
1780
|
if (!decision.allow) {
|
|
1469
1781
|
return toolResult({ executed: false, reason: decision.reason, quote: freshQuote }, `${decision.reason} The live quote is attached; no transaction was sent.`);
|
|
1470
1782
|
}
|
|
1471
1783
|
const receipt = await client.executeQuote({
|
|
1472
1784
|
quote: freshQuote,
|
|
1473
|
-
ownerWallet:
|
|
1474
|
-
signer:
|
|
1785
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1786
|
+
signer: autonomy.signer,
|
|
1475
1787
|
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
1476
1788
|
});
|
|
1477
1789
|
if (notional !== null)
|
|
1478
|
-
|
|
1790
|
+
autonomy.dailyBudget.record(notional, Date.now());
|
|
1479
1791
|
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${side} ${inputDisplay} as ${receipt.signature}.`);
|
|
1480
1792
|
}));
|
|
1481
1793
|
registerTool("strata_swap_quote", {
|
|
@@ -2135,7 +2447,8 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
2135
2447
|
});
|
|
2136
2448
|
return toolResult(response, `Submitted IntentBook control as ${response.signature}.`);
|
|
2137
2449
|
}));
|
|
2138
|
-
|
|
2450
|
+
const orderConnections = new WarmOrderConnectionPool(platformClient);
|
|
2451
|
+
registerAutonomyTools(registerTool, client, platformClient, autonomyForCall, orderConnections, toolMode, options.persistentOrderGuards ?? true, options.sessionCredentialDiagnostics, () => typeof Date !== "undefined" ? Date.now() : 0);
|
|
2139
2452
|
void [
|
|
2140
2453
|
markets,
|
|
2141
2454
|
quote,
|
|
@@ -2161,10 +2474,12 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
2161
2474
|
const refresh = async () => {
|
|
2162
2475
|
if (closed)
|
|
2163
2476
|
return;
|
|
2164
|
-
const [catalog, platformCatalog] = await Promise.all([
|
|
2477
|
+
const [catalog, platformCatalog, autonomy] = await Promise.all([
|
|
2165
2478
|
client.capabilities(),
|
|
2166
2479
|
platformClient.discovery.read(),
|
|
2480
|
+
autonomyForCall(),
|
|
2167
2481
|
]);
|
|
2482
|
+
orderConnections.reconcile(autonomy);
|
|
2168
2483
|
applyToolAvailability(registeredTools, catalog, platformCatalog, toolMode);
|
|
2169
2484
|
};
|
|
2170
2485
|
const timer = setInterval(() => {
|
|
@@ -2179,11 +2494,12 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
2179
2494
|
close: async () => {
|
|
2180
2495
|
closed = true;
|
|
2181
2496
|
clearInterval(timer);
|
|
2497
|
+
orderConnections.close();
|
|
2182
2498
|
await server.close();
|
|
2183
2499
|
},
|
|
2184
2500
|
};
|
|
2185
2501
|
}
|
|
2186
|
-
function registerAutonomyTools(registerTool, client, platformClient,
|
|
2502
|
+
function registerAutonomyTools(registerTool, client, platformClient, autonomyProvider, orderConnections, toolMode, persistentOrderGuards, credentialDiagnostics, nowMs) {
|
|
2187
2503
|
// Always present, always read-only: the agent may show the slider and offer
|
|
2188
2504
|
// to change it, but nothing it calls can raise its own autonomy.
|
|
2189
2505
|
registerTool("strata_autonomy", {
|
|
@@ -2199,8 +2515,16 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2199
2515
|
openWorldHint: false,
|
|
2200
2516
|
},
|
|
2201
2517
|
}, async () => {
|
|
2518
|
+
let autonomy = null;
|
|
2519
|
+
let runtimeError = null;
|
|
2520
|
+
try {
|
|
2521
|
+
autonomy = await autonomyProvider();
|
|
2522
|
+
}
|
|
2523
|
+
catch (error) {
|
|
2524
|
+
runtimeError = safeMessage(error);
|
|
2525
|
+
}
|
|
2202
2526
|
const howToChange = {
|
|
2203
|
-
setup: "
|
|
2527
|
+
setup: "Run `npx -y @stratabook/mcp connect`; it opens the Agents page, registers the locally generated public key, and the running MCP hot-loads the private credential.",
|
|
2204
2528
|
agents_page: "https://stratabook.app/agents",
|
|
2205
2529
|
generic_clients: "Claude Desktop, Cursor, Windsurf, Codex, or any local stdio MCP host",
|
|
2206
2530
|
note: "Read-only tools need none of this. Only the user changes trading authority; an agent can never raise its own level.",
|
|
@@ -2212,12 +2536,61 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2212
2536
|
session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
|
|
2213
2537
|
},
|
|
2214
2538
|
};
|
|
2539
|
+
let credential = {
|
|
2540
|
+
source: "none",
|
|
2541
|
+
credentials_file: null,
|
|
2542
|
+
credential_file_session_public_key: null,
|
|
2543
|
+
};
|
|
2544
|
+
try {
|
|
2545
|
+
credential = await credentialDiagnostics?.() ?? credential;
|
|
2546
|
+
}
|
|
2547
|
+
catch (error) {
|
|
2548
|
+
runtimeError ??= safeMessage(error);
|
|
2549
|
+
}
|
|
2215
2550
|
if (!autonomy) {
|
|
2216
|
-
return toolResult({
|
|
2217
|
-
|
|
2218
|
-
|
|
2551
|
+
return toolResult({
|
|
2552
|
+
session_configured: false,
|
|
2553
|
+
level: "ask",
|
|
2554
|
+
runtime: {
|
|
2555
|
+
mcp_version: SERVER_VERSION,
|
|
2556
|
+
tool_mode: toolMode,
|
|
2557
|
+
credential_source: credential.source,
|
|
2558
|
+
credentials_file: credential.credentials_file,
|
|
2559
|
+
loaded_session_public_key: null,
|
|
2560
|
+
credential_file_session_public_key: credential.credential_file_session_public_key,
|
|
2561
|
+
session_key_consistent: null,
|
|
2562
|
+
on_chain: null,
|
|
2563
|
+
trading_tools_usable: false,
|
|
2564
|
+
diagnostic_error: runtimeError,
|
|
2565
|
+
credential_reload_supported: true,
|
|
2566
|
+
credential_restart_required: false,
|
|
2567
|
+
package_upgrade_requires_process_restart: true,
|
|
2568
|
+
},
|
|
2569
|
+
how_to_change: howToChange,
|
|
2570
|
+
}, "Read-only is ready. Trading is not connected, so I cannot send transactions. "
|
|
2571
|
+
+ "If you want trading, run `npx -y @stratabook/mcp connect`; never paste the session secret into chat.");
|
|
2219
2572
|
}
|
|
2220
2573
|
const { config } = autonomy;
|
|
2574
|
+
let onChain = null;
|
|
2575
|
+
if (platformClient.vault?.status) {
|
|
2576
|
+
try {
|
|
2577
|
+
const status = await platformClient.vault.status({
|
|
2578
|
+
walletAddress: autonomy.ownerWallet,
|
|
2579
|
+
sessionPublicKey: autonomy.signer.publicKey,
|
|
2580
|
+
});
|
|
2581
|
+
onChain = {
|
|
2582
|
+
vault_state: status.state,
|
|
2583
|
+
session_public_key: status.session?.session_public_key ?? null,
|
|
2584
|
+
session_state: status.session?.state ?? "absent",
|
|
2585
|
+
market_execution_ready: status.session?.market_execution_ready ?? false,
|
|
2586
|
+
expires_at_ms: status.session?.expires_at_ms ?? null,
|
|
2587
|
+
clock_skew_ms: status.server_time_ms - nowMs(),
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
catch (error) {
|
|
2591
|
+
runtimeError ??= safeMessage(error);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2221
2594
|
const spentToday = autonomy.dailyBudget.spentToday(nowMs());
|
|
2222
2595
|
const state = {
|
|
2223
2596
|
session_configured: true,
|
|
@@ -2231,6 +2604,30 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2231
2604
|
? null
|
|
2232
2605
|
: Number(Math.max(0, config.maxUsdPerDay - spentToday).toFixed(2)),
|
|
2233
2606
|
allowed_market_ids: config.allowedMarketIds ?? null,
|
|
2607
|
+
runtime: {
|
|
2608
|
+
mcp_version: SERVER_VERSION,
|
|
2609
|
+
tool_mode: toolMode,
|
|
2610
|
+
credential_source: credential.source,
|
|
2611
|
+
credentials_file: credential.credentials_file,
|
|
2612
|
+
loaded_session_public_key: autonomy.signer.publicKey,
|
|
2613
|
+
credential_file_session_public_key: credential.credential_file_session_public_key,
|
|
2614
|
+
session_key_consistent: credential.source === "file"
|
|
2615
|
+
? credential.credential_file_session_public_key === autonomy.signer.publicKey
|
|
2616
|
+
: credential.source === "environment"
|
|
2617
|
+
? true
|
|
2618
|
+
: null,
|
|
2619
|
+
on_chain: onChain,
|
|
2620
|
+
trading_tools_usable: onChain === null
|
|
2621
|
+
? null
|
|
2622
|
+
: onChain.vault_state === "active"
|
|
2623
|
+
&& onChain.session_state === "active"
|
|
2624
|
+
&& onChain.market_execution_ready === true
|
|
2625
|
+
&& onChain.session_public_key === autonomy.signer.publicKey,
|
|
2626
|
+
diagnostic_error: runtimeError,
|
|
2627
|
+
credential_reload_supported: true,
|
|
2628
|
+
credential_restart_required: false,
|
|
2629
|
+
package_upgrade_requires_process_restart: true,
|
|
2630
|
+
},
|
|
2234
2631
|
how_to_change: howToChange,
|
|
2235
2632
|
};
|
|
2236
2633
|
const summary = config.level === "instant"
|
|
@@ -2241,8 +2638,6 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2241
2638
|
: "Autonomy: ask — I prepare trades but never sign them; you sign each one.";
|
|
2242
2639
|
return toolResult(state, summary);
|
|
2243
2640
|
});
|
|
2244
|
-
if (!autonomy)
|
|
2245
|
-
return;
|
|
2246
2641
|
const resolver = new MarketMetaResolver(platformClient, async () => (await client.markets()).markets, nowMs);
|
|
2247
2642
|
const markFor = async (marketId) => {
|
|
2248
2643
|
const mark = await platformClient.marketData.mark(marketId);
|
|
@@ -2253,6 +2648,7 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2253
2648
|
};
|
|
2254
2649
|
};
|
|
2255
2650
|
const refuse = (reason, prepared, summary) => toolResult({ executed: false, reason, prepared }, summary);
|
|
2651
|
+
const tradingNotConnected = () => toolError("trading_not_connected", "Trading is not connected. Run strata-mcp connect; this running MCP will pick up the private credential automatically.", false);
|
|
2256
2652
|
// ── one-shot existing IntentBook seat control ──────────────────────────────
|
|
2257
2653
|
registerTool("strata_market_making_intent_execute", {
|
|
2258
2654
|
title: "Execute Strata IntentBook control",
|
|
@@ -2274,6 +2670,9 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2274
2670
|
openWorldHint: true,
|
|
2275
2671
|
},
|
|
2276
2672
|
}, async (args) => guardedTool(client, "mm.intent.manage", async () => {
|
|
2673
|
+
const autonomy = await autonomyProvider();
|
|
2674
|
+
if (!autonomy)
|
|
2675
|
+
return tradingNotConnected();
|
|
2277
2676
|
let operation;
|
|
2278
2677
|
if (args.action === "revoke") {
|
|
2279
2678
|
operation = { action: "revoke", ownerWallet: autonomy.ownerWallet };
|
|
@@ -2332,6 +2731,9 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2332
2731
|
openWorldHint: true,
|
|
2333
2732
|
},
|
|
2334
2733
|
}, async (args) => guardedTool(client, "trade.submit", async () => {
|
|
2734
|
+
const autonomy = await autonomyProvider();
|
|
2735
|
+
if (!autonomy)
|
|
2736
|
+
return tradingNotConnected();
|
|
2335
2737
|
const quote = await retryReadOnce(() => client.quote({
|
|
2336
2738
|
market: args.market,
|
|
2337
2739
|
side: args.side,
|
|
@@ -2360,17 +2762,27 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2360
2762
|
// ── one-shot order control (place / cancel / replace / batch) ─────────────
|
|
2361
2763
|
registerTool("strata_order_execute", {
|
|
2362
2764
|
title: "Execute a Strata order control",
|
|
2363
|
-
description: "Place
|
|
2364
|
-
+ "
|
|
2365
|
-
+ "
|
|
2765
|
+
description: "Place a human limit order such as 'sell 10% of available SOL at mark +3%' in one call, or use "
|
|
2766
|
+
+ "the exact-atom cancel/replace/batch controls. Strata resolves the balance, market, mark, decimals "
|
|
2767
|
+
+ "and tick grid internally. Autonomous placements keep a warm dead-man guard; if it cannot be "
|
|
2768
|
+
+ "established, the new order is cancelled fail-closed.",
|
|
2366
2769
|
inputSchema: {
|
|
2367
|
-
|
|
2770
|
+
market: z.string().min(1).max(128).optional()
|
|
2771
|
+
.describe("Friendly market label such as SOL/USDC (also accepts a market ID)."),
|
|
2772
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/).optional()
|
|
2773
|
+
.describe("Advanced alternative to market."),
|
|
2368
2774
|
action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
|
|
2369
2775
|
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
2370
2776
|
side: z.enum(["buy", "sell"]).optional(),
|
|
2371
2777
|
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
2372
2778
|
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
2373
2779
|
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
2780
|
+
availablePercent: z.number().positive().max(100).optional()
|
|
2781
|
+
.describe("Friendly placement size: percentage of available input balance, for example 10."),
|
|
2782
|
+
markOffsetBps: z.number().int().min(-9_999).max(100_000).optional()
|
|
2783
|
+
.describe("Signed offset from the current mark in basis points; +300 is 3% above mark."),
|
|
2784
|
+
deadManTimeoutMs: z.number().int().min(1_000).max(30_000).optional()
|
|
2785
|
+
.describe("Guard timeout for an autonomous placement; friendly orders default to 10000ms."),
|
|
2374
2786
|
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
2375
2787
|
operations: z.array(z.object({
|
|
2376
2788
|
action: z.enum(["place", "cancel", "replace"]),
|
|
@@ -2390,34 +2802,136 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2390
2802
|
openWorldHint: true,
|
|
2391
2803
|
},
|
|
2392
2804
|
}, async (args) => guardedTool(client, "orders.submit", async () => {
|
|
2805
|
+
const autonomy = await autonomyProvider();
|
|
2806
|
+
if (!autonomy)
|
|
2807
|
+
return tradingNotConnected();
|
|
2808
|
+
const friendly = args.availablePercent !== undefined || args.markOffsetBps !== undefined;
|
|
2809
|
+
let resolved = null;
|
|
2810
|
+
let marketId = args.marketId;
|
|
2811
|
+
let clientOrderId = args.clientOrderId;
|
|
2812
|
+
let side = args.side;
|
|
2813
|
+
let orderType = args.orderType;
|
|
2814
|
+
let limitPriceAtoms = args.limitPriceAtoms;
|
|
2815
|
+
let sizeAtoms = args.sizeAtoms;
|
|
2816
|
+
if (friendly) {
|
|
2817
|
+
if (args.action !== "place"
|
|
2818
|
+
|| args.market === undefined
|
|
2819
|
+
|| args.side === undefined
|
|
2820
|
+
|| args.availablePercent === undefined
|
|
2821
|
+
|| args.limitPriceAtoms !== undefined
|
|
2822
|
+
|| args.sizeAtoms !== undefined
|
|
2823
|
+
|| args.marketId !== undefined) {
|
|
2824
|
+
return toolError("invalid_order_intent", "Friendly placement requires action=place, market, side and availablePercent; omit marketId, limitPriceAtoms and sizeAtoms.", false);
|
|
2825
|
+
}
|
|
2826
|
+
try {
|
|
2827
|
+
resolved = await resolveFriendlyOrderIntent(platformClient, autonomy, {
|
|
2828
|
+
market: args.market,
|
|
2829
|
+
side: args.side,
|
|
2830
|
+
availablePercent: args.availablePercent,
|
|
2831
|
+
markOffsetBps: args.markOffsetBps ?? 0,
|
|
2832
|
+
...(args.clientOrderId === undefined ? {} : { clientOrderId: args.clientOrderId }),
|
|
2833
|
+
...(args.orderType === undefined ? {} : { orderType: args.orderType }),
|
|
2834
|
+
}, nowMs);
|
|
2835
|
+
}
|
|
2836
|
+
catch (error) {
|
|
2837
|
+
if (error instanceof StrataApiError)
|
|
2838
|
+
throw error;
|
|
2839
|
+
return toolError("order_intent_unavailable", safeMessage(error), true);
|
|
2840
|
+
}
|
|
2841
|
+
marketId = resolved.marketId;
|
|
2842
|
+
clientOrderId = resolved.clientOrderId;
|
|
2843
|
+
side = resolved.side;
|
|
2844
|
+
orderType = resolved.orderType;
|
|
2845
|
+
limitPriceAtoms = resolved.limitPriceAtoms;
|
|
2846
|
+
sizeAtoms = resolved.sizeAtoms;
|
|
2847
|
+
}
|
|
2848
|
+
else if (marketId === undefined && args.market !== undefined) {
|
|
2849
|
+
try {
|
|
2850
|
+
marketId = (await platformClient.markets.resolve(args.market)).market_id;
|
|
2851
|
+
}
|
|
2852
|
+
catch (error) {
|
|
2853
|
+
if (error instanceof StrataApiError)
|
|
2854
|
+
throw error;
|
|
2855
|
+
return toolError("market_unavailable", safeMessage(error), true);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (marketId === undefined) {
|
|
2859
|
+
return toolError("invalid_request", "Give market or marketId.", false);
|
|
2860
|
+
}
|
|
2393
2861
|
const challenge = orderOperationFromArgs({
|
|
2394
|
-
|
|
2862
|
+
action: args.action,
|
|
2863
|
+
...(clientOrderId === undefined ? {} : { clientOrderId }),
|
|
2864
|
+
...(side === undefined ? {} : { side }),
|
|
2865
|
+
...(orderType === undefined ? {} : { orderType }),
|
|
2866
|
+
...(limitPriceAtoms === undefined ? {} : { limitPriceAtoms }),
|
|
2867
|
+
...(sizeAtoms === undefined ? {} : { sizeAtoms }),
|
|
2868
|
+
...(args.orderId === undefined ? {} : { orderId: args.orderId }),
|
|
2869
|
+
...(args.operations === undefined ? {} : { operations: args.operations }),
|
|
2395
2870
|
ownerWallet: autonomy.ownerWallet,
|
|
2396
2871
|
sessionPublicKey: autonomy.signer.publicKey,
|
|
2397
2872
|
});
|
|
2398
2873
|
if ("content" in challenge)
|
|
2399
2874
|
return challenge;
|
|
2400
2875
|
// A place/replace risks new base; a cancel reduces it (notional 0).
|
|
2401
|
-
const baseAtoms = (args.action === "place" || args.action === "replace") &&
|
|
2402
|
-
? BigInt(
|
|
2876
|
+
const baseAtoms = (args.action === "place" || args.action === "replace") && sizeAtoms !== undefined
|
|
2877
|
+
? BigInt(sizeAtoms)
|
|
2403
2878
|
: 0n;
|
|
2404
|
-
const notional =
|
|
2405
|
-
|
|
2879
|
+
const notional = resolved?.notionalUsd
|
|
2880
|
+
?? await estimateBaseNotionalUsd(resolver, markFor, marketId, baseAtoms);
|
|
2881
|
+
const decision = decideAutonomy(autonomy, marketId, notional, nowMs());
|
|
2406
2882
|
if (!decision.allow) {
|
|
2407
|
-
const prepared = await platformClient.orders.prepare(
|
|
2883
|
+
const prepared = await platformClient.orders.prepare(marketId, {
|
|
2408
2884
|
operation: challenge,
|
|
2409
2885
|
});
|
|
2410
|
-
return
|
|
2886
|
+
return toolResult({
|
|
2887
|
+
executed: false,
|
|
2888
|
+
reason: decision.reason,
|
|
2889
|
+
prepared,
|
|
2890
|
+
...(resolved === null ? {} : { resolved_intent: resolved.resolution }),
|
|
2891
|
+
}, decision.reason);
|
|
2411
2892
|
}
|
|
2412
2893
|
const { sessionPublicKey: _session, ...operation } = challenge;
|
|
2413
|
-
const
|
|
2894
|
+
const idempotencyKey = args.idempotencyKey ?? clientOrderId ?? `mcp-${nowMs().toString(36)}`;
|
|
2895
|
+
if (args.action === "place" && (friendly || args.deadManTimeoutMs !== undefined)) {
|
|
2896
|
+
if (!persistentOrderGuards) {
|
|
2897
|
+
return toolError("persistent_guard_unavailable", "Autonomous resting orders require a persistent stdio MCP runtime so dead-man heartbeats continue after this call.", false);
|
|
2898
|
+
}
|
|
2899
|
+
const connection = await orderConnections.get(marketId, autonomy);
|
|
2900
|
+
const guarded = await executeGuardedOrder(connection, autonomy, operation, idempotencyKey, args.deadManTimeoutMs ?? 10_000);
|
|
2901
|
+
if (!guarded.ok) {
|
|
2902
|
+
return toolError(guarded.code, guarded.message, true, undefined, guarded.details);
|
|
2903
|
+
}
|
|
2904
|
+
const lifecycle = await observePlacedOrderLifecycle(platformClient, autonomy.ownerWallet, guarded.receipt.order_ids);
|
|
2905
|
+
if (notional !== null)
|
|
2906
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
2907
|
+
return toolResult({
|
|
2908
|
+
executed: true,
|
|
2909
|
+
submission_state: guarded.status?.status === "submitted"
|
|
2910
|
+
? "confirmed"
|
|
2911
|
+
: guarded.status?.status ?? "submitted_unverified",
|
|
2912
|
+
order_state: lifecycle.state,
|
|
2913
|
+
receipt: guarded.receipt,
|
|
2914
|
+
order_status: guarded.status,
|
|
2915
|
+
order_status_error: guarded.statusError,
|
|
2916
|
+
lifecycle,
|
|
2917
|
+
dead_man: guarded.deadMan,
|
|
2918
|
+
notional_usd: notional,
|
|
2919
|
+
...(resolved === null ? {} : { resolved_intent: resolved.resolution }),
|
|
2920
|
+
}, `Placed guarded ${side} order ${guarded.receipt.order_ids.join(", ")} as ${guarded.receipt.signature}.`);
|
|
2921
|
+
}
|
|
2922
|
+
const receipt = await platformClient.orders.execute(marketId, {
|
|
2414
2923
|
operation: operation,
|
|
2415
2924
|
signer: autonomy.signer,
|
|
2416
|
-
|
|
2925
|
+
idempotencyKey,
|
|
2417
2926
|
});
|
|
2418
2927
|
if (notional !== null)
|
|
2419
2928
|
autonomy.dailyBudget.record(notional, nowMs());
|
|
2420
|
-
return toolResult({
|
|
2929
|
+
return toolResult({
|
|
2930
|
+
executed: true,
|
|
2931
|
+
receipt,
|
|
2932
|
+
notional_usd: notional,
|
|
2933
|
+
...(resolved === null ? {} : { resolved_intent: resolved.resolution }),
|
|
2934
|
+
}, `Executed ${receipt.action} control ${receipt.order_control_id} as ${receipt.signature}.`);
|
|
2421
2935
|
}));
|
|
2422
2936
|
// ── one-shot TWAP (schedule / cancel) ─────────────────────────────────────
|
|
2423
2937
|
registerTool("strata_twap_execute", {
|
|
@@ -2444,6 +2958,9 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2444
2958
|
openWorldHint: true,
|
|
2445
2959
|
},
|
|
2446
2960
|
}, async (args) => safeTool(async () => {
|
|
2961
|
+
const autonomy = await autonomyProvider();
|
|
2962
|
+
if (!autonomy)
|
|
2963
|
+
return tradingNotConnected();
|
|
2447
2964
|
let operation;
|
|
2448
2965
|
if (args.action === "cancel") {
|
|
2449
2966
|
if (args.twapId === undefined)
|
|
@@ -2540,7 +3057,7 @@ async function guardedTool(client, capabilityId, operation) {
|
|
|
2540
3057
|
}
|
|
2541
3058
|
catch (error) {
|
|
2542
3059
|
if (error instanceof StrataApiError) {
|
|
2543
|
-
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable);
|
|
3060
|
+
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable, error.retryAfterMs);
|
|
2544
3061
|
}
|
|
2545
3062
|
return toolError("request_failed", safeMessage(error), true);
|
|
2546
3063
|
}
|
|
@@ -2551,7 +3068,7 @@ async function safeTool(operation) {
|
|
|
2551
3068
|
}
|
|
2552
3069
|
catch (error) {
|
|
2553
3070
|
if (error instanceof StrataApiError) {
|
|
2554
|
-
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable);
|
|
3071
|
+
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable, error.retryAfterMs);
|
|
2555
3072
|
}
|
|
2556
3073
|
return toolError("request_failed", safeMessage(error), true);
|
|
2557
3074
|
}
|
|
@@ -2564,7 +3081,7 @@ async function retryReadOnce(operation) {
|
|
|
2564
3081
|
catch (error) {
|
|
2565
3082
|
if (!(error instanceof StrataApiError) || !error.retryable)
|
|
2566
3083
|
throw error;
|
|
2567
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
3084
|
+
await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 150));
|
|
2568
3085
|
return operation();
|
|
2569
3086
|
}
|
|
2570
3087
|
}
|
|
@@ -2596,8 +3113,16 @@ function toolResult(value, summary) {
|
|
|
2596
3113
|
structuredContent: value,
|
|
2597
3114
|
};
|
|
2598
3115
|
}
|
|
2599
|
-
function toolError(code, message, retryable) {
|
|
2600
|
-
const error = {
|
|
3116
|
+
function toolError(code, message, retryable, retryAfterMs, details) {
|
|
3117
|
+
const error = {
|
|
3118
|
+
error: {
|
|
3119
|
+
code,
|
|
3120
|
+
message,
|
|
3121
|
+
retryable,
|
|
3122
|
+
...(retryAfterMs === undefined ? {} : { retry_after_ms: retryAfterMs }),
|
|
3123
|
+
...(details === undefined ? {} : { details }),
|
|
3124
|
+
},
|
|
3125
|
+
};
|
|
2601
3126
|
return {
|
|
2602
3127
|
isError: true,
|
|
2603
3128
|
content: [{ type: "text", text: JSON.stringify(error) }],
|
package/dist/src/usability.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratabook/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Connect AI agents to Strata markets and Sonar quotes with MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
48
|
-
"@stratabook/sdk": "0.2.
|
|
48
|
+
"@stratabook/sdk": "0.2.18",
|
|
49
49
|
"zod": "^3.25.76"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|