@stratabook/mcp 0.1.12 → 0.2.1
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 +1547 -154
- 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.",
|
|
@@ -115,6 +271,748 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
115
271
|
openWorldHint: true,
|
|
116
272
|
},
|
|
117
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
|
+
},
|
|
900
|
+
annotations: {
|
|
901
|
+
readOnlyHint: true,
|
|
902
|
+
destructiveHint: false,
|
|
903
|
+
idempotentHint: true,
|
|
904
|
+
openWorldHint: true,
|
|
905
|
+
},
|
|
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
|
+
},
|
|
976
|
+
annotations: {
|
|
977
|
+
readOnlyHint: true,
|
|
978
|
+
destructiveHint: false,
|
|
979
|
+
idempotentHint: true,
|
|
980
|
+
openWorldHint: true,
|
|
981
|
+
},
|
|
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.",
|
|
@@ -122,29 +1020,84 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
122
1020
|
includePaused: z
|
|
123
1021
|
.boolean()
|
|
124
1022
|
.optional()
|
|
125
|
-
.default(false)
|
|
126
|
-
.describe("Include markets whose public Sonar quote operation is paused."),
|
|
1023
|
+
.default(false)
|
|
1024
|
+
.describe("Include markets whose public Sonar quote operation is paused."),
|
|
1025
|
+
},
|
|
1026
|
+
annotations: {
|
|
1027
|
+
readOnlyHint: true,
|
|
1028
|
+
destructiveHint: false,
|
|
1029
|
+
idempotentHint: true,
|
|
1030
|
+
openWorldHint: true,
|
|
1031
|
+
},
|
|
1032
|
+
}, async ({ includePaused }) => guardedTool(client, "markets.read", async () => {
|
|
1033
|
+
const response = await client.markets();
|
|
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
|
+
: "."));
|
|
1050
|
+
}));
|
|
1051
|
+
const quote = server.registerTool("strata_quote", {
|
|
1052
|
+
title: "Sonar quote",
|
|
1053
|
+
description: "Request a short-lived Sonar quote for a Strata market. Returns expected "
|
|
1054
|
+
+ "output, minimum output, fees, price impact, and expiry.",
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
market: z
|
|
1057
|
+
.string()
|
|
1058
|
+
.min(1)
|
|
1059
|
+
.max(128)
|
|
1060
|
+
.describe("Market label such as SOL/USDC, or its public market ID."),
|
|
1061
|
+
side: z.enum(["buy", "sell"]).describe("Buy or sell the market's base asset."),
|
|
1062
|
+
amountInAtoms: z
|
|
1063
|
+
.string()
|
|
1064
|
+
.regex(/^[0-9]+$/)
|
|
1065
|
+
.max(20)
|
|
1066
|
+
.describe("Exact input amount in the input token's smallest atomic unit."),
|
|
1067
|
+
maximumToleranceBps: z
|
|
1068
|
+
.number()
|
|
1069
|
+
.int()
|
|
1070
|
+
.min(0)
|
|
1071
|
+
.max(1_000)
|
|
1072
|
+
.optional()
|
|
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."),
|
|
127
1077
|
},
|
|
128
1078
|
annotations: {
|
|
129
1079
|
readOnlyHint: true,
|
|
130
1080
|
destructiveHint: false,
|
|
131
|
-
idempotentHint:
|
|
1081
|
+
idempotentHint: false,
|
|
132
1082
|
openWorldHint: true,
|
|
133
1083
|
},
|
|
134
|
-
}, async ({
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return toolResult(
|
|
1084
|
+
}, async ({ market, side, amountInAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
1085
|
+
const request = {
|
|
1086
|
+
market,
|
|
1087
|
+
side,
|
|
1088
|
+
amountInAtoms,
|
|
1089
|
+
maximumToleranceBps,
|
|
1090
|
+
};
|
|
1091
|
+
const response = await client.quote(request);
|
|
1092
|
+
return toolResult(response, quoteSummary(response));
|
|
143
1093
|
}));
|
|
144
|
-
const
|
|
145
|
-
title: "Sonar quote",
|
|
146
|
-
description: "Request a short-lived Sonar quote for
|
|
147
|
-
+ "
|
|
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.",
|
|
148
1101
|
inputSchema: {
|
|
149
1102
|
market: z
|
|
150
1103
|
.string()
|
|
@@ -152,20 +1105,22 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
152
1105
|
.max(128)
|
|
153
1106
|
.describe("Market label such as SOL/USDC, or its public market ID."),
|
|
154
1107
|
side: z.enum(["buy", "sell"]).describe("Buy or sell the market's base asset."),
|
|
155
|
-
|
|
1108
|
+
amountOutAtoms: z
|
|
156
1109
|
.string()
|
|
157
1110
|
.regex(/^[0-9]+$/)
|
|
158
1111
|
.max(20)
|
|
159
|
-
.describe("
|
|
160
|
-
|
|
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
|
|
161
1115
|
.number()
|
|
162
1116
|
.int()
|
|
163
1117
|
.min(0)
|
|
164
1118
|
.max(1_000)
|
|
165
1119
|
.optional()
|
|
166
|
-
.default(
|
|
167
|
-
.describe("
|
|
168
|
-
+ "
|
|
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."),
|
|
169
1124
|
},
|
|
170
1125
|
annotations: {
|
|
171
1126
|
readOnlyHint: true,
|
|
@@ -173,18 +1128,42 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
173
1128
|
idempotentHint: false,
|
|
174
1129
|
openWorldHint: true,
|
|
175
1130
|
},
|
|
176
|
-
}, async ({ market, side,
|
|
1131
|
+
}, async ({ market, side, amountOutAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
177
1132
|
const request = {
|
|
178
1133
|
market,
|
|
179
1134
|
side,
|
|
180
|
-
|
|
181
|
-
|
|
1135
|
+
amountOutAtoms,
|
|
1136
|
+
maximumToleranceBps,
|
|
182
1137
|
};
|
|
183
1138
|
const response = await client.quote(request);
|
|
184
|
-
return toolResult(response,
|
|
185
|
-
+ `for ${response.amount_out_atoms} user-net output atoms; user-net minimum `
|
|
186
|
-
+ `${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
|
|
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,190 @@ 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
|
+
const makerStrandPrepare = server.registerTool("strata_market_making_strand_prepare", {
|
|
1447
|
+
title: "Prepare Strata Strand control",
|
|
1448
|
+
description: "Build one exact unsigned maker-owned Strand transaction. Verify and sign it externally with the maker wallet, then submit it with the Strand submit tool.",
|
|
1449
|
+
inputSchema: {
|
|
1450
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1451
|
+
action: z.enum(["upsert", "recenter", "set_enabled", "cancel"]),
|
|
1452
|
+
makerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
1453
|
+
enabled: z.boolean().optional(),
|
|
1454
|
+
asyncOnly: z.boolean().optional(),
|
|
1455
|
+
syncSpreadTicks: z.number().int().min(0).max(65_535).optional(),
|
|
1456
|
+
midPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1457
|
+
maxExposureBaseLots: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1458
|
+
bidOffsetsTicks: z.array(z.number().int().min(0).max(65_535)).length(16).optional(),
|
|
1459
|
+
askOffsetsTicks: z.array(z.number().int().min(0).max(65_535)).length(16).optional(),
|
|
1460
|
+
bidSizesBaseLots: z.array(z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20)).length(16).optional(),
|
|
1461
|
+
askSizesBaseLots: z.array(z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20)).length(16).optional(),
|
|
1462
|
+
newMidPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1463
|
+
validUntilSlot: z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20).optional(),
|
|
1464
|
+
},
|
|
1465
|
+
annotations: {
|
|
1466
|
+
readOnlyHint: false,
|
|
1467
|
+
destructiveHint: true,
|
|
1468
|
+
idempotentHint: false,
|
|
1469
|
+
openWorldHint: true,
|
|
1470
|
+
},
|
|
1471
|
+
}, async (args) => guardedTool(client, "mm.strand.manage", async () => {
|
|
1472
|
+
let request;
|
|
1473
|
+
if (args.action === "upsert") {
|
|
1474
|
+
if (args.enabled === undefined || args.asyncOnly === undefined
|
|
1475
|
+
|| args.syncSpreadTicks === undefined || args.midPriceAtoms === undefined
|
|
1476
|
+
|| args.maxExposureBaseLots === undefined || args.bidOffsetsTicks === undefined
|
|
1477
|
+
|| args.askOffsetsTicks === undefined || args.bidSizesBaseLots === undefined
|
|
1478
|
+
|| args.askSizesBaseLots === undefined || args.validUntilSlot === undefined) {
|
|
1479
|
+
return toolError("invalid_request", "Strand upsert requires every level, exposure, midpoint, spread, flag, and expiry field.", false);
|
|
1480
|
+
}
|
|
1481
|
+
request = {
|
|
1482
|
+
action: "upsert",
|
|
1483
|
+
makerWallet: args.makerWallet,
|
|
1484
|
+
enabled: args.enabled,
|
|
1485
|
+
asyncOnly: args.asyncOnly,
|
|
1486
|
+
syncSpreadTicks: args.syncSpreadTicks,
|
|
1487
|
+
midPriceAtoms: args.midPriceAtoms,
|
|
1488
|
+
maxExposureBaseLots: args.maxExposureBaseLots,
|
|
1489
|
+
bidOffsetsTicks: args.bidOffsetsTicks,
|
|
1490
|
+
askOffsetsTicks: args.askOffsetsTicks,
|
|
1491
|
+
bidSizesBaseLots: args.bidSizesBaseLots,
|
|
1492
|
+
askSizesBaseLots: args.askSizesBaseLots,
|
|
1493
|
+
validUntilSlot: args.validUntilSlot,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
else if (args.action === "recenter") {
|
|
1497
|
+
if (args.newMidPriceAtoms === undefined || args.validUntilSlot === undefined) {
|
|
1498
|
+
return toolError("invalid_request", "Strand recenter requires newMidPriceAtoms and validUntilSlot.", false);
|
|
1499
|
+
}
|
|
1500
|
+
request = {
|
|
1501
|
+
action: "recenter",
|
|
1502
|
+
makerWallet: args.makerWallet,
|
|
1503
|
+
newMidPriceAtoms: args.newMidPriceAtoms,
|
|
1504
|
+
validUntilSlot: args.validUntilSlot,
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
else if (args.action === "set_enabled") {
|
|
1508
|
+
if (args.enabled === undefined) {
|
|
1509
|
+
return toolError("invalid_request", "Strand set_enabled requires enabled.", false);
|
|
1510
|
+
}
|
|
1511
|
+
request = { action: "set_enabled", makerWallet: args.makerWallet, enabled: args.enabled };
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
request = { action: "cancel", makerWallet: args.makerWallet };
|
|
1515
|
+
}
|
|
1516
|
+
const response = await platformClient.marketMaking.strand.prepare(args.marketId, request);
|
|
1517
|
+
return toolResult(response, `Prepared ${response.action} control ${response.maker_control_id}; verify and sign only this transaction with ${response.maker_wallet} before ${response.expires_at_ms}.`);
|
|
1518
|
+
}));
|
|
1519
|
+
const makerStrandSubmit = server.registerTool("strata_market_making_strand_submit", {
|
|
1520
|
+
title: "Submit Strata Strand control",
|
|
1521
|
+
description: "Submit the exact externally maker-signed Strand transaction with a stable retry key.",
|
|
1522
|
+
inputSchema: {
|
|
1523
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1524
|
+
makerControlId: z.string().regex(/^mc_[0-9a-f]{32}$/),
|
|
1525
|
+
signedTransactionBase64: z.string().min(4).max(4_096).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
|
|
1526
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/),
|
|
1527
|
+
},
|
|
1528
|
+
annotations: {
|
|
1529
|
+
readOnlyHint: false,
|
|
1530
|
+
destructiveHint: true,
|
|
1531
|
+
idempotentHint: true,
|
|
1532
|
+
openWorldHint: true,
|
|
1533
|
+
},
|
|
1534
|
+
}, async ({ marketId, makerControlId, signedTransactionBase64, idempotencyKey }) => guardedTool(client, "mm.strand.manage", async () => {
|
|
1535
|
+
const response = await platformClient.marketMaking.strand.submit(marketId, {
|
|
1536
|
+
makerControlId,
|
|
1537
|
+
signedTransactionBase64,
|
|
1538
|
+
idempotencyKey,
|
|
1539
|
+
});
|
|
1540
|
+
return toolResult(response, `Submitted ${response.action} as ${response.signature}.`);
|
|
1541
|
+
}));
|
|
1542
|
+
const makerCurrentPrepare = server.registerTool("strata_market_making_current_prepare", {
|
|
1543
|
+
title: "Prepare Strata Current control",
|
|
1544
|
+
description: "Build one exact unsigned maker-owned Current transaction. Upsert fails closed until the market has a verified on-chain reference; cancel does not need that reference.",
|
|
1545
|
+
inputSchema: {
|
|
1546
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1547
|
+
action: z.enum(["upsert", "cancel"]),
|
|
1548
|
+
makerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
|
|
1549
|
+
enabled: z.boolean().optional(),
|
|
1550
|
+
asyncOnly: z.boolean().optional(),
|
|
1551
|
+
halfSpreadBps: z.number().int().min(1).max(65_535).optional(),
|
|
1552
|
+
bandStepBps: z.number().int().min(0).max(65_535).optional(),
|
|
1553
|
+
maxConfidenceBps: z.number().int().min(1).max(100).optional(),
|
|
1554
|
+
maxOracleDeviationBps: z.number().int().min(1).max(500).optional(),
|
|
1555
|
+
maxOracleAgeSeconds: z.number().int().min(0).max(4_294_967_295).optional(),
|
|
1556
|
+
syncSpreadBps: z.number().int().min(0).max(65_535).optional(),
|
|
1557
|
+
maxExposureBaseAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1558
|
+
bidDepthBaseAtoms: z.array(z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20)).length(8).optional(),
|
|
1559
|
+
askDepthBaseAtoms: z.array(z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20)).length(8).optional(),
|
|
1560
|
+
validUntilSlot: z.string().regex(/^(?:0|[1-9][0-9]*)$/).max(20).optional(),
|
|
1561
|
+
},
|
|
1562
|
+
annotations: {
|
|
1563
|
+
readOnlyHint: false,
|
|
1564
|
+
destructiveHint: true,
|
|
1565
|
+
idempotentHint: false,
|
|
1566
|
+
openWorldHint: true,
|
|
1567
|
+
},
|
|
1568
|
+
}, async (args) => guardedTool(client, "mm.current.manage", async () => {
|
|
1569
|
+
let request;
|
|
1570
|
+
if (args.action === "cancel") {
|
|
1571
|
+
request = { action: "cancel", makerWallet: args.makerWallet };
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
if (args.enabled === undefined || args.asyncOnly === undefined
|
|
1575
|
+
|| args.halfSpreadBps === undefined || args.bandStepBps === undefined
|
|
1576
|
+
|| args.maxConfidenceBps === undefined || args.maxOracleDeviationBps === undefined
|
|
1577
|
+
|| args.maxOracleAgeSeconds === undefined || args.syncSpreadBps === undefined
|
|
1578
|
+
|| args.maxExposureBaseAtoms === undefined || args.bidDepthBaseAtoms === undefined
|
|
1579
|
+
|| args.askDepthBaseAtoms === undefined || args.validUntilSlot === undefined) {
|
|
1580
|
+
return toolError("invalid_request", "Current upsert requires every depth, exposure, spread, oracle-bound, flag, and expiry field.", false);
|
|
1581
|
+
}
|
|
1582
|
+
request = {
|
|
1583
|
+
action: "upsert",
|
|
1584
|
+
makerWallet: args.makerWallet,
|
|
1585
|
+
enabled: args.enabled,
|
|
1586
|
+
asyncOnly: args.asyncOnly,
|
|
1587
|
+
halfSpreadBps: args.halfSpreadBps,
|
|
1588
|
+
bandStepBps: args.bandStepBps,
|
|
1589
|
+
maxConfidenceBps: args.maxConfidenceBps,
|
|
1590
|
+
maxOracleDeviationBps: args.maxOracleDeviationBps,
|
|
1591
|
+
maxOracleAgeSeconds: args.maxOracleAgeSeconds,
|
|
1592
|
+
syncSpreadBps: args.syncSpreadBps,
|
|
1593
|
+
maxExposureBaseAtoms: args.maxExposureBaseAtoms,
|
|
1594
|
+
bidDepthBaseAtoms: args.bidDepthBaseAtoms,
|
|
1595
|
+
askDepthBaseAtoms: args.askDepthBaseAtoms,
|
|
1596
|
+
validUntilSlot: args.validUntilSlot,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
const response = await platformClient.marketMaking.current.prepare(args.marketId, request);
|
|
1600
|
+
return toolResult(response, `Prepared ${response.action} control ${response.maker_control_id}; verify and sign only this transaction with ${response.maker_wallet} before ${response.expires_at_ms}.`);
|
|
1601
|
+
}));
|
|
1602
|
+
const makerCurrentSubmit = server.registerTool("strata_market_making_current_submit", {
|
|
1603
|
+
title: "Submit Strata Current control",
|
|
1604
|
+
description: "Submit the exact externally maker-signed Current transaction with a stable retry key.",
|
|
1605
|
+
inputSchema: {
|
|
1606
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1607
|
+
makerControlId: z.string().regex(/^mc_[0-9a-f]{32}$/),
|
|
1608
|
+
signedTransactionBase64: z.string().min(4).max(4_096).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
|
|
1609
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/),
|
|
1610
|
+
},
|
|
1611
|
+
annotations: {
|
|
1612
|
+
readOnlyHint: false,
|
|
1613
|
+
destructiveHint: true,
|
|
1614
|
+
idempotentHint: true,
|
|
1615
|
+
openWorldHint: true,
|
|
1616
|
+
},
|
|
1617
|
+
}, async ({ marketId, makerControlId, signedTransactionBase64, idempotencyKey }) => guardedTool(client, "mm.current.manage", async () => {
|
|
1618
|
+
const response = await platformClient.marketMaking.current.submit(marketId, {
|
|
1619
|
+
makerControlId,
|
|
1620
|
+
signedTransactionBase64,
|
|
1621
|
+
idempotencyKey,
|
|
1622
|
+
});
|
|
1623
|
+
return toolResult(response, `Submitted ${response.action} as ${response.signature}.`);
|
|
1624
|
+
}));
|
|
1625
|
+
registerAutonomyTools(server, client, platformClient, options.sessionAutonomy, () => typeof Date !== "undefined" ? Date.now() : 0);
|
|
502
1626
|
const handles = {
|
|
503
1627
|
markets,
|
|
504
1628
|
quote,
|
|
1629
|
+
exactOutputQuote,
|
|
505
1630
|
executionChallenge,
|
|
506
1631
|
executionPrepare,
|
|
507
1632
|
executionSubmit,
|
|
@@ -509,6 +1634,10 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
509
1634
|
orderPrepare,
|
|
510
1635
|
orderSubmit,
|
|
511
1636
|
orderStatus,
|
|
1637
|
+
makerStrandPrepare,
|
|
1638
|
+
makerStrandSubmit,
|
|
1639
|
+
makerCurrentPrepare,
|
|
1640
|
+
makerCurrentSubmit,
|
|
512
1641
|
};
|
|
513
1642
|
applyCapabilityCatalog(handles, initialCatalog);
|
|
514
1643
|
let closed = false;
|
|
@@ -533,9 +1662,257 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
533
1662
|
},
|
|
534
1663
|
};
|
|
535
1664
|
}
|
|
1665
|
+
function registerAutonomyTools(server, client, platformClient, autonomy, nowMs) {
|
|
1666
|
+
// Always present, always read-only: the agent may show the slider and offer
|
|
1667
|
+
// to change it, but nothing it calls can raise its own autonomy.
|
|
1668
|
+
server.registerTool("strata_autonomy", {
|
|
1669
|
+
title: "Strata session autonomy",
|
|
1670
|
+
description: "Read how much this MCP may finish by itself: the autonomy level (ask / limits / instant), "
|
|
1671
|
+
+ "any USD ceilings, and how to change it. Read-only — the level is the user's, set out-of-band "
|
|
1672
|
+
+ "(the Agents page or the MCP's own env), never by an agent.",
|
|
1673
|
+
inputSchema: {},
|
|
1674
|
+
annotations: {
|
|
1675
|
+
readOnlyHint: true,
|
|
1676
|
+
destructiveHint: false,
|
|
1677
|
+
idempotentHint: true,
|
|
1678
|
+
openWorldHint: false,
|
|
1679
|
+
},
|
|
1680
|
+
}, async () => {
|
|
1681
|
+
const howToChange = {
|
|
1682
|
+
level_env: "STRATA_AUTONOMY = ask | limits | instant",
|
|
1683
|
+
per_trade_env: "STRATA_AUTONOMY_MAX_USD_PER_TRADE",
|
|
1684
|
+
per_day_env: "STRATA_AUTONOMY_MAX_USD_PER_DAY",
|
|
1685
|
+
markets_env: "STRATA_AUTONOMY_MARKETS (comma-separated opaque market IDs)",
|
|
1686
|
+
session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
|
|
1687
|
+
agents_page: "https://stratabook.app/agents",
|
|
1688
|
+
note: "Only the user changes these; an agent can offer but never raise its own level.",
|
|
1689
|
+
};
|
|
1690
|
+
if (!autonomy) {
|
|
1691
|
+
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, "
|
|
1692
|
+
+ "but I cannot sign any myself. To let me trade unattended, register a Vault session "
|
|
1693
|
+
+ "key on the Agents page and set STRATA_SESSION_SECRET_KEY (+ STRATA_OWNER_WALLET), "
|
|
1694
|
+
+ "then choose STRATA_AUTONOMY=limits or instant.");
|
|
1695
|
+
}
|
|
1696
|
+
const { config } = autonomy;
|
|
1697
|
+
const spentToday = autonomy.dailyBudget.spentToday(nowMs());
|
|
1698
|
+
const state = {
|
|
1699
|
+
session_configured: true,
|
|
1700
|
+
wallet_address: autonomy.ownerWallet,
|
|
1701
|
+
session_public_key: autonomy.signer.publicKey,
|
|
1702
|
+
level: config.level,
|
|
1703
|
+
max_usd_per_trade: config.maxUsdPerTrade ?? null,
|
|
1704
|
+
max_usd_per_day: config.maxUsdPerDay ?? null,
|
|
1705
|
+
spent_today_usd: Number(spentToday.toFixed(2)),
|
|
1706
|
+
remaining_today_usd: config.maxUsdPerDay === undefined
|
|
1707
|
+
? null
|
|
1708
|
+
: Number(Math.max(0, config.maxUsdPerDay - spentToday).toFixed(2)),
|
|
1709
|
+
allowed_market_ids: config.allowedMarketIds ?? null,
|
|
1710
|
+
how_to_change: howToChange,
|
|
1711
|
+
};
|
|
1712
|
+
const summary = config.level === "instant"
|
|
1713
|
+
? "Autonomy: instant — I trade within your on-chain session caps without asking."
|
|
1714
|
+
: config.level === "limits"
|
|
1715
|
+
? `Autonomy: limits — I trade instantly up to ${config.maxUsdPerTrade !== undefined ? "$" + config.maxUsdPerTrade + "/trade" : "no per-trade cap"}`
|
|
1716
|
+
+ `${config.maxUsdPerDay !== undefined ? ", $" + config.maxUsdPerDay + "/day" : ""}; above that I stop and ask.`
|
|
1717
|
+
: "Autonomy: ask — I prepare trades but never sign them; you sign each one.";
|
|
1718
|
+
return toolResult(state, summary);
|
|
1719
|
+
});
|
|
1720
|
+
if (!autonomy)
|
|
1721
|
+
return;
|
|
1722
|
+
const resolver = new MarketMetaResolver(platformClient, async () => (await client.markets()).markets, nowMs);
|
|
1723
|
+
const markFor = async (marketId) => {
|
|
1724
|
+
const mark = await platformClient.marketData.mark(marketId);
|
|
1725
|
+
return {
|
|
1726
|
+
price_atoms_per_base_unit: mark.price_atoms_per_base_unit,
|
|
1727
|
+
quote_decimals: mark.quote_decimals,
|
|
1728
|
+
stale: mark.stale,
|
|
1729
|
+
};
|
|
1730
|
+
};
|
|
1731
|
+
const refuse = (reason, prepared, summary) => toolResult({ executed: false, reason, prepared }, summary);
|
|
1732
|
+
// ── one-shot immediate execution from a fresh Sonar quote ─────────────────
|
|
1733
|
+
server.registerTool("strata_execute_quote", {
|
|
1734
|
+
title: "Execute a Strata quote",
|
|
1735
|
+
description: "Take a fresh Sonar quote and, within the autonomy slider, sign it with the session key and "
|
|
1736
|
+
+ "submit it in one call. Under \"ask\" (or over a \"limits\" ceiling) it does not sign — it returns "
|
|
1737
|
+
+ "the quote and asks you to sign or raise the slider.",
|
|
1738
|
+
inputSchema: {
|
|
1739
|
+
market: z.string().min(2).max(64).describe("Market label such as SOL/USDC, or its public market ID."),
|
|
1740
|
+
side: z.enum(["buy", "sell"]),
|
|
1741
|
+
amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20),
|
|
1742
|
+
toleranceBps: z.number().int().min(0).max(1_000).optional(),
|
|
1743
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1744
|
+
},
|
|
1745
|
+
annotations: {
|
|
1746
|
+
readOnlyHint: false,
|
|
1747
|
+
destructiveHint: true,
|
|
1748
|
+
idempotentHint: false,
|
|
1749
|
+
openWorldHint: true,
|
|
1750
|
+
},
|
|
1751
|
+
}, async (args) => guardedTool(client, "trade.submit", async () => {
|
|
1752
|
+
const quote = await client.quote({
|
|
1753
|
+
market: args.market,
|
|
1754
|
+
side: args.side,
|
|
1755
|
+
amountInAtoms: args.amountInAtoms,
|
|
1756
|
+
...(args.toleranceBps === undefined ? {} : { toleranceBps: args.toleranceBps }),
|
|
1757
|
+
});
|
|
1758
|
+
const sonar = (await client.markets()).markets.find((market) => market.market_pda === quote.market_id);
|
|
1759
|
+
const notional = sonar
|
|
1760
|
+
? quoteNotionalUsd(quote.side, quote.amount_in_atoms, quote.minimum_output_atoms, sonar.quote_decimals)
|
|
1761
|
+
: null;
|
|
1762
|
+
const marketId = sonar ? await resolver.idForLabel(sonar.label) : null;
|
|
1763
|
+
const decision = decideAutonomy(autonomy, marketId ?? "", notional, nowMs());
|
|
1764
|
+
if (!decision.allow) {
|
|
1765
|
+
return refuse(decision.reason, quote, decision.reason);
|
|
1766
|
+
}
|
|
1767
|
+
const receipt = await client.executeQuote({
|
|
1768
|
+
quote,
|
|
1769
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1770
|
+
signer: autonomy.signer,
|
|
1771
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1772
|
+
});
|
|
1773
|
+
if (notional !== null)
|
|
1774
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1775
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${quote.side} on ${quote.market_id} as ${receipt.signature}.`);
|
|
1776
|
+
}));
|
|
1777
|
+
// ── one-shot order control (place / cancel / replace / batch) ─────────────
|
|
1778
|
+
server.registerTool("strata_order_execute", {
|
|
1779
|
+
title: "Execute a Strata order control",
|
|
1780
|
+
description: "Place, cancel, replace, or batch orders and, within the autonomy slider, sign with the session "
|
|
1781
|
+
+ "key and submit in one call. Owner wallet and session key come from the configured session. "
|
|
1782
|
+
+ "Under \"ask\" (or over a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
|
|
1783
|
+
inputSchema: {
|
|
1784
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1785
|
+
action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
|
|
1786
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1787
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1788
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1789
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1790
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1791
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1792
|
+
operations: z.array(z.object({
|
|
1793
|
+
action: z.enum(["place", "cancel", "replace"]),
|
|
1794
|
+
clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1795
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1796
|
+
orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
|
|
1797
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1798
|
+
sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
|
|
1799
|
+
orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
|
|
1800
|
+
}).strict()).min(1).max(6).optional(),
|
|
1801
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1802
|
+
},
|
|
1803
|
+
annotations: {
|
|
1804
|
+
readOnlyHint: false,
|
|
1805
|
+
destructiveHint: true,
|
|
1806
|
+
idempotentHint: false,
|
|
1807
|
+
openWorldHint: true,
|
|
1808
|
+
},
|
|
1809
|
+
}, async (args) => guardedTool(client, "orders.submit", async () => {
|
|
1810
|
+
const challenge = orderOperationFromArgs({
|
|
1811
|
+
...args,
|
|
1812
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1813
|
+
sessionPublicKey: autonomy.signer.publicKey,
|
|
1814
|
+
});
|
|
1815
|
+
if ("content" in challenge)
|
|
1816
|
+
return challenge;
|
|
1817
|
+
// A place/replace risks new base; a cancel reduces it (notional 0).
|
|
1818
|
+
const baseAtoms = (args.action === "place" || args.action === "replace") && args.sizeAtoms !== undefined
|
|
1819
|
+
? BigInt(args.sizeAtoms)
|
|
1820
|
+
: 0n;
|
|
1821
|
+
const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
|
|
1822
|
+
const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
|
|
1823
|
+
if (!decision.allow) {
|
|
1824
|
+
const prepared = await platformClient.orders.prepare(args.marketId, {
|
|
1825
|
+
operation: challenge,
|
|
1826
|
+
});
|
|
1827
|
+
return refuse(decision.reason, prepared, decision.reason);
|
|
1828
|
+
}
|
|
1829
|
+
const { sessionPublicKey: _session, ...operation } = challenge;
|
|
1830
|
+
const receipt = await platformClient.orders.execute(args.marketId, {
|
|
1831
|
+
operation: operation,
|
|
1832
|
+
signer: autonomy.signer,
|
|
1833
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1834
|
+
});
|
|
1835
|
+
if (notional !== null)
|
|
1836
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1837
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${receipt.action} control ${receipt.order_control_id} as ${receipt.signature}.`);
|
|
1838
|
+
}));
|
|
1839
|
+
// ── one-shot TWAP (schedule / cancel) ─────────────────────────────────────
|
|
1840
|
+
server.registerTool("strata_twap_execute", {
|
|
1841
|
+
title: "Execute a Strata TWAP",
|
|
1842
|
+
description: "Schedule or cancel a TWAP and, within the autonomy slider, sign with the session key and submit "
|
|
1843
|
+
+ "in one call. Owner wallet and session key come from the configured session. Under \"ask\" (or over "
|
|
1844
|
+
+ "a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
|
|
1845
|
+
inputSchema: {
|
|
1846
|
+
marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
|
|
1847
|
+
action: z.enum(["place", "cancel"]),
|
|
1848
|
+
side: z.enum(["buy", "sell"]).optional(),
|
|
1849
|
+
totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
1850
|
+
slicesTotal: z.number().int().min(2).max(120).optional(),
|
|
1851
|
+
maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
|
|
1852
|
+
intervalSlots: z.number().int().min(25).max(4_500).optional(),
|
|
1853
|
+
limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
1854
|
+
twapId: z.string().regex(/^twap_[0-9a-f]{32}$/).optional(),
|
|
1855
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1856
|
+
},
|
|
1857
|
+
annotations: {
|
|
1858
|
+
readOnlyHint: false,
|
|
1859
|
+
destructiveHint: true,
|
|
1860
|
+
idempotentHint: false,
|
|
1861
|
+
openWorldHint: true,
|
|
1862
|
+
},
|
|
1863
|
+
}, async (args) => guardedTool(client, "algos.submit", async () => {
|
|
1864
|
+
let operation;
|
|
1865
|
+
if (args.action === "cancel") {
|
|
1866
|
+
if (args.twapId === undefined)
|
|
1867
|
+
return toolError("invalid_request", "Cancel requires twapId.", false);
|
|
1868
|
+
operation = { action: "cancel", ownerWallet: autonomy.ownerWallet, twapId: args.twapId };
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
if (args.side === undefined
|
|
1872
|
+
|| args.totalSizeAtoms === undefined
|
|
1873
|
+
|| args.slicesTotal === undefined
|
|
1874
|
+
|| args.maximumToleranceBps === undefined
|
|
1875
|
+
|| args.intervalSlots === undefined
|
|
1876
|
+
|| args.limitPriceAtoms === undefined) {
|
|
1877
|
+
return toolError("invalid_request", "Place requires side, totalSizeAtoms, slicesTotal, maximumToleranceBps, intervalSlots, and limitPriceAtoms.", false);
|
|
1878
|
+
}
|
|
1879
|
+
operation = {
|
|
1880
|
+
action: "place",
|
|
1881
|
+
ownerWallet: autonomy.ownerWallet,
|
|
1882
|
+
side: args.side,
|
|
1883
|
+
totalSizeAtoms: args.totalSizeAtoms,
|
|
1884
|
+
slicesTotal: args.slicesTotal,
|
|
1885
|
+
maximumToleranceBps: args.maximumToleranceBps,
|
|
1886
|
+
intervalSlots: args.intervalSlots,
|
|
1887
|
+
limitPriceAtoms: args.limitPriceAtoms,
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
const baseAtoms = args.action === "place" && args.totalSizeAtoms !== undefined ? BigInt(args.totalSizeAtoms) : 0n;
|
|
1891
|
+
const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
|
|
1892
|
+
const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
|
|
1893
|
+
if (!decision.allow) {
|
|
1894
|
+
const prepared = await platformClient.algos.prepare(args.marketId, {
|
|
1895
|
+
operation: {
|
|
1896
|
+
...operation,
|
|
1897
|
+
sessionPublicKey: autonomy.signer.publicKey,
|
|
1898
|
+
},
|
|
1899
|
+
});
|
|
1900
|
+
return refuse(decision.reason, prepared, decision.reason);
|
|
1901
|
+
}
|
|
1902
|
+
const receipt = await platformClient.algos.execute(args.marketId, {
|
|
1903
|
+
operation,
|
|
1904
|
+
signer: autonomy.signer,
|
|
1905
|
+
...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
|
|
1906
|
+
});
|
|
1907
|
+
if (notional !== null)
|
|
1908
|
+
autonomy.dailyBudget.record(notional, nowMs());
|
|
1909
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed TWAP ${receipt.twap_control_id} as ${receipt.signature}.`);
|
|
1910
|
+
}));
|
|
1911
|
+
}
|
|
536
1912
|
function applyCapabilityCatalog(handles, catalog) {
|
|
537
1913
|
setToolEnabled(handles.markets, capabilityAvailable(catalog, "markets.read"));
|
|
538
1914
|
setToolEnabled(handles.quote, capabilityAvailable(catalog, "quotes.read"));
|
|
1915
|
+
setToolEnabled(handles.exactOutputQuote, capabilityAvailable(catalog, "quotes.read"));
|
|
539
1916
|
setToolEnabled(handles.executionChallenge, capabilityAvailable(catalog, "trade.prepare"));
|
|
540
1917
|
setToolEnabled(handles.executionPrepare, capabilityAvailable(catalog, "trade.prepare"));
|
|
541
1918
|
setToolEnabled(handles.executionSubmit, capabilityAvailable(catalog, "trade.submit"));
|
|
@@ -543,6 +1920,12 @@ function applyCapabilityCatalog(handles, catalog) {
|
|
|
543
1920
|
setToolEnabled(handles.orderPrepare, capabilityAvailable(catalog, "orders.prepare"));
|
|
544
1921
|
setToolEnabled(handles.orderSubmit, capabilityAvailable(catalog, "orders.submit"));
|
|
545
1922
|
setToolEnabled(handles.orderStatus, capabilityAvailable(catalog, "orders.submit"));
|
|
1923
|
+
const strandEnabled = capabilityAvailable(catalog, "mm.strand.manage");
|
|
1924
|
+
setToolEnabled(handles.makerStrandPrepare, strandEnabled);
|
|
1925
|
+
setToolEnabled(handles.makerStrandSubmit, strandEnabled);
|
|
1926
|
+
const currentEnabled = capabilityAvailable(catalog, "mm.current.manage");
|
|
1927
|
+
setToolEnabled(handles.makerCurrentPrepare, currentEnabled);
|
|
1928
|
+
setToolEnabled(handles.makerCurrentSubmit, currentEnabled);
|
|
546
1929
|
}
|
|
547
1930
|
function setToolEnabled(tool, enabled) {
|
|
548
1931
|
if (enabled)
|
|
@@ -565,6 +1948,16 @@ async function guardedTool(client, capabilityId, operation) {
|
|
|
565
1948
|
return toolError("request_failed", safeMessage(error), true);
|
|
566
1949
|
}
|
|
567
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* One line that keeps the two numbers apart: price impact is measured from the
|
|
1953
|
+
* book; the tolerance is the caller's own floor.
|
|
1954
|
+
*/
|
|
1955
|
+
function quoteSummary(response) {
|
|
1956
|
+
return (`Sonar ${response.side} quote: ${response.amount_in_consumed_atoms} input atoms for `
|
|
1957
|
+
+ `${response.amount_out_atoms} user-net output atoms; price impact ${response.price_impact_pct}% `
|
|
1958
|
+
+ `(measured from the book); your tolerance ${response.maximum_tolerance_bps} bps, so the `
|
|
1959
|
+
+ `user-net floor is ${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
|
|
1960
|
+
}
|
|
568
1961
|
function toolResult(value, summary) {
|
|
569
1962
|
return {
|
|
570
1963
|
content: [
|