@stratabook/mcp 0.1.12 → 0.2.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/README.md +37 -10
- package/dist/src/autonomy.d.ts +108 -0
- package/dist/src/autonomy.js +230 -0
- package/dist/src/cli.js +10 -3
- package/dist/src/generated-harness.d.ts +55 -11
- package/dist/src/generated-harness.js +127 -9
- package/dist/src/server.d.ts +8 -0
- package/dist/src/server.js +1366 -162
- package/package.json +2 -2
package/dist/src/server.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_MAXIMUM_TOLERANCE_BPS, StrataApiError, StrataClient, StrataPlatformClient, } from "@stratabook/sdk";
|
|
3
|
+
import { decideAutonomy, estimateBaseNotionalUsd, quoteNotionalUsd, MarketMetaResolver, } from "./autonomy.js";
|
|
3
4
|
import * as z from "zod/v4";
|
|
4
5
|
import { STRATA_AGENT_HARNESS, STRATA_AGENT_HARNESS_INSTRUCTIONS, STRATA_AGENT_HARNESS_URI, STRATA_ACTION_GRAPH_URI, } from "./generated-harness.js";
|
|
5
6
|
import { SERVER_VERSION } from "./version.js";
|
|
6
7
|
const REFRESH_INTERVAL_MS = 5_000;
|
|
8
|
+
export const STRATA_PLATFORM_GRAPH_URI = "strata://platform-graph/v2";
|
|
7
9
|
export function capabilityAvailable(catalog, id) {
|
|
8
10
|
return catalog.capabilities.some((capability) => capability.id === id
|
|
9
11
|
&& capability.default_enabled
|
|
@@ -31,6 +33,147 @@ export async function probeStrataMcpReadiness(options = {}) {
|
|
|
31
33
|
harness_version: STRATA_AGENT_HARNESS.harness_version,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
/** Map tool arguments onto one order-control operation, or a tool error. */
|
|
37
|
+
function orderOperationFromArgs(args) {
|
|
38
|
+
let request;
|
|
39
|
+
if (args.action === "place") {
|
|
40
|
+
if (args.clientOrderId === undefined
|
|
41
|
+
|| args.side === undefined
|
|
42
|
+
|| args.orderType === undefined
|
|
43
|
+
|| args.limitPriceAtoms === undefined
|
|
44
|
+
|| args.sizeAtoms === undefined) {
|
|
45
|
+
return toolError("invalid_request", "Place requires clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
|
|
46
|
+
}
|
|
47
|
+
request = {
|
|
48
|
+
action: "place",
|
|
49
|
+
ownerWallet: args.ownerWallet,
|
|
50
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
51
|
+
...(args.accountSequence === undefined ? {} : { accountSequence: args.accountSequence }),
|
|
52
|
+
clientOrderId: args.clientOrderId,
|
|
53
|
+
side: args.side,
|
|
54
|
+
orderType: args.orderType,
|
|
55
|
+
limitPriceAtoms: args.limitPriceAtoms,
|
|
56
|
+
sizeAtoms: args.sizeAtoms,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
else if (args.action === "cancel") {
|
|
60
|
+
if (args.orderId === undefined) {
|
|
61
|
+
return toolError("invalid_request", "Cancel requires orderId.", false);
|
|
62
|
+
}
|
|
63
|
+
request = {
|
|
64
|
+
action: "cancel",
|
|
65
|
+
ownerWallet: args.ownerWallet,
|
|
66
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
67
|
+
orderId: args.orderId,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
else if (args.action === "cancel_all") {
|
|
71
|
+
request = {
|
|
72
|
+
action: "cancel_all",
|
|
73
|
+
ownerWallet: args.ownerWallet,
|
|
74
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else if (args.action === "replace") {
|
|
78
|
+
if (args.orderId === undefined
|
|
79
|
+
|| args.clientOrderId === undefined
|
|
80
|
+
|| args.side === undefined
|
|
81
|
+
|| args.orderType === undefined
|
|
82
|
+
|| args.limitPriceAtoms === undefined
|
|
83
|
+
|| args.sizeAtoms === undefined) {
|
|
84
|
+
return toolError("invalid_request", "Replace requires orderId, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
|
|
85
|
+
}
|
|
86
|
+
request = {
|
|
87
|
+
action: "replace",
|
|
88
|
+
ownerWallet: args.ownerWallet,
|
|
89
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
90
|
+
orderId: args.orderId,
|
|
91
|
+
...(args.accountSequence === undefined ? {} : { accountSequence: args.accountSequence }),
|
|
92
|
+
clientOrderId: args.clientOrderId,
|
|
93
|
+
side: args.side,
|
|
94
|
+
orderType: args.orderType,
|
|
95
|
+
limitPriceAtoms: args.limitPriceAtoms,
|
|
96
|
+
sizeAtoms: args.sizeAtoms,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (args.operations === undefined) {
|
|
101
|
+
return toolError("invalid_request", "Batch requires operations.", false);
|
|
102
|
+
}
|
|
103
|
+
const operations = [];
|
|
104
|
+
for (const operation of args.operations) {
|
|
105
|
+
if (operation.action === "cancel") {
|
|
106
|
+
if (operation.orderId === undefined) {
|
|
107
|
+
return toolError("invalid_request", "Batch cancel requires orderId.", false);
|
|
108
|
+
}
|
|
109
|
+
operations.push({ action: "cancel", orderId: operation.orderId });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (operation.clientOrderId === undefined
|
|
113
|
+
|| operation.side === undefined
|
|
114
|
+
|| operation.orderType === undefined
|
|
115
|
+
|| operation.limitPriceAtoms === undefined
|
|
116
|
+
|| operation.sizeAtoms === undefined
|
|
117
|
+
|| (operation.action === "replace" && operation.orderId === undefined)) {
|
|
118
|
+
return toolError("invalid_request", `Batch ${operation.action} has incomplete fields.`, false);
|
|
119
|
+
}
|
|
120
|
+
const place = {
|
|
121
|
+
...(operation.accountSequence === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { accountSequence: operation.accountSequence }),
|
|
124
|
+
clientOrderId: operation.clientOrderId,
|
|
125
|
+
side: operation.side,
|
|
126
|
+
orderType: operation.orderType,
|
|
127
|
+
limitPriceAtoms: operation.limitPriceAtoms,
|
|
128
|
+
sizeAtoms: operation.sizeAtoms,
|
|
129
|
+
};
|
|
130
|
+
operations.push(operation.action === "replace"
|
|
131
|
+
? { action: "replace", orderId: operation.orderId, ...place }
|
|
132
|
+
: { action: "place", ...place });
|
|
133
|
+
}
|
|
134
|
+
request = {
|
|
135
|
+
action: "batch",
|
|
136
|
+
ownerWallet: args.ownerWallet,
|
|
137
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
138
|
+
operations,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return request;
|
|
142
|
+
}
|
|
143
|
+
const PLATFORM_MARKET_PAGE_LIMIT = 100;
|
|
144
|
+
const PLATFORM_MARKET_MAX_PAGES = 20;
|
|
145
|
+
/**
|
|
146
|
+
* Every live platform market keyed by label, so the tool can hand agents the
|
|
147
|
+
* opaque `market_id` (and asset ids) that every by-market tool takes. A
|
|
148
|
+
* platform read failure leaves the list unidentified rather than failing it.
|
|
149
|
+
*/
|
|
150
|
+
async function platformMarketIdentities(platformClient) {
|
|
151
|
+
const identities = new Map();
|
|
152
|
+
try {
|
|
153
|
+
let cursor;
|
|
154
|
+
for (let page = 0; page < PLATFORM_MARKET_MAX_PAGES; page += 1) {
|
|
155
|
+
const response = await platformClient.markets.list(cursor === undefined
|
|
156
|
+
? { limit: PLATFORM_MARKET_PAGE_LIMIT }
|
|
157
|
+
: { limit: PLATFORM_MARKET_PAGE_LIMIT, cursor });
|
|
158
|
+
for (const market of response.markets) {
|
|
159
|
+
identities.set(market.label, {
|
|
160
|
+
market_id: market.market_id,
|
|
161
|
+
base_asset_id: market.base_asset_id,
|
|
162
|
+
quote_asset_id: market.quote_asset_id,
|
|
163
|
+
status: market.status,
|
|
164
|
+
available_actions: market.available_actions,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!response.page.has_more || response.page.next_cursor === null)
|
|
168
|
+
break;
|
|
169
|
+
cursor = response.page.next_cursor;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Identity is a convenience layered on the Sonar list; never fail the list for it.
|
|
174
|
+
}
|
|
175
|
+
return identities;
|
|
176
|
+
}
|
|
34
177
|
export async function createStrataMcpServer(options = {}) {
|
|
35
178
|
const client = strataClient(options);
|
|
36
179
|
const platformClient = options.platformClient ?? new StrataPlatformClient({
|
|
@@ -60,6 +203,19 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
60
203
|
},
|
|
61
204
|
],
|
|
62
205
|
}));
|
|
206
|
+
server.registerResource("strata_platform_graph", STRATA_PLATFORM_GRAPH_URI, {
|
|
207
|
+
title: "Strata Platform Graph",
|
|
208
|
+
description: "Complete customer-safe entity, operation, and workflow graph with live capability gates.",
|
|
209
|
+
mimeType: "application/json",
|
|
210
|
+
}, async () => ({
|
|
211
|
+
contents: [
|
|
212
|
+
{
|
|
213
|
+
uri: STRATA_PLATFORM_GRAPH_URI,
|
|
214
|
+
mimeType: "application/json",
|
|
215
|
+
text: JSON.stringify(await platformClient.discovery.graph()),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
}));
|
|
63
219
|
server.registerResource("strata_action_graph", STRATA_ACTION_GRAPH_URI, {
|
|
64
220
|
title: "Strata Action Graph",
|
|
65
221
|
description: "Live executable topology for discovery, quoting, external signing, and submission.",
|
|
@@ -83,38 +239,780 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
83
239
|
.max(2_000)
|
|
84
240
|
.describe("The user's concrete Strata market or quote objective."),
|
|
85
241
|
},
|
|
86
|
-
}, async ({ objective }) => ({
|
|
87
|
-
description: "Capability-gated Strata objective",
|
|
88
|
-
messages: [
|
|
89
|
-
{
|
|
90
|
-
role: "user",
|
|
91
|
-
content: {
|
|
92
|
-
type: "text",
|
|
93
|
-
text: `${STRATA_AGENT_HARNESS_INSTRUCTIONS}\n\nObjective: ${objective.trim()}`,
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
}));
|
|
98
|
-
server.registerTool("strata_capabilities", {
|
|
99
|
-
title: "Strata capabilities",
|
|
100
|
-
description: "See which Strata features are currently available to MCP clients.",
|
|
242
|
+
}, async ({ objective }) => ({
|
|
243
|
+
description: "Capability-gated Strata objective",
|
|
244
|
+
messages: [
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: {
|
|
248
|
+
type: "text",
|
|
249
|
+
text: `${STRATA_AGENT_HARNESS_INSTRUCTIONS}\n\nObjective: ${objective.trim()}`,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
}));
|
|
254
|
+
server.registerTool("strata_capabilities", {
|
|
255
|
+
title: "Strata capabilities",
|
|
256
|
+
description: "See which Strata features are currently available to MCP clients.",
|
|
257
|
+
annotations: {
|
|
258
|
+
readOnlyHint: true,
|
|
259
|
+
destructiveHint: false,
|
|
260
|
+
idempotentHint: true,
|
|
261
|
+
openWorldHint: true,
|
|
262
|
+
},
|
|
263
|
+
}, async () => toolResult(await client.capabilities(), "Current Strata capabilities."));
|
|
264
|
+
server.registerTool("strata_action_graph", {
|
|
265
|
+
title: "Strata action graph",
|
|
266
|
+
description: "Discover live operations, required capabilities, transition conditions, and external signing boundaries.",
|
|
267
|
+
annotations: {
|
|
268
|
+
readOnlyHint: true,
|
|
269
|
+
destructiveHint: false,
|
|
270
|
+
idempotentHint: true,
|
|
271
|
+
openWorldHint: true,
|
|
272
|
+
},
|
|
273
|
+
}, async () => toolResult(await client.actionGraph(), "Current Strata action graph."));
|
|
274
|
+
server.registerTool("strata_platform_graph", {
|
|
275
|
+
title: "Strata platform graph",
|
|
276
|
+
description: "Discover every public module, entity relationship, operation binding, workflow, and live availability gate.",
|
|
277
|
+
annotations: {
|
|
278
|
+
readOnlyHint: true,
|
|
279
|
+
destructiveHint: false,
|
|
280
|
+
idempotentHint: true,
|
|
281
|
+
openWorldHint: true,
|
|
282
|
+
},
|
|
283
|
+
}, async () => {
|
|
284
|
+
const graph = await platformClient.discovery.graph();
|
|
285
|
+
const liveOperations = graph.operations.filter((operation) => operation.available).length;
|
|
286
|
+
return toolResult(graph, `${liveOperations} of ${graph.operations.length} mapped Strata operations are currently live.`);
|
|
287
|
+
});
|
|
288
|
+
server.registerTool("strata_market_making_status", {
|
|
289
|
+
title: "Read Strata maker status",
|
|
290
|
+
description: "A maker's products, live exposure, health, and kill state in one market — public by wallet address, no signature.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
293
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
294
|
+
},
|
|
295
|
+
annotations: {
|
|
296
|
+
readOnlyHint: true,
|
|
297
|
+
destructiveHint: false,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
openWorldHint: true,
|
|
300
|
+
},
|
|
301
|
+
}, async ({ marketId, walletAddress }) => {
|
|
302
|
+
const response = await platformClient.marketMaking.status(marketId, walletAddress);
|
|
303
|
+
return toolResult(response, `${response.active_products} active maker products; reconcile intent, Strand, Current, signed-quote, and dead-man state before changing exposure.`);
|
|
304
|
+
});
|
|
305
|
+
server.registerTool("strata_market_making_reputation", {
|
|
306
|
+
title: "Read Strata maker reputation",
|
|
307
|
+
description: "A maker's reliability, participation, tier, and signed-quote eligibility in one market — public by wallet address, no signature.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
310
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
311
|
+
},
|
|
312
|
+
annotations: {
|
|
313
|
+
readOnlyHint: true,
|
|
314
|
+
destructiveHint: false,
|
|
315
|
+
idempotentHint: true,
|
|
316
|
+
openWorldHint: true,
|
|
317
|
+
},
|
|
318
|
+
}, async ({ marketId, walletAddress }) => toolResult(await platformClient.marketMaking.reputation(marketId, walletAddress), "Maker reputation record. Use tier_progress and signed_quote_stream_eligible before choosing a maker transport."));
|
|
319
|
+
server.registerTool("strata_status", {
|
|
320
|
+
title: "Strata status",
|
|
321
|
+
description: "Read product-level readiness and the number of currently live mapped operations.",
|
|
322
|
+
annotations: {
|
|
323
|
+
readOnlyHint: true,
|
|
324
|
+
destructiveHint: false,
|
|
325
|
+
idempotentHint: true,
|
|
326
|
+
openWorldHint: true,
|
|
327
|
+
},
|
|
328
|
+
}, async () => {
|
|
329
|
+
const status = await platformClient.discovery.status();
|
|
330
|
+
return toolResult(status, `Strata is ${status.status}; ${status.available_operations} mapped operations are live.`);
|
|
331
|
+
});
|
|
332
|
+
server.registerTool("strata_candles", {
|
|
333
|
+
title: "Strata candles",
|
|
334
|
+
description: "Read bounded time-bucketed candles for one opaque Strata market ID.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
337
|
+
fromMs: z.number().int().nonnegative(),
|
|
338
|
+
toMs: z.number().int().positive(),
|
|
339
|
+
resolutionSeconds: z.number().int().min(60).max(86_400).optional().default(300),
|
|
340
|
+
},
|
|
341
|
+
annotations: {
|
|
342
|
+
readOnlyHint: true,
|
|
343
|
+
destructiveHint: false,
|
|
344
|
+
idempotentHint: true,
|
|
345
|
+
openWorldHint: true,
|
|
346
|
+
},
|
|
347
|
+
}, async ({ marketId, fromMs, toMs, resolutionSeconds }) => {
|
|
348
|
+
const candles = await platformClient.marketData.candles(marketId, {
|
|
349
|
+
fromMs,
|
|
350
|
+
toMs,
|
|
351
|
+
resolutionSeconds,
|
|
352
|
+
});
|
|
353
|
+
return toolResult(candles, `${candles.candles.length} Strata candles returned.`);
|
|
354
|
+
});
|
|
355
|
+
server.registerTool("strata_marks", {
|
|
356
|
+
title: "Strata mark",
|
|
357
|
+
description: "Read the current customer-facing reference price for one opaque market ID.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
360
|
+
},
|
|
361
|
+
annotations: {
|
|
362
|
+
readOnlyHint: true,
|
|
363
|
+
destructiveHint: false,
|
|
364
|
+
idempotentHint: true,
|
|
365
|
+
openWorldHint: true,
|
|
366
|
+
},
|
|
367
|
+
}, async ({ marketId }) => {
|
|
368
|
+
const mark = await platformClient.marketData.mark(marketId);
|
|
369
|
+
return toolResult(mark, mark.stale ? "Strata mark is stale." : "Current Strata mark.");
|
|
370
|
+
});
|
|
371
|
+
server.registerTool("strata_book", {
|
|
372
|
+
title: "Strata order book",
|
|
373
|
+
description: "Read the executable order book for one opaque market ID: bids and asks, one size per price "
|
|
374
|
+
+ "level. Top of book is the best bid and ask.",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
377
|
+
depth: z
|
|
378
|
+
.number()
|
|
379
|
+
.int()
|
|
380
|
+
.min(1)
|
|
381
|
+
.max(2_000)
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("Price levels per side (default server depth; max 2000)."),
|
|
384
|
+
},
|
|
385
|
+
annotations: {
|
|
386
|
+
readOnlyHint: true,
|
|
387
|
+
destructiveHint: false,
|
|
388
|
+
idempotentHint: true,
|
|
389
|
+
openWorldHint: true,
|
|
390
|
+
},
|
|
391
|
+
}, async ({ marketId, depth }) => {
|
|
392
|
+
const book = await platformClient.books.snapshot(marketId, depth === undefined ? {} : { depth });
|
|
393
|
+
return toolResult(book, `Book for ${marketId}: ${book.bids.length} bid / ${book.asks.length} ask levels at sequence ${book.sequence}.`);
|
|
394
|
+
});
|
|
395
|
+
server.registerTool("strata_bbo", {
|
|
396
|
+
title: "Strata best bid/ask",
|
|
397
|
+
description: "Read the current best bid and best ask (top of book) for one opaque market ID.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
400
|
+
},
|
|
401
|
+
annotations: {
|
|
402
|
+
readOnlyHint: true,
|
|
403
|
+
destructiveHint: false,
|
|
404
|
+
idempotentHint: true,
|
|
405
|
+
openWorldHint: true,
|
|
406
|
+
},
|
|
407
|
+
}, async ({ marketId }) => {
|
|
408
|
+
const bbo = await platformClient.books.bestBidAsk(marketId);
|
|
409
|
+
return toolResult(bbo, `BBO for ${marketId}: bid ${bbo.best_bid?.price_atoms ?? "—"} / ask ${bbo.best_ask?.price_atoms ?? "—"}.`);
|
|
410
|
+
});
|
|
411
|
+
server.registerTool("strata_trades", {
|
|
412
|
+
title: "Strata recent trades",
|
|
413
|
+
description: "Read recent anonymized prints for one opaque market ID: price, size, side, and time.",
|
|
414
|
+
inputSchema: {
|
|
415
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
416
|
+
limit: z
|
|
417
|
+
.number()
|
|
418
|
+
.int()
|
|
419
|
+
.min(1)
|
|
420
|
+
.max(500)
|
|
421
|
+
.optional()
|
|
422
|
+
.describe("Most recent prints to return (default server limit; max 500)."),
|
|
423
|
+
},
|
|
424
|
+
annotations: {
|
|
425
|
+
readOnlyHint: true,
|
|
426
|
+
destructiveHint: false,
|
|
427
|
+
idempotentHint: true,
|
|
428
|
+
openWorldHint: true,
|
|
429
|
+
},
|
|
430
|
+
}, async ({ marketId, limit }) => {
|
|
431
|
+
const trades = await platformClient.books.trades(marketId, limit === undefined ? {} : { limit });
|
|
432
|
+
return toolResult(trades, `${trades.trades.length} recent prints for ${marketId}.`);
|
|
433
|
+
});
|
|
434
|
+
server.registerTool("strata_execution_status", {
|
|
435
|
+
title: "Strata execution status",
|
|
436
|
+
description: "Recover prepared state or a restart-durable confirmed execution receipt.",
|
|
437
|
+
inputSchema: {
|
|
438
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
439
|
+
executionId: z.string().regex(/^se_[0-9a-f]{32}$/),
|
|
440
|
+
},
|
|
441
|
+
annotations: {
|
|
442
|
+
readOnlyHint: true,
|
|
443
|
+
destructiveHint: false,
|
|
444
|
+
idempotentHint: true,
|
|
445
|
+
openWorldHint: true,
|
|
446
|
+
},
|
|
447
|
+
}, async ({ marketId, executionId }) => {
|
|
448
|
+
const receipt = await platformClient.executions.status(marketId, executionId);
|
|
449
|
+
return toolResult(receipt, `Execution is ${receipt.status}.`);
|
|
450
|
+
});
|
|
451
|
+
server.registerTool("strata_twaps", {
|
|
452
|
+
title: "Strata TWAPs",
|
|
453
|
+
description: "Read sanitized progress and terminal receipts for wallet-owned TWAP schedules.",
|
|
454
|
+
inputSchema: {
|
|
455
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
456
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
457
|
+
},
|
|
458
|
+
annotations: {
|
|
459
|
+
readOnlyHint: true,
|
|
460
|
+
destructiveHint: false,
|
|
461
|
+
idempotentHint: true,
|
|
462
|
+
openWorldHint: true,
|
|
463
|
+
},
|
|
464
|
+
}, async ({ marketId, walletAddress }) => {
|
|
465
|
+
const response = await platformClient.algos.twaps(marketId, walletAddress);
|
|
466
|
+
return toolResult(response, `${response.twaps.length} TWAP schedules returned.`);
|
|
467
|
+
});
|
|
468
|
+
server.registerTool("strata_twap_challenge", {
|
|
469
|
+
title: "Prepare a Strata TWAP authorization",
|
|
470
|
+
description: "Request exact external-signing bytes for a bounded TWAP schedule.",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
473
|
+
ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
474
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
475
|
+
side: z.enum(["buy", "sell"]),
|
|
476
|
+
totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/),
|
|
477
|
+
slicesTotal: z.number().int().min(2).max(120),
|
|
478
|
+
maximumToleranceBps: z.number().int().min(1).max(1_000),
|
|
479
|
+
intervalSlots: z.number().int().min(25).max(4_500),
|
|
480
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/),
|
|
481
|
+
},
|
|
482
|
+
annotations: {
|
|
483
|
+
readOnlyHint: false,
|
|
484
|
+
destructiveHint: false,
|
|
485
|
+
idempotentHint: false,
|
|
486
|
+
openWorldHint: true,
|
|
487
|
+
},
|
|
488
|
+
}, async (input) => {
|
|
489
|
+
const response = await platformClient.algos.challenge(input.marketId, {
|
|
490
|
+
action: "place",
|
|
491
|
+
ownerWallet: input.ownerWallet,
|
|
492
|
+
sessionPublicKey: input.sessionPublicKey,
|
|
493
|
+
side: input.side,
|
|
494
|
+
totalSizeAtoms: input.totalSizeAtoms,
|
|
495
|
+
slicesTotal: input.slicesTotal,
|
|
496
|
+
maximumToleranceBps: input.maximumToleranceBps,
|
|
497
|
+
intervalSlots: input.intervalSlots,
|
|
498
|
+
limitPriceAtoms: input.limitPriceAtoms,
|
|
499
|
+
});
|
|
500
|
+
return toolResult(response, "Sign the returned authorization payload externally.");
|
|
501
|
+
});
|
|
502
|
+
server.registerTool("strata_twap_cancel", {
|
|
503
|
+
title: "Prepare Strata TWAP cancellation",
|
|
504
|
+
description: "Request exact external-signing bytes to cancel one active owned TWAP.",
|
|
505
|
+
inputSchema: {
|
|
506
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
507
|
+
ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
508
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
509
|
+
twapId: z.string().regex(/^twap_[0-9a-f]{32}$/),
|
|
510
|
+
},
|
|
511
|
+
annotations: {
|
|
512
|
+
readOnlyHint: false,
|
|
513
|
+
destructiveHint: true,
|
|
514
|
+
idempotentHint: false,
|
|
515
|
+
openWorldHint: true,
|
|
516
|
+
},
|
|
517
|
+
}, async (input) => {
|
|
518
|
+
const response = await platformClient.algos.challenge(input.marketId, {
|
|
519
|
+
action: "cancel",
|
|
520
|
+
ownerWallet: input.ownerWallet,
|
|
521
|
+
sessionPublicKey: input.sessionPublicKey,
|
|
522
|
+
twapId: input.twapId,
|
|
523
|
+
});
|
|
524
|
+
return toolResult(response, "Sign the returned cancellation payload externally.");
|
|
525
|
+
});
|
|
526
|
+
server.registerTool("strata_twap_prepare", {
|
|
527
|
+
title: "Prepare Strata TWAP transaction",
|
|
528
|
+
description: "Prepare a canonical TWAP transaction. One signature: pass the action itself (place fields or twapId to cancel) and sign only the returned transaction. (A challengeId + authorizationSignature is still accepted.)",
|
|
529
|
+
inputSchema: {
|
|
530
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
531
|
+
challengeId: z.string().regex(/^twc_[0-9a-f]{32}$/).optional(),
|
|
532
|
+
authorizationSignature: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{64,88}$/).optional(),
|
|
533
|
+
action: z.enum(["place", "cancel"]).optional(),
|
|
534
|
+
ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
535
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
536
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
537
|
+
totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
538
|
+
slicesTotal: z.number().int().min(2).max(120).optional(),
|
|
539
|
+
maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
|
|
540
|
+
intervalSlots: z.number().int().min(25).max(4_500).optional(),
|
|
541
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
542
|
+
twapId: z.string().regex(/^twap_[0-9a-f]{32}$/).optional(),
|
|
543
|
+
},
|
|
544
|
+
annotations: {
|
|
545
|
+
readOnlyHint: false,
|
|
546
|
+
destructiveHint: false,
|
|
547
|
+
idempotentHint: false,
|
|
548
|
+
openWorldHint: true,
|
|
549
|
+
},
|
|
550
|
+
}, async (input) => {
|
|
551
|
+
if (input.challengeId !== undefined || input.authorizationSignature !== undefined) {
|
|
552
|
+
if (input.challengeId === undefined || input.authorizationSignature === undefined) {
|
|
553
|
+
return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
|
|
554
|
+
}
|
|
555
|
+
const response = await platformClient.algos.prepare(input.marketId, {
|
|
556
|
+
challengeId: input.challengeId,
|
|
557
|
+
authorizationSignature: input.authorizationSignature,
|
|
558
|
+
});
|
|
559
|
+
return toolResult(response, "Verify and sign this canonical transaction externally.");
|
|
560
|
+
}
|
|
561
|
+
if (input.ownerWallet === undefined || input.sessionPublicKey === undefined || input.action === undefined) {
|
|
562
|
+
return toolError("invalid_request", "Pass action, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
|
|
563
|
+
}
|
|
564
|
+
let operation;
|
|
565
|
+
if (input.action === "cancel") {
|
|
566
|
+
if (input.twapId === undefined)
|
|
567
|
+
return toolError("invalid_request", "Cancel requires twapId.", false);
|
|
568
|
+
operation = {
|
|
569
|
+
action: "cancel",
|
|
570
|
+
ownerWallet: input.ownerWallet,
|
|
571
|
+
sessionPublicKey: input.sessionPublicKey,
|
|
572
|
+
twapId: input.twapId,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
if (input.side === undefined
|
|
577
|
+
|| input.totalSizeAtoms === undefined
|
|
578
|
+
|| input.slicesTotal === undefined
|
|
579
|
+
|| input.maximumToleranceBps === undefined
|
|
580
|
+
|| input.intervalSlots === undefined
|
|
581
|
+
|| input.limitPriceAtoms === undefined) {
|
|
582
|
+
return toolError("invalid_request", "Place requires side, totalSizeAtoms, slicesTotal, maximumToleranceBps, intervalSlots, and limitPriceAtoms.", false);
|
|
583
|
+
}
|
|
584
|
+
operation = {
|
|
585
|
+
action: "place",
|
|
586
|
+
ownerWallet: input.ownerWallet,
|
|
587
|
+
sessionPublicKey: input.sessionPublicKey,
|
|
588
|
+
side: input.side,
|
|
589
|
+
totalSizeAtoms: input.totalSizeAtoms,
|
|
590
|
+
slicesTotal: input.slicesTotal,
|
|
591
|
+
maximumToleranceBps: input.maximumToleranceBps,
|
|
592
|
+
intervalSlots: input.intervalSlots,
|
|
593
|
+
limitPriceAtoms: input.limitPriceAtoms,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const response = await platformClient.algos.prepare(input.marketId, { operation });
|
|
597
|
+
return toolResult(response, "One signature: verify this canonical transaction, then sign it externally with the session key and submit.");
|
|
598
|
+
});
|
|
599
|
+
server.registerTool("strata_twap_submit", {
|
|
600
|
+
title: "Submit Strata TWAP transaction",
|
|
601
|
+
description: "Submit the exact externally signed TWAP transaction idempotently.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
604
|
+
twapControlId: z.string().regex(/^twctl_[0-9a-f]{32}$/),
|
|
605
|
+
signedTransactionBase64: z.string().min(4),
|
|
606
|
+
idempotencyKey: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/),
|
|
607
|
+
},
|
|
608
|
+
annotations: {
|
|
609
|
+
readOnlyHint: false,
|
|
610
|
+
destructiveHint: true,
|
|
611
|
+
idempotentHint: true,
|
|
612
|
+
openWorldHint: true,
|
|
613
|
+
},
|
|
614
|
+
}, async ({ marketId, twapControlId, signedTransactionBase64, idempotencyKey }) => {
|
|
615
|
+
const response = await platformClient.algos.submit(marketId, {
|
|
616
|
+
twapControlId,
|
|
617
|
+
signedTransactionBase64,
|
|
618
|
+
idempotencyKey,
|
|
619
|
+
});
|
|
620
|
+
return toolResult(response, `TWAP action submitted as ${response.signature}.`);
|
|
621
|
+
});
|
|
622
|
+
server.registerTool("strata_portfolio", {
|
|
623
|
+
title: "Strata account",
|
|
624
|
+
description: "The whole account in one public read, by wallet address: balances (total / available / locked, exact USD), positions, open orders, and recent fills across every live market. No signature, no session key, no market selection.",
|
|
625
|
+
inputSchema: {
|
|
626
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
627
|
+
},
|
|
628
|
+
annotations: {
|
|
629
|
+
readOnlyHint: true,
|
|
630
|
+
destructiveHint: false,
|
|
631
|
+
idempotentHint: true,
|
|
632
|
+
openWorldHint: true,
|
|
633
|
+
},
|
|
634
|
+
}, async ({ walletAddress }) => {
|
|
635
|
+
const response = await platformClient.account.read(walletAddress);
|
|
636
|
+
const activity = `${response.open_orders.length} open orders, ${response.recent_fills.length} recent fills`
|
|
637
|
+
+ (response.unavailable_market_ids.length > 0
|
|
638
|
+
? ` (${response.unavailable_market_ids.length} markets unavailable)`
|
|
639
|
+
: "");
|
|
640
|
+
return toolResult(response, response.valuation_complete
|
|
641
|
+
? `${response.balances.length} held assets; ${activity}; equity ${response.equity_usd_micros} USD micros at slot ${response.observed_slot}.`
|
|
642
|
+
: `${response.balances.length} held assets; ${activity}; ${response.unpriced_asset_ids.length} unpriced, USD totals unavailable.`);
|
|
643
|
+
});
|
|
644
|
+
server.registerTool("strata_portfolio_history", {
|
|
645
|
+
title: "Strata portfolio history",
|
|
646
|
+
description: "Read genuine stored account-equity history in exact USD micros.",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
649
|
+
range: z.enum(["24h", "7d", "30d"]).optional().default("24h"),
|
|
650
|
+
},
|
|
651
|
+
annotations: {
|
|
652
|
+
readOnlyHint: true,
|
|
653
|
+
destructiveHint: false,
|
|
654
|
+
idempotentHint: true,
|
|
655
|
+
openWorldHint: true,
|
|
656
|
+
},
|
|
657
|
+
}, async ({ walletAddress, range }) => {
|
|
658
|
+
const response = await platformClient.account.portfolioHistory(walletAddress, range);
|
|
659
|
+
return toolResult(response, `${response.points.length} stored equity samples returned.`);
|
|
660
|
+
});
|
|
661
|
+
server.registerTool("strata_vault_status", {
|
|
662
|
+
title: "Strata Vault status",
|
|
663
|
+
description: "Read sealed owner state and optional external-session readiness without construction identifiers.",
|
|
664
|
+
inputSchema: {
|
|
665
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
666
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
667
|
+
},
|
|
668
|
+
annotations: {
|
|
669
|
+
readOnlyHint: true,
|
|
670
|
+
destructiveHint: false,
|
|
671
|
+
idempotentHint: true,
|
|
672
|
+
openWorldHint: true,
|
|
673
|
+
},
|
|
674
|
+
}, async ({ walletAddress, sessionPublicKey }) => {
|
|
675
|
+
const response = await platformClient.vault.status({ walletAddress, sessionPublicKey });
|
|
676
|
+
return toolResult(response, response.session === null
|
|
677
|
+
? `Vault is ${response.state}; no session was requested.`
|
|
678
|
+
: `Vault is ${response.state}; requested session is ${response.session.state}.`);
|
|
679
|
+
});
|
|
680
|
+
server.registerTool("strata_vault_pause", {
|
|
681
|
+
title: "Prepare Strata Vault pause",
|
|
682
|
+
description: "Prepare an owner-authorized pause or resume transaction for external verification, signing, and broadcast.",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
685
|
+
paused: z.boolean(),
|
|
686
|
+
},
|
|
687
|
+
annotations: {
|
|
688
|
+
readOnlyHint: false,
|
|
689
|
+
destructiveHint: true,
|
|
690
|
+
idempotentHint: false,
|
|
691
|
+
openWorldHint: true,
|
|
692
|
+
},
|
|
693
|
+
}, async ({ walletAddress, paused }) => {
|
|
694
|
+
const response = await platformClient.vault.preparePause({ walletAddress, paused });
|
|
695
|
+
return toolResult(response, `Verify this ${paused ? "pause" : "resume"} transaction, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).`);
|
|
696
|
+
});
|
|
697
|
+
server.registerTool("strata_vault_setup", {
|
|
698
|
+
title: "Prepare Strata Vault onboarding",
|
|
699
|
+
description: "One-signature onboarding: register an external session key for a wallet. Only the wallet and the session key are needed; one session then trades every market. Policy fields are optional. A first strata_vault_deposit that names the session key does this in the same transaction.",
|
|
700
|
+
inputSchema: {
|
|
701
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
702
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
703
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/).optional(),
|
|
704
|
+
spendingLimits: z
|
|
705
|
+
.array(z.object({
|
|
706
|
+
assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
|
|
707
|
+
maximumPerExecutionAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
708
|
+
}))
|
|
709
|
+
.max(4)
|
|
710
|
+
.optional(),
|
|
711
|
+
expiresAtMs: z.number().int().positive().optional(),
|
|
712
|
+
minimumIntervalSeconds: z.number().int().min(1).max(86_400).optional(),
|
|
713
|
+
maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
|
|
714
|
+
},
|
|
715
|
+
annotations: {
|
|
716
|
+
readOnlyHint: false,
|
|
717
|
+
destructiveHint: false,
|
|
718
|
+
idempotentHint: false,
|
|
719
|
+
openWorldHint: true,
|
|
720
|
+
},
|
|
721
|
+
}, async ({ walletAddress, sessionPublicKey, marketId, spendingLimits, expiresAtMs, minimumIntervalSeconds, maximumToleranceBps, }) => {
|
|
722
|
+
const response = await platformClient.vault.prepareSetup({
|
|
723
|
+
walletAddress,
|
|
724
|
+
sessionPublicKey,
|
|
725
|
+
marketId: marketId ?? null,
|
|
726
|
+
expiresAtMs: expiresAtMs ?? null,
|
|
727
|
+
minimumIntervalSeconds,
|
|
728
|
+
maximumToleranceBps,
|
|
729
|
+
spendingLimits: (spendingLimits ?? []).map((limit) => ({
|
|
730
|
+
assetId: limit.assetId,
|
|
731
|
+
maximumPerExecutionAtoms: limit.maximumPerExecutionAtoms ?? null,
|
|
732
|
+
})),
|
|
733
|
+
});
|
|
734
|
+
return toolResult(response, "Verify every echoed session policy field, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
|
|
735
|
+
});
|
|
736
|
+
server.registerTool("strata_vault_deposit", {
|
|
737
|
+
title: "Prepare Strata Vault deposit",
|
|
738
|
+
description: "Prepare an exact owner-funded Vault deposit using opaque market and asset IDs. Name sessionPublicKey and a first deposit also registers that session in the same transaction (one owner signature onboards and funds the wallet).",
|
|
739
|
+
inputSchema: {
|
|
740
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
741
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
742
|
+
assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
|
|
743
|
+
amountAtoms: z.string().regex(/^[1-9][0-9]*$/),
|
|
744
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
745
|
+
},
|
|
746
|
+
annotations: {
|
|
747
|
+
readOnlyHint: false,
|
|
748
|
+
destructiveHint: false,
|
|
749
|
+
idempotentHint: false,
|
|
750
|
+
openWorldHint: true,
|
|
751
|
+
},
|
|
752
|
+
}, async ({ walletAddress, marketId, assetId, amountAtoms, sessionPublicKey }) => {
|
|
753
|
+
const response = await platformClient.vault.prepareDeposit({
|
|
754
|
+
walletAddress,
|
|
755
|
+
marketId,
|
|
756
|
+
assetId,
|
|
757
|
+
amountAtoms,
|
|
758
|
+
sessionPublicKey: sessionPublicKey ?? null,
|
|
759
|
+
});
|
|
760
|
+
return toolResult(response, response.registers_session
|
|
761
|
+
? "This deposit also registers the session key. Verify the exact market, asset, amount, and session, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true)."
|
|
762
|
+
: "Verify the exact market, asset, and amount, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
|
|
763
|
+
});
|
|
764
|
+
server.registerTool("strata_vault_withdraw", {
|
|
765
|
+
title: "Prepare Strata Vault withdrawal",
|
|
766
|
+
description: "Prepare an exact owner-authorized withdrawal to a destination wallet.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
769
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
770
|
+
assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
|
|
771
|
+
destinationWalletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
772
|
+
amountAtoms: z.string().regex(/^[1-9][0-9]*$/),
|
|
773
|
+
},
|
|
774
|
+
annotations: {
|
|
775
|
+
readOnlyHint: false,
|
|
776
|
+
destructiveHint: true,
|
|
777
|
+
idempotentHint: false,
|
|
778
|
+
openWorldHint: true,
|
|
779
|
+
},
|
|
780
|
+
}, async ({ walletAddress, marketId, assetId, destinationWalletAddress, amountAtoms }) => {
|
|
781
|
+
const response = await platformClient.vault.prepareWithdrawal({
|
|
782
|
+
walletAddress,
|
|
783
|
+
marketId,
|
|
784
|
+
assetId,
|
|
785
|
+
destinationWalletAddress,
|
|
786
|
+
amountAtoms,
|
|
787
|
+
});
|
|
788
|
+
return toolResult(response, "Verify the exact destination and amount, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
|
|
789
|
+
});
|
|
790
|
+
server.registerTool("strata_vault_delegate", {
|
|
791
|
+
title: "Prepare Strata Vault session control",
|
|
792
|
+
description: "Prepare owner-authorized revocation of one externally held Vault session key.",
|
|
793
|
+
inputSchema: {
|
|
794
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
795
|
+
sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
796
|
+
action: z.literal("revoke"),
|
|
797
|
+
},
|
|
798
|
+
annotations: {
|
|
799
|
+
readOnlyHint: false,
|
|
800
|
+
destructiveHint: true,
|
|
801
|
+
idempotentHint: false,
|
|
802
|
+
openWorldHint: true,
|
|
803
|
+
},
|
|
804
|
+
}, async ({ walletAddress, sessionPublicKey, action }) => {
|
|
805
|
+
const response = await platformClient.vault.prepareDelegate({
|
|
806
|
+
walletAddress,
|
|
807
|
+
sessionPublicKey,
|
|
808
|
+
action,
|
|
809
|
+
});
|
|
810
|
+
return toolResult(response, "Verify both identities and the destructive action, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
|
|
811
|
+
});
|
|
812
|
+
server.registerTool("strata_vault_policy", {
|
|
813
|
+
title: "Prepare Strata Vault withdrawal policy",
|
|
814
|
+
description: "Prepare an owner-authorized blocked or restricted withdrawal access policy.",
|
|
815
|
+
inputSchema: {
|
|
816
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
817
|
+
mode: z.enum(["blocked", "restricted"]),
|
|
818
|
+
allowedWalletAddresses: z.array(z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/)).max(8).optional().default([]),
|
|
819
|
+
},
|
|
820
|
+
annotations: {
|
|
821
|
+
readOnlyHint: false,
|
|
822
|
+
destructiveHint: true,
|
|
823
|
+
idempotentHint: false,
|
|
824
|
+
openWorldHint: true,
|
|
825
|
+
},
|
|
826
|
+
}, async ({ walletAddress, mode, allowedWalletAddresses }) => {
|
|
827
|
+
const response = await platformClient.vault.preparePolicy({
|
|
828
|
+
walletAddress,
|
|
829
|
+
withdrawalAccess: { mode, allowedWalletAddresses },
|
|
830
|
+
});
|
|
831
|
+
return toolResult(response, "Verify the exact withdrawal access policy, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
|
|
832
|
+
});
|
|
833
|
+
server.registerTool("strata_vault_submit", {
|
|
834
|
+
title: "Submit a prepared Strata Vault transaction",
|
|
835
|
+
description: "Submit an owner-signed prepared Vault transaction (setup, deposit, withdrawal, session, "
|
|
836
|
+
+ "policy, pause). Strata verifies it is exactly the prepared transaction, pays the network "
|
|
837
|
+
+ "fee and any rent when the preparation was sponsored (owners without SOL; recovered later "
|
|
838
|
+
+ "from their deposits as network_cost_atoms), and broadcasts it — the owner needs no SOL "
|
|
839
|
+
+ "and no RPC. Idempotent per idempotencyKey; read the outcome with strata_vault_submission.",
|
|
840
|
+
inputSchema: {
|
|
841
|
+
preparationId: z.string().regex(/^vp_[0-9a-f]{32}$/).describe("preparation_id from the prepare response."),
|
|
842
|
+
signedTransactionBase64: z.string().min(1).describe("The prepared transaction with the owner's signature added, base64."),
|
|
843
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/),
|
|
844
|
+
},
|
|
845
|
+
annotations: {
|
|
846
|
+
readOnlyHint: false,
|
|
847
|
+
destructiveHint: false,
|
|
848
|
+
idempotentHint: true,
|
|
849
|
+
openWorldHint: true,
|
|
850
|
+
},
|
|
851
|
+
}, async ({ preparationId, signedTransactionBase64, idempotencyKey }) => {
|
|
852
|
+
const response = await platformClient.vault.submit({
|
|
853
|
+
preparationId,
|
|
854
|
+
signedTransactionBase64,
|
|
855
|
+
idempotencyKey,
|
|
856
|
+
});
|
|
857
|
+
return toolResult(response, `Vault ${response.action} ${response.status}${response.sponsored ? " (Strata paid the fee)" : ""}: `
|
|
858
|
+
+ `signature ${response.signature}. Poll strata_vault_submission until confirmed.`);
|
|
859
|
+
});
|
|
860
|
+
server.registerTool("strata_vault_submission", {
|
|
861
|
+
title: "Strata Vault submission status",
|
|
862
|
+
description: "Read the durable outcome of a submitted Vault transaction: submitted, confirmed, or failed.",
|
|
863
|
+
inputSchema: {
|
|
864
|
+
preparationId: z.string().regex(/^vp_[0-9a-f]{32}$/),
|
|
865
|
+
},
|
|
866
|
+
annotations: {
|
|
867
|
+
readOnlyHint: true,
|
|
868
|
+
destructiveHint: false,
|
|
869
|
+
idempotentHint: true,
|
|
870
|
+
openWorldHint: true,
|
|
871
|
+
},
|
|
872
|
+
}, async ({ preparationId }) => {
|
|
873
|
+
const response = await platformClient.vault.submission(preparationId);
|
|
874
|
+
return toolResult(response, `Vault ${response.action} is ${response.status}`
|
|
875
|
+
+ `${response.failure_code ? ` (${response.failure_code})` : ""}.`);
|
|
876
|
+
});
|
|
877
|
+
server.registerTool("strata_rewards", {
|
|
878
|
+
title: "Strata rewards",
|
|
879
|
+
description: "Read the current rewards season, standings, and optional owner score.",
|
|
880
|
+
inputSchema: {
|
|
881
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
882
|
+
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
883
|
+
},
|
|
884
|
+
annotations: {
|
|
885
|
+
readOnlyHint: true,
|
|
886
|
+
destructiveHint: false,
|
|
887
|
+
idempotentHint: true,
|
|
888
|
+
openWorldHint: true,
|
|
889
|
+
},
|
|
890
|
+
}, async ({ walletAddress, limit }) => {
|
|
891
|
+
const response = await platformClient.rewards.read({ walletAddress, limit });
|
|
892
|
+
return toolResult(response, `${response.standings.length} reward standings returned.`);
|
|
893
|
+
});
|
|
894
|
+
server.registerTool("strata_referrals", {
|
|
895
|
+
title: "Strata referrals",
|
|
896
|
+
description: "Read an owner's referral state and exact claimable reward atoms.",
|
|
897
|
+
inputSchema: {
|
|
898
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
899
|
+
},
|
|
101
900
|
annotations: {
|
|
102
901
|
readOnlyHint: true,
|
|
103
902
|
destructiveHint: false,
|
|
104
903
|
idempotentHint: true,
|
|
105
904
|
openWorldHint: true,
|
|
106
905
|
},
|
|
107
|
-
}, async (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
906
|
+
}, async ({ walletAddress }) => {
|
|
907
|
+
const response = await platformClient.referrals.read(walletAddress);
|
|
908
|
+
return toolResult(response, response.enabled ? "Referral state returned." : "Referrals are disabled.");
|
|
909
|
+
});
|
|
910
|
+
server.registerTool("strata_referral_link", {
|
|
911
|
+
title: "Link a Strata referral",
|
|
912
|
+
description: "Prepare or submit an externally authorized referral link for a new wallet.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
915
|
+
referralCode: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/),
|
|
916
|
+
authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
|
|
917
|
+
},
|
|
918
|
+
annotations: {
|
|
919
|
+
readOnlyHint: false,
|
|
920
|
+
destructiveHint: false,
|
|
921
|
+
idempotentHint: false,
|
|
922
|
+
openWorldHint: true,
|
|
923
|
+
},
|
|
924
|
+
}, async ({ walletAddress, referralCode, authorizationSignature }) => {
|
|
925
|
+
if (authorizationSignature === undefined) {
|
|
926
|
+
const payload = platformClient.referrals.linkAuthorizationPayload(referralCode);
|
|
927
|
+
return toolResult({
|
|
928
|
+
wallet_address: walletAddress,
|
|
929
|
+
authorization_payload_base64: Buffer.from(payload).toString("base64"),
|
|
930
|
+
}, "Have the referred wallet sign this payload externally, then call again with its hex signature.");
|
|
931
|
+
}
|
|
932
|
+
const response = await platformClient.referrals.link({
|
|
933
|
+
walletAddress,
|
|
934
|
+
referralCode,
|
|
935
|
+
authorizationSignature,
|
|
936
|
+
});
|
|
937
|
+
return toolResult(response, "Referral link is pending the wallet's first fill.");
|
|
938
|
+
});
|
|
939
|
+
server.registerTool("strata_referral_claim", {
|
|
940
|
+
title: "Claim Strata referral rewards",
|
|
941
|
+
description: "Prepare or submit an externally authorized request for currently claimable referral rewards.",
|
|
942
|
+
inputSchema: {
|
|
943
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
944
|
+
payoutWalletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
|
|
945
|
+
authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
|
|
946
|
+
},
|
|
947
|
+
annotations: {
|
|
948
|
+
readOnlyHint: false,
|
|
949
|
+
destructiveHint: false,
|
|
950
|
+
idempotentHint: false,
|
|
951
|
+
openWorldHint: true,
|
|
952
|
+
},
|
|
953
|
+
}, async ({ walletAddress, payoutWalletAddress, authorizationSignature }) => {
|
|
954
|
+
const payout = payoutWalletAddress ?? walletAddress;
|
|
955
|
+
if (authorizationSignature === undefined) {
|
|
956
|
+
const payload = platformClient.referrals.claimAuthorizationPayload(payout);
|
|
957
|
+
return toolResult({
|
|
958
|
+
wallet_address: walletAddress,
|
|
959
|
+
payout_wallet_address: payout,
|
|
960
|
+
authorization_payload_base64: Buffer.from(payload).toString("base64"),
|
|
961
|
+
}, "Have the claiming wallet sign this payload externally, then call again with its hex signature.");
|
|
962
|
+
}
|
|
963
|
+
const response = await platformClient.referrals.claim({
|
|
964
|
+
walletAddress,
|
|
965
|
+
payoutWalletAddress: payout,
|
|
966
|
+
authorizationSignature,
|
|
967
|
+
});
|
|
968
|
+
return toolResult(response, `${response.claimable_atoms} referral reward atoms requested.`);
|
|
969
|
+
});
|
|
970
|
+
server.registerTool("strata_bugs", {
|
|
971
|
+
title: "Strata bug reports",
|
|
972
|
+
description: "Read an owner's redacted bug reports and confirmed points.",
|
|
973
|
+
inputSchema: {
|
|
974
|
+
walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
975
|
+
},
|
|
111
976
|
annotations: {
|
|
112
977
|
readOnlyHint: true,
|
|
113
978
|
destructiveHint: false,
|
|
114
979
|
idempotentHint: true,
|
|
115
980
|
openWorldHint: true,
|
|
116
981
|
},
|
|
117
|
-
}, async (
|
|
982
|
+
}, async ({ walletAddress }) => {
|
|
983
|
+
const response = await platformClient.bugs.read(walletAddress);
|
|
984
|
+
return toolResult(response, `${response.reports.length} redacted bug reports returned.`);
|
|
985
|
+
});
|
|
986
|
+
server.registerTool("strata_bug_submit", {
|
|
987
|
+
title: "Submit Strata bug report",
|
|
988
|
+
description: "Prepare or submit a bug report. Omit authorizationSignature to receive the exact "
|
|
989
|
+
+ "payload for the owner wallet to sign externally; provide that hex signature to submit.",
|
|
990
|
+
inputSchema: {
|
|
991
|
+
ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
992
|
+
message: z.string().trim().min(1).max(2_000),
|
|
993
|
+
authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
|
|
994
|
+
},
|
|
995
|
+
annotations: {
|
|
996
|
+
readOnlyHint: false,
|
|
997
|
+
destructiveHint: false,
|
|
998
|
+
idempotentHint: false,
|
|
999
|
+
openWorldHint: true,
|
|
1000
|
+
},
|
|
1001
|
+
}, async ({ ownerWallet, message, authorizationSignature }) => {
|
|
1002
|
+
if (authorizationSignature === undefined) {
|
|
1003
|
+
const payload = platformClient.bugs.authorizationPayload(message);
|
|
1004
|
+
return toolResult({
|
|
1005
|
+
owner_wallet: ownerWallet,
|
|
1006
|
+
authorization_payload_base64: Buffer.from(payload).toString("base64"),
|
|
1007
|
+
}, "Sign this payload externally, then call strata_bug_submit again with the hex signature.");
|
|
1008
|
+
}
|
|
1009
|
+
const response = await platformClient.bugs.submit({
|
|
1010
|
+
ownerWallet,
|
|
1011
|
+
message,
|
|
1012
|
+
authorizationSignature,
|
|
1013
|
+
});
|
|
1014
|
+
return toolResult(response, `Bug report ${response.bug_id} is pending review.`);
|
|
1015
|
+
});
|
|
118
1016
|
const markets = server.registerTool("strata_markets", {
|
|
119
1017
|
title: "Strata markets",
|
|
120
1018
|
description: "List Strata markets and their current Sonar quote availability.",
|
|
@@ -133,13 +1031,22 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
133
1031
|
},
|
|
134
1032
|
}, async ({ includePaused }) => guardedTool(client, "markets.read", async () => {
|
|
135
1033
|
const response = await client.markets();
|
|
136
|
-
const
|
|
137
|
-
? response
|
|
138
|
-
:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1034
|
+
const visible = includePaused
|
|
1035
|
+
? response.markets
|
|
1036
|
+
: response.markets.filter((market) => market.ready);
|
|
1037
|
+
const identities = await platformMarketIdentities(platformClient);
|
|
1038
|
+
const output = {
|
|
1039
|
+
...response,
|
|
1040
|
+
markets: visible.map((market) => {
|
|
1041
|
+
const identity = identities.get(market.label);
|
|
1042
|
+
return identity === undefined ? market : { ...market, ...identity };
|
|
1043
|
+
}),
|
|
1044
|
+
};
|
|
1045
|
+
const identified = output.markets.filter((market) => "market_id" in market).length;
|
|
1046
|
+
return toolResult(output, `${output.markets.length} Strata markets available`
|
|
1047
|
+
+ (identified > 0
|
|
1048
|
+
? `; ${identified} carry a market_id — pass it as marketId to every by-market tool.`
|
|
1049
|
+
: "."));
|
|
143
1050
|
}));
|
|
144
1051
|
const quote = server.registerTool("strata_quote", {
|
|
145
1052
|
title: "Sonar quote",
|
|
@@ -157,15 +1064,16 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
157
1064
|
.regex(/^[0-9]+$/)
|
|
158
1065
|
.max(20)
|
|
159
1066
|
.describe("Exact input amount in the input token's smallest atomic unit."),
|
|
160
|
-
|
|
1067
|
+
maximumToleranceBps: z
|
|
161
1068
|
.number()
|
|
162
1069
|
.int()
|
|
163
1070
|
.min(0)
|
|
164
1071
|
.max(1_000)
|
|
165
1072
|
.optional()
|
|
166
|
-
.default(
|
|
167
|
-
.describe("
|
|
168
|
-
+ "
|
|
1073
|
+
.default(DEFAULT_MAXIMUM_TOLERANCE_BPS)
|
|
1074
|
+
.describe("The most you accept below the quoted output, in basis points (default 0: the "
|
|
1075
|
+
+ "quoted output exactly). This is YOUR choice. It is not price impact — "
|
|
1076
|
+
+ "price_impact_pct in the response is measured from the book and is not a setting."),
|
|
169
1077
|
},
|
|
170
1078
|
annotations: {
|
|
171
1079
|
readOnlyHint: true,
|
|
@@ -173,18 +1081,89 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
173
1081
|
idempotentHint: false,
|
|
174
1082
|
openWorldHint: true,
|
|
175
1083
|
},
|
|
176
|
-
}, async ({ market, side, amountInAtoms,
|
|
1084
|
+
}, async ({ market, side, amountInAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
177
1085
|
const request = {
|
|
178
1086
|
market,
|
|
179
1087
|
side,
|
|
180
1088
|
amountInAtoms,
|
|
181
|
-
|
|
1089
|
+
maximumToleranceBps,
|
|
182
1090
|
};
|
|
183
1091
|
const response = await client.quote(request);
|
|
184
|
-
return toolResult(response,
|
|
185
|
-
|
|
186
|
-
|
|
1092
|
+
return toolResult(response, quoteSummary(response));
|
|
1093
|
+
}));
|
|
1094
|
+
const exactOutputQuote = server.registerTool("strata_exact_output_quote", {
|
|
1095
|
+
title: "Sonar exact-output quote",
|
|
1096
|
+
description: "Request a short-lived Sonar quote for an exact output amount (for example: buy "
|
|
1097
|
+
+ "1 SOL). Strata inverts its best route and returns the input that delivers it as "
|
|
1098
|
+
+ "amount_in_atoms; minimum_output_atoms is the requested amount lowered by the "
|
|
1099
|
+
+ "optional maximumToleranceBps (default 0: exactly the requested amount or the "
|
|
1100
|
+
+ "execution fails closed). Execute it with the same quote_id flow as strata_quote.",
|
|
1101
|
+
inputSchema: {
|
|
1102
|
+
market: z
|
|
1103
|
+
.string()
|
|
1104
|
+
.min(1)
|
|
1105
|
+
.max(128)
|
|
1106
|
+
.describe("Market label such as SOL/USDC, or its public market ID."),
|
|
1107
|
+
side: z.enum(["buy", "sell"]).describe("Buy or sell the market's base asset."),
|
|
1108
|
+
amountOutAtoms: z
|
|
1109
|
+
.string()
|
|
1110
|
+
.regex(/^[0-9]+$/)
|
|
1111
|
+
.max(20)
|
|
1112
|
+
.describe("Output amount to receive at least, in the output token's smallest atomic unit "
|
|
1113
|
+
+ "(base atoms for a buy, quote atoms for a sell)."),
|
|
1114
|
+
maximumToleranceBps: z
|
|
1115
|
+
.number()
|
|
1116
|
+
.int()
|
|
1117
|
+
.min(0)
|
|
1118
|
+
.max(1_000)
|
|
1119
|
+
.optional()
|
|
1120
|
+
.default(DEFAULT_MAXIMUM_TOLERANCE_BPS)
|
|
1121
|
+
.describe("The most you accept below the quoted output, in basis points (default 0: the "
|
|
1122
|
+
+ "quoted output exactly). This is YOUR choice. It is not price impact — "
|
|
1123
|
+
+ "price_impact_pct in the response is measured from the book and is not a setting."),
|
|
1124
|
+
},
|
|
1125
|
+
annotations: {
|
|
1126
|
+
readOnlyHint: true,
|
|
1127
|
+
destructiveHint: false,
|
|
1128
|
+
idempotentHint: false,
|
|
1129
|
+
openWorldHint: true,
|
|
1130
|
+
},
|
|
1131
|
+
}, async ({ market, side, amountOutAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
1132
|
+
const request = {
|
|
1133
|
+
market,
|
|
1134
|
+
side,
|
|
1135
|
+
amountOutAtoms,
|
|
1136
|
+
maximumToleranceBps,
|
|
1137
|
+
};
|
|
1138
|
+
const response = await client.quote(request);
|
|
1139
|
+
return toolResult(response, quoteSummary(response));
|
|
187
1140
|
}));
|
|
1141
|
+
server.registerTool("strata_swap_quote", {
|
|
1142
|
+
title: "Sonar asset swap quote",
|
|
1143
|
+
description: "Request short-lived exact-input customer economics between two opaque Strata asset IDs.",
|
|
1144
|
+
inputSchema: {
|
|
1145
|
+
inputAssetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
|
|
1146
|
+
outputAssetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
|
|
1147
|
+
amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20),
|
|
1148
|
+
maximumToleranceBps: z.number().int().min(0).max(1_000).optional().default(0),
|
|
1149
|
+
},
|
|
1150
|
+
annotations: {
|
|
1151
|
+
readOnlyHint: true,
|
|
1152
|
+
destructiveHint: false,
|
|
1153
|
+
idempotentHint: false,
|
|
1154
|
+
openWorldHint: true,
|
|
1155
|
+
},
|
|
1156
|
+
}, async ({ inputAssetId, outputAssetId, amountInAtoms, maximumToleranceBps }) => {
|
|
1157
|
+
const response = await platformClient.quotes.swap({
|
|
1158
|
+
inputAssetId,
|
|
1159
|
+
outputAssetId,
|
|
1160
|
+
amountInAtoms,
|
|
1161
|
+
maximumToleranceBps,
|
|
1162
|
+
});
|
|
1163
|
+
return toolResult(response, `Sonar swap quote: ${response.amount_in_consumed_atoms} input atoms for `
|
|
1164
|
+
+ `${response.amount_out_atoms} user-net output atoms; minimum `
|
|
1165
|
+
+ `${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
|
|
1166
|
+
});
|
|
188
1167
|
const executionChallenge = server.registerTool("strata_execution_challenge", {
|
|
189
1168
|
title: "Strata execution challenge",
|
|
190
1169
|
description: "Request canonical quote-bound authorization bytes for the external signer configured by the agent owner.",
|
|
@@ -201,7 +1180,8 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
201
1180
|
.string()
|
|
202
1181
|
.regex(/^[0-9]+$/)
|
|
203
1182
|
.max(20)
|
|
204
|
-
.
|
|
1183
|
+
.optional()
|
|
1184
|
+
.describe("Optional Vault market account sequence as an unsigned decimal string. Omit it and Strata resolves the next sequence from the Vault's confirmed market account."),
|
|
205
1185
|
},
|
|
206
1186
|
annotations: {
|
|
207
1187
|
readOnlyHint: false,
|
|
@@ -215,26 +1195,32 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
215
1195
|
quoteId,
|
|
216
1196
|
ownerWallet,
|
|
217
1197
|
sessionPublicKey,
|
|
218
|
-
accountSequence,
|
|
1198
|
+
...(accountSequence === undefined ? {} : { accountSequence }),
|
|
219
1199
|
};
|
|
220
1200
|
const response = await client.executionChallenge(request);
|
|
221
1201
|
return toolResult(response, `Authorization challenge ${response.challenge_id}; expires at ${response.expires_at_ms}.`);
|
|
222
1202
|
}));
|
|
223
1203
|
const executionPrepare = server.registerTool("strata_execution_prepare", {
|
|
224
1204
|
title: "Prepare Strata execution",
|
|
225
|
-
description: "
|
|
1205
|
+
description: "Prepare a quote-bound partially signed transaction. One signature: pass quoteId + ownerWallet + sessionPublicKey and sign only the returned transaction. (A challengeId + authorizationSignature from strata_execution_challenge is still accepted.)",
|
|
226
1206
|
inputSchema: {
|
|
227
1207
|
market: z.string().min(1).max(128).describe("Market label or public market ID."),
|
|
1208
|
+
quoteId: z.string().regex(/^sq_[0-9a-f]{32}$/).optional().describe("Unexpired Sonar quote ID (direct, one-signature path)."),
|
|
1209
|
+
ownerWallet: z.string().min(32).max(44).optional(),
|
|
1210
|
+
sessionPublicKey: z.string().min(32).max(44).optional(),
|
|
1211
|
+
accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
|
|
228
1212
|
challengeId: z
|
|
229
1213
|
.string()
|
|
230
1214
|
.regex(/^sc_[0-9a-f]{32}$/)
|
|
231
|
-
.
|
|
1215
|
+
.optional()
|
|
1216
|
+
.describe("Execution challenge ID returned by Strata (two-step path)."),
|
|
232
1217
|
authorizationSignature: z
|
|
233
1218
|
.string()
|
|
234
1219
|
.min(1)
|
|
235
1220
|
.max(128)
|
|
236
1221
|
.regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
|
|
237
|
-
.
|
|
1222
|
+
.optional()
|
|
1223
|
+
.describe("Base58 Ed25519 signature made externally over the challenge payload (two-step path)."),
|
|
238
1224
|
},
|
|
239
1225
|
annotations: {
|
|
240
1226
|
readOnlyHint: false,
|
|
@@ -242,14 +1228,28 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
242
1228
|
idempotentHint: false,
|
|
243
1229
|
openWorldHint: true,
|
|
244
1230
|
},
|
|
245
|
-
}, async ({ market, challengeId, authorizationSignature }) => guardedTool(client, "trade.prepare", async () => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
challengeId
|
|
249
|
-
|
|
250
|
-
|
|
1231
|
+
}, async ({ market, quoteId, ownerWallet, sessionPublicKey, accountSequence, challengeId, authorizationSignature }) => guardedTool(client, "trade.prepare", async () => {
|
|
1232
|
+
let request;
|
|
1233
|
+
if (challengeId !== undefined || authorizationSignature !== undefined) {
|
|
1234
|
+
if (challengeId === undefined || authorizationSignature === undefined) {
|
|
1235
|
+
return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
|
|
1236
|
+
}
|
|
1237
|
+
request = { market, challengeId, authorizationSignature };
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
if (quoteId === undefined || ownerWallet === undefined || sessionPublicKey === undefined) {
|
|
1241
|
+
return toolError("invalid_request", "Pass quoteId, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
|
|
1242
|
+
}
|
|
1243
|
+
request = {
|
|
1244
|
+
market,
|
|
1245
|
+
quoteId,
|
|
1246
|
+
ownerWallet,
|
|
1247
|
+
sessionPublicKey,
|
|
1248
|
+
...(accountSequence === undefined ? {} : { accountSequence }),
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
251
1251
|
const response = await client.executionPrepare(request);
|
|
252
|
-
return toolResult(response, `Prepared execution ${response.execution_id};
|
|
1252
|
+
return toolResult(response, `Prepared execution ${response.execution_id}; verify it, then sign the transaction externally with the session key before ${response.expires_at_ms} and submit.`);
|
|
253
1253
|
}));
|
|
254
1254
|
const executionSubmit = server.registerTool("strata_execution_submit", {
|
|
255
1255
|
title: "Submit Strata execution",
|
|
@@ -297,7 +1297,12 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
297
1297
|
action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
|
|
298
1298
|
ownerWallet: z.string().min(32).max(44),
|
|
299
1299
|
sessionPublicKey: z.string().min(32).max(44),
|
|
300
|
-
accountSequence: z
|
|
1300
|
+
accountSequence: z
|
|
1301
|
+
.string()
|
|
1302
|
+
.regex(/^[0-9]+$/)
|
|
1303
|
+
.max(20)
|
|
1304
|
+
.optional()
|
|
1305
|
+
.describe("Optional Vault market account sequence. Omit it and Strata resolves the next sequence from the Vault's confirmed market account."),
|
|
301
1306
|
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
302
1307
|
side: z.enum(["buy", "sell"]).optional(),
|
|
303
1308
|
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
@@ -322,124 +1327,45 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
322
1327
|
openWorldHint: true,
|
|
323
1328
|
},
|
|
324
1329
|
}, async (args) => guardedTool(client, "orders.prepare", async () => {
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|| args.side === undefined
|
|
330
|
-
|| args.orderType === undefined
|
|
331
|
-
|| args.limitPriceAtoms === undefined
|
|
332
|
-
|| args.sizeAtoms === undefined) {
|
|
333
|
-
return toolError("invalid_request", "Place requires accountSequence, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
|
|
334
|
-
}
|
|
335
|
-
request = {
|
|
336
|
-
action: "place",
|
|
337
|
-
ownerWallet: args.ownerWallet,
|
|
338
|
-
sessionPublicKey: args.sessionPublicKey,
|
|
339
|
-
accountSequence: args.accountSequence,
|
|
340
|
-
clientOrderId: args.clientOrderId,
|
|
341
|
-
side: args.side,
|
|
342
|
-
orderType: args.orderType,
|
|
343
|
-
limitPriceAtoms: args.limitPriceAtoms,
|
|
344
|
-
sizeAtoms: args.sizeAtoms,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
else if (args.action === "cancel") {
|
|
348
|
-
if (args.orderId === undefined) {
|
|
349
|
-
return toolError("invalid_request", "Cancel requires orderId.", false);
|
|
350
|
-
}
|
|
351
|
-
request = {
|
|
352
|
-
action: "cancel",
|
|
353
|
-
ownerWallet: args.ownerWallet,
|
|
354
|
-
sessionPublicKey: args.sessionPublicKey,
|
|
355
|
-
orderId: args.orderId,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
else if (args.action === "cancel_all") {
|
|
359
|
-
request = {
|
|
360
|
-
action: "cancel_all",
|
|
361
|
-
ownerWallet: args.ownerWallet,
|
|
362
|
-
sessionPublicKey: args.sessionPublicKey,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
else if (args.action === "replace") {
|
|
366
|
-
if (args.orderId === undefined
|
|
367
|
-
|| args.accountSequence === undefined
|
|
368
|
-
|| args.clientOrderId === undefined
|
|
369
|
-
|| args.side === undefined
|
|
370
|
-
|| args.orderType === undefined
|
|
371
|
-
|| args.limitPriceAtoms === undefined
|
|
372
|
-
|| args.sizeAtoms === undefined) {
|
|
373
|
-
return toolError("invalid_request", "Replace requires orderId, accountSequence, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
|
|
374
|
-
}
|
|
375
|
-
request = {
|
|
376
|
-
action: "replace",
|
|
377
|
-
ownerWallet: args.ownerWallet,
|
|
378
|
-
sessionPublicKey: args.sessionPublicKey,
|
|
379
|
-
orderId: args.orderId,
|
|
380
|
-
accountSequence: args.accountSequence,
|
|
381
|
-
clientOrderId: args.clientOrderId,
|
|
382
|
-
side: args.side,
|
|
383
|
-
orderType: args.orderType,
|
|
384
|
-
limitPriceAtoms: args.limitPriceAtoms,
|
|
385
|
-
sizeAtoms: args.sizeAtoms,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
if (args.operations === undefined) {
|
|
390
|
-
return toolError("invalid_request", "Batch requires operations.", false);
|
|
391
|
-
}
|
|
392
|
-
const operations = [];
|
|
393
|
-
for (const operation of args.operations) {
|
|
394
|
-
if (operation.action === "cancel") {
|
|
395
|
-
if (operation.orderId === undefined) {
|
|
396
|
-
return toolError("invalid_request", "Batch cancel requires orderId.", false);
|
|
397
|
-
}
|
|
398
|
-
operations.push({ action: "cancel", orderId: operation.orderId });
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (operation.accountSequence === undefined
|
|
402
|
-
|| operation.clientOrderId === undefined
|
|
403
|
-
|| operation.side === undefined
|
|
404
|
-
|| operation.orderType === undefined
|
|
405
|
-
|| operation.limitPriceAtoms === undefined
|
|
406
|
-
|| operation.sizeAtoms === undefined
|
|
407
|
-
|| (operation.action === "replace" && operation.orderId === undefined)) {
|
|
408
|
-
return toolError("invalid_request", `Batch ${operation.action} has incomplete fields.`, false);
|
|
409
|
-
}
|
|
410
|
-
const place = {
|
|
411
|
-
accountSequence: operation.accountSequence,
|
|
412
|
-
clientOrderId: operation.clientOrderId,
|
|
413
|
-
side: operation.side,
|
|
414
|
-
orderType: operation.orderType,
|
|
415
|
-
limitPriceAtoms: operation.limitPriceAtoms,
|
|
416
|
-
sizeAtoms: operation.sizeAtoms,
|
|
417
|
-
};
|
|
418
|
-
operations.push(operation.action === "replace"
|
|
419
|
-
? { action: "replace", orderId: operation.orderId, ...place }
|
|
420
|
-
: { action: "place", ...place });
|
|
421
|
-
}
|
|
422
|
-
request = {
|
|
423
|
-
action: "batch",
|
|
424
|
-
ownerWallet: args.ownerWallet,
|
|
425
|
-
sessionPublicKey: args.sessionPublicKey,
|
|
426
|
-
operations,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
1330
|
+
const mapped = orderOperationFromArgs(args);
|
|
1331
|
+
if ("content" in mapped)
|
|
1332
|
+
return mapped;
|
|
1333
|
+
const request = mapped;
|
|
429
1334
|
const response = await platformClient.orders.challenge(args.marketId, request);
|
|
430
1335
|
return toolResult(response, `Order challenge ${response.challenge_id} binds ${response.order_ids.length} opaque order ID(s); expires at ${response.expires_at_ms}.`);
|
|
431
1336
|
}));
|
|
432
1337
|
const orderPrepare = server.registerTool("strata_order_prepare", {
|
|
433
1338
|
title: "Prepare Strata order control",
|
|
434
|
-
description: "
|
|
1339
|
+
description: "Prepare an immutable partially signed order-control transaction. One signature: pass the operation itself (same fields as strata_order_challenge) and sign only the returned transaction with the session key. (A challengeId + authorizationSignature from strata_order_challenge is still accepted.)",
|
|
435
1340
|
inputSchema: {
|
|
436
1341
|
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
437
|
-
challengeId: z.string().regex(/^oc_[0-9a-f]{32}$/),
|
|
1342
|
+
challengeId: z.string().regex(/^oc_[0-9a-f]{32}$/).optional(),
|
|
438
1343
|
authorizationSignature: z
|
|
439
1344
|
.string()
|
|
440
1345
|
.min(64)
|
|
441
1346
|
.max(88)
|
|
442
|
-
.regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
|
|
1347
|
+
.regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
|
|
1348
|
+
.optional(),
|
|
1349
|
+
action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]).optional(),
|
|
1350
|
+
ownerWallet: z.string().min(32).max(44).optional(),
|
|
1351
|
+
sessionPublicKey: z.string().min(32).max(44).optional(),
|
|
1352
|
+
accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
|
|
1353
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1354
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1355
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1356
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1357
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1358
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1359
|
+
operations: z.array(z.object({
|
|
1360
|
+
action: z.enum(["place", "cancel", "replace"]),
|
|
1361
|
+
accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
|
|
1362
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1363
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1364
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1365
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1366
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1367
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1368
|
+
}).strict()).min(1).max(6).optional(),
|
|
443
1369
|
},
|
|
444
1370
|
annotations: {
|
|
445
1371
|
readOnlyHint: false,
|
|
@@ -447,12 +1373,30 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
447
1373
|
idempotentHint: false,
|
|
448
1374
|
openWorldHint: true,
|
|
449
1375
|
},
|
|
450
|
-
}, async (
|
|
451
|
-
|
|
452
|
-
challengeId
|
|
453
|
-
|
|
1376
|
+
}, async (args) => guardedTool(client, "orders.prepare", async () => {
|
|
1377
|
+
if (args.challengeId !== undefined || args.authorizationSignature !== undefined) {
|
|
1378
|
+
if (args.challengeId === undefined || args.authorizationSignature === undefined) {
|
|
1379
|
+
return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
|
|
1380
|
+
}
|
|
1381
|
+
const response = await platformClient.orders.prepare(args.marketId, {
|
|
1382
|
+
challengeId: args.challengeId,
|
|
1383
|
+
authorizationSignature: args.authorizationSignature,
|
|
1384
|
+
});
|
|
1385
|
+
return toolResult(response, `Prepared ${response.action} control ${response.order_control_id}; externally verify and sign before ${response.expires_at_ms}.`);
|
|
1386
|
+
}
|
|
1387
|
+
if (args.action === undefined || args.ownerWallet === undefined || args.sessionPublicKey === undefined) {
|
|
1388
|
+
return toolError("invalid_request", "Pass action, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
|
|
1389
|
+
}
|
|
1390
|
+
const mapped = orderOperationFromArgs({
|
|
1391
|
+
...args,
|
|
1392
|
+
action: args.action,
|
|
1393
|
+
ownerWallet: args.ownerWallet,
|
|
1394
|
+
sessionPublicKey: args.sessionPublicKey,
|
|
454
1395
|
});
|
|
455
|
-
|
|
1396
|
+
if ("content" in mapped)
|
|
1397
|
+
return mapped;
|
|
1398
|
+
const response = await platformClient.orders.prepare(args.marketId, { operation: mapped });
|
|
1399
|
+
return toolResult(response, `Prepared ${response.action} control ${response.order_control_id} — one signature: verify it, sign the transaction externally with the session key before ${response.expires_at_ms}, then submit.`);
|
|
456
1400
|
}));
|
|
457
1401
|
const orderSubmit = server.registerTool("strata_order_submit", {
|
|
458
1402
|
title: "Submit Strata order control",
|
|
@@ -499,9 +1443,11 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
499
1443
|
const response = await platformClient.orders.status(marketId, { orderControlId, idempotencyKey });
|
|
500
1444
|
return toolResult(response, `Order control ${response.order_control_id} is ${response.status}.`);
|
|
501
1445
|
}));
|
|
1446
|
+
registerAutonomyTools(server, client, platformClient, options.sessionAutonomy, () => typeof Date !== "undefined" ? Date.now() : 0);
|
|
502
1447
|
const handles = {
|
|
503
1448
|
markets,
|
|
504
1449
|
quote,
|
|
1450
|
+
exactOutputQuote,
|
|
505
1451
|
executionChallenge,
|
|
506
1452
|
executionPrepare,
|
|
507
1453
|
executionSubmit,
|
|
@@ -533,9 +1479,257 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
533
1479
|
},
|
|
534
1480
|
};
|
|
535
1481
|
}
|
|
1482
|
+
function registerAutonomyTools(server, client, platformClient, autonomy, nowMs) {
|
|
1483
|
+
// Always present, always read-only: the agent may show the slider and offer
|
|
1484
|
+
// to change it, but nothing it calls can raise its own autonomy.
|
|
1485
|
+
server.registerTool("strata_autonomy", {
|
|
1486
|
+
title: "Strata session autonomy",
|
|
1487
|
+
description: "Read how much this MCP may finish by itself: the autonomy level (ask / limits / instant), "
|
|
1488
|
+
+ "any USD ceilings, and how to change it. Read-only — the level is the user's, set out-of-band "
|
|
1489
|
+
+ "(the Agents page or the MCP's own env), never by an agent.",
|
|
1490
|
+
inputSchema: {},
|
|
1491
|
+
annotations: {
|
|
1492
|
+
readOnlyHint: true,
|
|
1493
|
+
destructiveHint: false,
|
|
1494
|
+
idempotentHint: true,
|
|
1495
|
+
openWorldHint: false,
|
|
1496
|
+
},
|
|
1497
|
+
}, async () => {
|
|
1498
|
+
const howToChange = {
|
|
1499
|
+
level_env: "STRATA_AUTONOMY = ask | limits | instant",
|
|
1500
|
+
per_trade_env: "STRATA_AUTONOMY_MAX_USD_PER_TRADE",
|
|
1501
|
+
per_day_env: "STRATA_AUTONOMY_MAX_USD_PER_DAY",
|
|
1502
|
+
markets_env: "STRATA_AUTONOMY_MARKETS (comma-separated opaque market IDs)",
|
|
1503
|
+
session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
|
|
1504
|
+
agents_page: "https://stratabook.app/agents",
|
|
1505
|
+
note: "Only the user changes these; an agent can offer but never raise its own level.",
|
|
1506
|
+
};
|
|
1507
|
+
if (!autonomy) {
|
|
1508
|
+
return toolResult({ session_configured: false, level: "ask", how_to_change: howToChange }, "Autonomy: ask (no session key configured). I can prepare trades for you to sign, "
|
|
1509
|
+
+ "but I cannot sign any myself. To let me trade unattended, register a Vault session "
|
|
1510
|
+
+ "key on the Agents page and set STRATA_SESSION_SECRET_KEY (+ STRATA_OWNER_WALLET), "
|
|
1511
|
+
+ "then choose STRATA_AUTONOMY=limits or instant.");
|
|
1512
|
+
}
|
|
1513
|
+
const { config } = autonomy;
|
|
1514
|
+
const spentToday = autonomy.dailyBudget.spentToday(nowMs());
|
|
1515
|
+
const state = {
|
|
1516
|
+
session_configured: true,
|
|
1517
|
+
wallet_address: autonomy.ownerWallet,
|
|
1518
|
+
session_public_key: autonomy.signer.publicKey,
|
|
1519
|
+
level: config.level,
|
|
1520
|
+
max_usd_per_trade: config.maxUsdPerTrade ?? null,
|
|
1521
|
+
max_usd_per_day: config.maxUsdPerDay ?? null,
|
|
1522
|
+
spent_today_usd: Number(spentToday.toFixed(2)),
|
|
1523
|
+
remaining_today_usd: config.maxUsdPerDay === undefined
|
|
1524
|
+
? null
|
|
1525
|
+
: Number(Math.max(0, config.maxUsdPerDay - spentToday).toFixed(2)),
|
|
1526
|
+
allowed_market_ids: config.allowedMarketIds ?? null,
|
|
1527
|
+
how_to_change: howToChange,
|
|
1528
|
+
};
|
|
1529
|
+
const summary = config.level === "instant"
|
|
1530
|
+
? "Autonomy: instant — I trade within your on-chain session caps without asking."
|
|
1531
|
+
: config.level === "limits"
|
|
1532
|
+
? `Autonomy: limits — I trade instantly up to ${config.maxUsdPerTrade !== undefined ? "$" + config.maxUsdPerTrade + "/trade" : "no per-trade cap"}`
|
|
1533
|
+
+ `${config.maxUsdPerDay !== undefined ? ", $" + config.maxUsdPerDay + "/day" : ""}; above that I stop and ask.`
|
|
1534
|
+
: "Autonomy: ask — I prepare trades but never sign them; you sign each one.";
|
|
1535
|
+
return toolResult(state, summary);
|
|
1536
|
+
});
|
|
1537
|
+
if (!autonomy)
|
|
1538
|
+
return;
|
|
1539
|
+
const resolver = new MarketMetaResolver(platformClient, async () => (await client.markets()).markets, nowMs);
|
|
1540
|
+
const markFor = async (marketId) => {
|
|
1541
|
+
const mark = await platformClient.marketData.mark(marketId);
|
|
1542
|
+
return {
|
|
1543
|
+
price_atoms_per_base_unit: mark.price_atoms_per_base_unit,
|
|
1544
|
+
quote_decimals: mark.quote_decimals,
|
|
1545
|
+
stale: mark.stale,
|
|
1546
|
+
};
|
|
1547
|
+
};
|
|
1548
|
+
const refuse = (reason, prepared, summary) => toolResult({ executed: false, reason, prepared }, summary);
|
|
1549
|
+
// ── one-shot immediate execution from a fresh Sonar quote ─────────────────
|
|
1550
|
+
server.registerTool("strata_execute_quote", {
|
|
1551
|
+
title: "Execute a Strata quote",
|
|
1552
|
+
description: "Take a fresh Sonar quote and, within the autonomy slider, sign it with the session key and "
|
|
1553
|
+
+ "submit it in one call. Under \"ask\" (or over a \"limits\" ceiling) it does not sign — it returns "
|
|
1554
|
+
+ "the quote and asks you to sign or raise the slider.",
|
|
1555
|
+
inputSchema: {
|
|
1556
|
+
market: z.string().min(2).max(64).describe("Market label such as SOL/USDC, or its public market ID."),
|
|
1557
|
+
side: z.enum(["buy", "sell"]),
|
|
1558
|
+
amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20),
|
|
1559
|
+
toleranceBps: z.number().int().min(0).max(1_000).optional(),
|
|
1560
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1561
|
+
},
|
|
1562
|
+
annotations: {
|
|
1563
|
+
readOnlyHint: false,
|
|
1564
|
+
destructiveHint: true,
|
|
1565
|
+
idempotentHint: false,
|
|
1566
|
+
openWorldHint: true,
|
|
1567
|
+
},
|
|
1568
|
+
}, async (args) => guardedTool(client, "trade.submit", async () => {
|
|
1569
|
+
const quote = await client.quote({
|
|
1570
|
+
market: args.market,
|
|
1571
|
+
side: args.side,
|
|
1572
|
+
amountInAtoms: args.amountInAtoms,
|
|
1573
|
+
...(args.toleranceBps === undefined ? {} : { toleranceBps: args.toleranceBps }),
|
|
1574
|
+
});
|
|
1575
|
+
const sonar = (await client.markets()).markets.find((market) => market.market_pda === quote.market_id);
|
|
1576
|
+
const notional = sonar
|
|
1577
|
+
? quoteNotionalUsd(quote.side, quote.amount_in_atoms, quote.minimum_output_atoms, sonar.quote_decimals)
|
|
1578
|
+
: null;
|
|
1579
|
+
const marketId = sonar ? await resolver.idForLabel(sonar.label) : null;
|
|
1580
|
+
const decision = decideAutonomy(autonomy, marketId ?? "", notional, nowMs());
|
|
1581
|
+
if (!decision.allow) {
|
|
1582
|
+
return refuse(decision.reason, quote, decision.reason);
|
|
1583
|
+
}
|
|
1584
|
+
const receipt = await client.executeQuote({
|
|
1585
|
+
quote,
|
|
1586
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1587
|
+
signer: autonomy.signer,
|
|
1588
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1589
|
+
});
|
|
1590
|
+
if (notional !== null)
|
|
1591
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1592
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${quote.side} on ${quote.market_id} as ${receipt.signature}.`);
|
|
1593
|
+
}));
|
|
1594
|
+
// ── one-shot order control (place / cancel / replace / batch) ─────────────
|
|
1595
|
+
server.registerTool("strata_order_execute", {
|
|
1596
|
+
title: "Execute a Strata order control",
|
|
1597
|
+
description: "Place, cancel, replace, or batch orders and, within the autonomy slider, sign with the session "
|
|
1598
|
+
+ "key and submit in one call. Owner wallet and session key come from the configured session. "
|
|
1599
|
+
+ "Under \"ask\" (or over a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
|
|
1600
|
+
inputSchema: {
|
|
1601
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1602
|
+
action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
|
|
1603
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1604
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1605
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1606
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1607
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1608
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1609
|
+
operations: z.array(z.object({
|
|
1610
|
+
action: z.enum(["place", "cancel", "replace"]),
|
|
1611
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1612
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1613
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1614
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1615
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1616
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1617
|
+
}).strict()).min(1).max(6).optional(),
|
|
1618
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1619
|
+
},
|
|
1620
|
+
annotations: {
|
|
1621
|
+
readOnlyHint: false,
|
|
1622
|
+
destructiveHint: true,
|
|
1623
|
+
idempotentHint: false,
|
|
1624
|
+
openWorldHint: true,
|
|
1625
|
+
},
|
|
1626
|
+
}, async (args) => guardedTool(client, "orders.submit", async () => {
|
|
1627
|
+
const challenge = orderOperationFromArgs({
|
|
1628
|
+
...args,
|
|
1629
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1630
|
+
sessionPublicKey: autonomy.signer.publicKey,
|
|
1631
|
+
});
|
|
1632
|
+
if ("content" in challenge)
|
|
1633
|
+
return challenge;
|
|
1634
|
+
// A place/replace risks new base; a cancel reduces it (notional 0).
|
|
1635
|
+
const baseAtoms = (args.action === "place" || args.action === "replace") && args.sizeAtoms !== undefined
|
|
1636
|
+
? BigInt(args.sizeAtoms)
|
|
1637
|
+
: 0n;
|
|
1638
|
+
const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
|
|
1639
|
+
const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
|
|
1640
|
+
if (!decision.allow) {
|
|
1641
|
+
const prepared = await platformClient.orders.prepare(args.marketId, {
|
|
1642
|
+
operation: challenge,
|
|
1643
|
+
});
|
|
1644
|
+
return refuse(decision.reason, prepared, decision.reason);
|
|
1645
|
+
}
|
|
1646
|
+
const { sessionPublicKey: _session, ...operation } = challenge;
|
|
1647
|
+
const receipt = await platformClient.orders.execute(args.marketId, {
|
|
1648
|
+
operation: operation,
|
|
1649
|
+
signer: autonomy.signer,
|
|
1650
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1651
|
+
});
|
|
1652
|
+
if (notional !== null)
|
|
1653
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1654
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${receipt.action} control ${receipt.order_control_id} as ${receipt.signature}.`);
|
|
1655
|
+
}));
|
|
1656
|
+
// ── one-shot TWAP (schedule / cancel) ─────────────────────────────────────
|
|
1657
|
+
server.registerTool("strata_twap_execute", {
|
|
1658
|
+
title: "Execute a Strata TWAP",
|
|
1659
|
+
description: "Schedule or cancel a TWAP and, within the autonomy slider, sign with the session key and submit "
|
|
1660
|
+
+ "in one call. Owner wallet and session key come from the configured session. Under \"ask\" (or over "
|
|
1661
|
+
+ "a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
|
|
1662
|
+
inputSchema: {
|
|
1663
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1664
|
+
action: z.enum(["place", "cancel"]),
|
|
1665
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1666
|
+
totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
1667
|
+
slicesTotal: z.number().int().min(2).max(120).optional(),
|
|
1668
|
+
maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
|
|
1669
|
+
intervalSlots: z.number().int().min(25).max(4_500).optional(),
|
|
1670
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
1671
|
+
twapId: z.string().regex(/^twap_[0-9a-f]{32}$/).optional(),
|
|
1672
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1673
|
+
},
|
|
1674
|
+
annotations: {
|
|
1675
|
+
readOnlyHint: false,
|
|
1676
|
+
destructiveHint: true,
|
|
1677
|
+
idempotentHint: false,
|
|
1678
|
+
openWorldHint: true,
|
|
1679
|
+
},
|
|
1680
|
+
}, async (args) => guardedTool(client, "algos.submit", async () => {
|
|
1681
|
+
let operation;
|
|
1682
|
+
if (args.action === "cancel") {
|
|
1683
|
+
if (args.twapId === undefined)
|
|
1684
|
+
return toolError("invalid_request", "Cancel requires twapId.", false);
|
|
1685
|
+
operation = { action: "cancel", ownerWallet: autonomy.ownerWallet, twapId: args.twapId };
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
if (args.side === undefined
|
|
1689
|
+
|| args.totalSizeAtoms === undefined
|
|
1690
|
+
|| args.slicesTotal === undefined
|
|
1691
|
+
|| args.maximumToleranceBps === undefined
|
|
1692
|
+
|| args.intervalSlots === undefined
|
|
1693
|
+
|| args.limitPriceAtoms === undefined) {
|
|
1694
|
+
return toolError("invalid_request", "Place requires side, totalSizeAtoms, slicesTotal, maximumToleranceBps, intervalSlots, and limitPriceAtoms.", false);
|
|
1695
|
+
}
|
|
1696
|
+
operation = {
|
|
1697
|
+
action: "place",
|
|
1698
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1699
|
+
side: args.side,
|
|
1700
|
+
totalSizeAtoms: args.totalSizeAtoms,
|
|
1701
|
+
slicesTotal: args.slicesTotal,
|
|
1702
|
+
maximumToleranceBps: args.maximumToleranceBps,
|
|
1703
|
+
intervalSlots: args.intervalSlots,
|
|
1704
|
+
limitPriceAtoms: args.limitPriceAtoms,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
const baseAtoms = args.action === "place" && args.totalSizeAtoms !== undefined ? BigInt(args.totalSizeAtoms) : 0n;
|
|
1708
|
+
const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
|
|
1709
|
+
const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
|
|
1710
|
+
if (!decision.allow) {
|
|
1711
|
+
const prepared = await platformClient.algos.prepare(args.marketId, {
|
|
1712
|
+
operation: {
|
|
1713
|
+
...operation,
|
|
1714
|
+
sessionPublicKey: autonomy.signer.publicKey,
|
|
1715
|
+
},
|
|
1716
|
+
});
|
|
1717
|
+
return refuse(decision.reason, prepared, decision.reason);
|
|
1718
|
+
}
|
|
1719
|
+
const receipt = await platformClient.algos.execute(args.marketId, {
|
|
1720
|
+
operation,
|
|
1721
|
+
signer: autonomy.signer,
|
|
1722
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1723
|
+
});
|
|
1724
|
+
if (notional !== null)
|
|
1725
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1726
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed TWAP ${receipt.twap_control_id} as ${receipt.signature}.`);
|
|
1727
|
+
}));
|
|
1728
|
+
}
|
|
536
1729
|
function applyCapabilityCatalog(handles, catalog) {
|
|
537
1730
|
setToolEnabled(handles.markets, capabilityAvailable(catalog, "markets.read"));
|
|
538
1731
|
setToolEnabled(handles.quote, capabilityAvailable(catalog, "quotes.read"));
|
|
1732
|
+
setToolEnabled(handles.exactOutputQuote, capabilityAvailable(catalog, "quotes.read"));
|
|
539
1733
|
setToolEnabled(handles.executionChallenge, capabilityAvailable(catalog, "trade.prepare"));
|
|
540
1734
|
setToolEnabled(handles.executionPrepare, capabilityAvailable(catalog, "trade.prepare"));
|
|
541
1735
|
setToolEnabled(handles.executionSubmit, capabilityAvailable(catalog, "trade.submit"));
|
|
@@ -565,6 +1759,16 @@ async function guardedTool(client, capabilityId, operation) {
|
|
|
565
1759
|
return toolError("request_failed", safeMessage(error), true);
|
|
566
1760
|
}
|
|
567
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* One line that keeps the two numbers apart: price impact is measured from the
|
|
1764
|
+
* book; the tolerance is the caller's own floor.
|
|
1765
|
+
*/
|
|
1766
|
+
function quoteSummary(response) {
|
|
1767
|
+
return (`Sonar ${response.side} quote: ${response.amount_in_consumed_atoms} input atoms for `
|
|
1768
|
+
+ `${response.amount_out_atoms} user-net output atoms; price impact ${response.price_impact_pct}% `
|
|
1769
|
+
+ `(measured from the book); your tolerance ${response.maximum_tolerance_bps} bps, so the `
|
|
1770
|
+
+ `user-net floor is ${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
|
|
1771
|
+
}
|
|
568
1772
|
function toolResult(value, summary) {
|
|
569
1773
|
return {
|
|
570
1774
|
content: [
|