@stratabook/mcp 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +44 -9
- package/dist/src/cli.js +52 -7
- package/dist/src/generated-harness.d.ts +7 -7
- package/dist/src/generated-harness.js +7 -6
- package/dist/src/server.d.ts +3 -0
- package/dist/src/server.js +166 -28
- package/dist/src/usability.d.ts +27 -0
- package/dist/src/usability.js +116 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All notable changes to the Strata SDKs (`@stratabook/sdk`, `@stratabook/mcp`,
|
|
4
4
|
and the `strata-sdk` Rust crate) are recorded here. Versions move together.
|
|
5
5
|
|
|
6
|
+
## 0.2.12
|
|
7
|
+
|
|
8
|
+
- Make MCP client-neutral: the primary setup works with any stdio or
|
|
9
|
+
Streamable HTTP MCP host; Codex is only one optional client example.
|
|
10
|
+
- Replace the forced discovery preamble with direct read-only tool use and a
|
|
11
|
+
compact 12-tool default. `--mode advanced` retains the complete protocol.
|
|
12
|
+
- Accept exact human quote and trade amounts such as `0.1 SOL`, `20 USDC`, and
|
|
13
|
+
`$20`, resolving market labels and decimals without floating point.
|
|
14
|
+
- Add progressive `strata_trade`, `strata-mcp doctor`, one safe transient quote
|
|
15
|
+
retry, and plain next steps for common failures.
|
|
16
|
+
|
|
17
|
+
## 0.2.11
|
|
18
|
+
|
|
19
|
+
- Consume the exact TypeScript SDK 0.2.11 release containing the installed
|
|
20
|
+
maker-conformance CLI entrypoint fix.
|
|
21
|
+
- Make the zero-credential read-only path explicit in MCP initialization:
|
|
22
|
+
markets, books, marks, candles, trades, quotes, public portfolios, and maker
|
|
23
|
+
reads work immediately. Agents must not request session secrets in chat or
|
|
24
|
+
begin write onboarding unless the user asks to trade.
|
|
25
|
+
|
|
6
26
|
## 0.2.10
|
|
7
27
|
|
|
8
28
|
- Consume the matching SDK conformance release. The safe deployment gate now
|
package/README.md
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
# Strata MCP
|
|
2
2
|
|
|
3
|
-
Official
|
|
4
|
-
to
|
|
5
|
-
|
|
3
|
+
Official MCP access to Strata and Sonar for every MCP-compatible client. It is
|
|
4
|
+
not tied to Codex, Claude, Cursor, or any other host. The server delegates to
|
|
5
|
+
`@stratabook/sdk` and follows Strata's live public policy.
|
|
6
6
|
|
|
7
7
|
The official hosted endpoint currently exposes market, exact-output, and
|
|
8
8
|
asset-to-asset Sonar quotes, together with quote-bound execution tools.
|
|
9
|
-
Capability
|
|
10
|
-
|
|
9
|
+
"Capability gated" simply means a tool disappears if Strata disables that
|
|
10
|
+
operation live. It does **not** mean quotes need activation or user setup.
|
|
11
|
+
|
|
12
|
+
The default tool mode is deliberately compact. Agents call the requested tool
|
|
13
|
+
directly instead of burning discovery calls before a quote. Use
|
|
14
|
+
`--mode advanced` (or `STRATA_MCP_MODE=advanced`) only when an integration
|
|
15
|
+
needs the explicit challenge / prepare / submit protocol tools.
|
|
11
16
|
|
|
12
17
|
## Local stdio
|
|
13
18
|
|
|
19
|
+
Read-only use needs no wallet, key, autonomy setting, or environment variable:
|
|
20
|
+
|
|
14
21
|
```sh
|
|
15
22
|
npx -y @stratabook/mcp
|
|
16
23
|
```
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
Do not paste a session secret into chat. Session setup is optional and appears
|
|
26
|
+
only when the user wants the local MCP to sign trading writes.
|
|
27
|
+
|
|
28
|
+
Generic configuration for Claude Desktop, Cursor, Windsurf, and other
|
|
29
|
+
JSON-config MCP clients:
|
|
19
30
|
|
|
20
31
|
```json
|
|
21
32
|
{
|
|
@@ -28,7 +39,28 @@ Example client configuration:
|
|
|
28
39
|
}
|
|
29
40
|
```
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
Codex happens to support a one-line client-specific installer:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
codex mcp add strata -- npx -y @stratabook/mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Check the whole read-only connection without placing a trade:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx -y @stratabook/mcp doctor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The compact default exposes the tools ordinary users need:
|
|
55
|
+
|
|
56
|
+
- `strata_markets`, `strata_marks`, `strata_book`, `strata_candles`, `strata_trades`
|
|
57
|
+
- `strata_quote` — accepts `0.1 SOL`, `20 USDC`, or `$20`; token atoms remain optional
|
|
58
|
+
- `strata_portfolio` and `strata_market_making_status`
|
|
59
|
+
- `strata_trade` — returns a live quote when trading is not connected, with one setup link; follows the user's session limits when connected
|
|
60
|
+
- `strata_market_making_prepare` and `strata_market_making_submit_and_wait`
|
|
61
|
+
- `strata_autonomy` — reports whether optional trading is connected and the user's limits
|
|
62
|
+
|
|
63
|
+
Advanced mode additionally exposes the complete protocol surface, including:
|
|
32
64
|
|
|
33
65
|
- `strata_capabilities`
|
|
34
66
|
- `strata_action_graph`
|
|
@@ -71,7 +103,8 @@ The tools currently available are:
|
|
|
71
103
|
- `strata_order_submit`, when `orders.submit` is enabled for MCP
|
|
72
104
|
- `strata_order_status`, when `orders.submit` is enabled for MCP
|
|
73
105
|
|
|
74
|
-
Every initialization response carries the compact Strata Agent Harness.
|
|
106
|
+
Every initialization response carries the compact Strata Agent Harness. It
|
|
107
|
+
instructs an agent to call normal read tools directly. The
|
|
75
108
|
server also publishes the complete harness as the
|
|
76
109
|
`strata://agent-harness/v1` resource and provides a `strata_start` prompt for
|
|
77
110
|
applying it to one concrete objective.
|
|
@@ -147,7 +180,9 @@ bounded TWAP placement or cancellation, or atomic place, cancel, cancel-all,
|
|
|
147
180
|
replace, or bounded batch order controls, and
|
|
148
181
|
submit the externally signed result. It accepts
|
|
149
182
|
public keys, detached signatures, and signed transactions, never private keys,
|
|
150
|
-
seed phrases, or wallet secrets.
|
|
183
|
+
seed phrases, or wallet secrets. Simple quote and trade tools accept exact
|
|
184
|
+
decimal strings with their symbol; conversion never uses floating-point
|
|
185
|
+
arithmetic. Advanced protocol fields remain token atoms encoded as base-10
|
|
151
186
|
strings.
|
|
152
187
|
|
|
153
188
|
Quotes default to zero tolerance. `maximumToleranceBps` is the agent's own
|
package/dist/src/cli.js
CHANGED
|
@@ -2,22 +2,25 @@
|
|
|
2
2
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
-
import { DEFAULT_API_BASE } from "@stratabook/sdk";
|
|
5
|
+
import { DEFAULT_API_BASE, StrataApiError, StrataClient } from "@stratabook/sdk";
|
|
6
6
|
import { STRATA_ACTION_GRAPH, STRATA_AGENT_HARNESS, } from "./generated-harness.js";
|
|
7
7
|
import { createStrataMcpServer, probeStrataMcpReadiness } from "./server.js";
|
|
8
8
|
import { sessionAutonomyFromEnv } from "./autonomy.js";
|
|
9
9
|
import { SERVER_VERSION } from "./version.js";
|
|
10
|
+
import { friendlyApiError, humanQuoteAmount, parseToolMode } from "./usability.js";
|
|
10
11
|
function parse(argv) {
|
|
12
|
+
const command = argv[0] === "doctor" ? "doctor" : "serve";
|
|
13
|
+
const args = command === "doctor" ? argv.slice(1) : argv;
|
|
11
14
|
const values = new Map();
|
|
12
|
-
for (let index = 0; index <
|
|
13
|
-
const token =
|
|
15
|
+
for (let index = 0; index < args.length; index++) {
|
|
16
|
+
const token = args[index];
|
|
14
17
|
if (token === "--help" || token === "-h") {
|
|
15
18
|
help();
|
|
16
19
|
process.exit(0);
|
|
17
20
|
}
|
|
18
21
|
if (!token?.startsWith("--"))
|
|
19
22
|
throw new Error(`unexpected argument: ${token}`);
|
|
20
|
-
const next =
|
|
23
|
+
const next = args[index + 1];
|
|
21
24
|
if (!next || next.startsWith("--"))
|
|
22
25
|
throw new Error(`${token} requires a value`);
|
|
23
26
|
values.set(token.slice(2), next);
|
|
@@ -33,7 +36,9 @@ function parse(argv) {
|
|
|
33
36
|
if (!/^[a-zA-Z0-9.:[\]-]+$/.test(host))
|
|
34
37
|
throw new Error("host is invalid");
|
|
35
38
|
return {
|
|
39
|
+
command,
|
|
36
40
|
transport,
|
|
41
|
+
toolMode: parseToolMode(values.get("mode") ?? process.env.STRATA_MCP_MODE),
|
|
37
42
|
apiBase: values.get("api-base") ?? process.env.STRATA_API_BASE ?? DEFAULT_API_BASE,
|
|
38
43
|
timeoutMs,
|
|
39
44
|
host,
|
|
@@ -52,20 +57,56 @@ function help() {
|
|
|
52
57
|
|
|
53
58
|
Usage:
|
|
54
59
|
strata-mcp
|
|
60
|
+
strata-mcp doctor
|
|
55
61
|
strata-mcp --transport http [--host localhost] [--port 8787]
|
|
56
62
|
|
|
57
63
|
Options:
|
|
58
64
|
--transport stdio|http Local stdio by default; Streamable HTTP for hosting
|
|
65
|
+
--mode simple|advanced Compact direct tools (default) or full protocol tools
|
|
59
66
|
--api-base URL Strata public API (default: ${DEFAULT_API_BASE})
|
|
60
67
|
--timeout-ms N Upstream timeout, 250..60000 (default: 10000)
|
|
61
68
|
--host HOST HTTP bind host (default: localhost)
|
|
62
69
|
--port N HTTP port (default: 8787)
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
Read-only markets, books, and Sonar quotes work immediately. Run strata-mcp
|
|
72
|
+
doctor to check them. Trading setup is only needed for trading writes; connect
|
|
73
|
+
it at https://stratabook.app/agents. Strata never asks for seed phrases.
|
|
67
74
|
`);
|
|
68
75
|
}
|
|
76
|
+
async function runDoctor(options) {
|
|
77
|
+
const client = new StrataClient({ apiBase: options.apiBase, timeoutMs: options.timeoutMs });
|
|
78
|
+
process.stdout.write("Strata MCP doctor\n\n");
|
|
79
|
+
try {
|
|
80
|
+
const capabilities = await client.capabilities();
|
|
81
|
+
process.stdout.write(`✓ Public API connected (contract ${capabilities.contract_version})\n`);
|
|
82
|
+
const response = await client.markets();
|
|
83
|
+
const ready = response.markets.filter((market) => market.ready);
|
|
84
|
+
process.stdout.write(`✓ ${ready.length} read-only markets available\n`);
|
|
85
|
+
const solUsdc = ready.find((market) => market.label.toUpperCase() === "SOL/USDC");
|
|
86
|
+
if (solUsdc) {
|
|
87
|
+
const amount = humanQuoteAmount(ready, solUsdc.label, "sell", "0.01 SOL");
|
|
88
|
+
const quote = await client.quote({
|
|
89
|
+
market: solUsdc.label,
|
|
90
|
+
side: "sell",
|
|
91
|
+
amountInAtoms: amount.atoms,
|
|
92
|
+
});
|
|
93
|
+
process.stdout.write(`✓ Sonar quote works (${quote.quote_id})\n`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
process.stdout.write("○ SOL/USDC is not currently listed; quote check skipped\n");
|
|
97
|
+
}
|
|
98
|
+
const sessionConfigured = Boolean(process.env.STRATA_SESSION_SECRET_KEY && process.env.STRATA_OWNER_WALLET);
|
|
99
|
+
process.stdout.write(sessionConfigured
|
|
100
|
+
? "✓ Trading connection found (no transaction sent)\n"
|
|
101
|
+
: "○ Trading is not connected; read-only use is ready. Connect later at https://stratabook.app/agents\n");
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof StrataApiError) {
|
|
105
|
+
throw new Error(friendlyApiError(error.code, error.message));
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
69
110
|
async function runStdio(options) {
|
|
70
111
|
const runtime = await createStrataMcpServer(options);
|
|
71
112
|
const transport = new StdioServerTransport();
|
|
@@ -161,6 +202,10 @@ function safeError(error) {
|
|
|
161
202
|
}
|
|
162
203
|
async function main() {
|
|
163
204
|
const options = parse(process.argv.slice(2));
|
|
205
|
+
if (options.command === "doctor") {
|
|
206
|
+
await runDoctor(options);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
164
209
|
const sessionAutonomy = await sessionAutonomyFromEnv(process.env);
|
|
165
210
|
const withSession = sessionAutonomy ? { ...options, sessionAutonomy } : options;
|
|
166
211
|
if (sessionAutonomy) {
|
|
@@ -17,21 +17,21 @@ export declare const STRATA_AGENT_HARNESS: {
|
|
|
17
17
|
readonly manifest: "https://api.stratabook.app/.well-known/strata-agent.json";
|
|
18
18
|
};
|
|
19
19
|
readonly interfaces: {
|
|
20
|
-
readonly mcp_tool_order: readonly ["strata_capabilities", "strata_action_graph", "strata_platform_graph", "strata_status", "strata_markets", "strata_marks", "strata_candles", "strata_twaps", "strata_twap_challenge", "strata_twap_cancel", "strata_twap_prepare", "strata_twap_submit", "strata_portfolio", "strata_portfolio_history", "strata_market_making_status", "strata_market_making_reputation", "strata_market_making_prepare", "strata_market_making_submit_and_wait", "strata_vault_status", "strata_vault_setup", "strata_vault_deposit", "strata_vault_withdraw", "strata_vault_delegate", "strata_vault_policy", "strata_vault_pause", "strata_vault_submit", "strata_vault_submission", "strata_rewards", "strata_referrals", "strata_referral_link", "strata_referral_claim", "strata_bugs", "strata_bug_submit", "strata_quote", "strata_exact_output_quote", "strata_swap_quote", "strata_execution_challenge", "strata_execution_prepare", "strata_execution_submit", "strata_execution_status", "strata_order_challenge", "strata_order_prepare", "strata_order_submit", "strata_order_status"];
|
|
20
|
+
readonly mcp_tool_order: readonly ["strata_capabilities", "strata_action_graph", "strata_platform_graph", "strata_status", "strata_markets", "strata_marks", "strata_candles", "strata_twaps", "strata_twap_challenge", "strata_twap_cancel", "strata_twap_prepare", "strata_twap_submit", "strata_portfolio", "strata_portfolio_history", "strata_market_making_status", "strata_market_making_reputation", "strata_market_making_prepare", "strata_market_making_submit_and_wait", "strata_vault_status", "strata_vault_setup", "strata_vault_deposit", "strata_vault_withdraw", "strata_vault_delegate", "strata_vault_policy", "strata_vault_pause", "strata_vault_submit", "strata_vault_submission", "strata_rewards", "strata_referrals", "strata_referral_link", "strata_referral_claim", "strata_bugs", "strata_bug_submit", "strata_quote", "strata_trade", "strata_exact_output_quote", "strata_swap_quote", "strata_execution_challenge", "strata_execution_prepare", "strata_execution_submit", "strata_execution_status", "strata_order_challenge", "strata_order_prepare", "strata_order_submit", "strata_order_status"];
|
|
21
21
|
readonly terminal: readonly ["npx -y @stratabook/sdk capabilities --json", "npx -y @stratabook/sdk action-graph --json", "npx -y @stratabook/sdk platform-graph --json", "npx -y @stratabook/sdk platform-status --json", "npx -y @stratabook/sdk mark --market-id MARKET_ID --json", "npx -y @stratabook/sdk candles --market-id MARKET_ID --from-ms FROM_MS --to-ms TO_MS --resolution-seconds 300 --json", "npx -y @stratabook/sdk execution-status --market-id MARKET_ID --execution-id EXECUTION_ID --json", "npx -y @stratabook/sdk twaps --market-id MARKET_ID --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk twap-challenge --market-id MARKET_ID --owner-wallet OWNER_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --side buy --total-size-atoms TOTAL_ATOMS --slices 10 --tolerance-bps 100 --interval-slots 100 --limit-price-atoms PRICE_ATOMS --json", "npx -y @stratabook/sdk twap-cancel --market-id MARKET_ID --owner-wallet OWNER_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --twap-id TWAP_ID --json", "npx -y @stratabook/sdk twap-prepare --market-id MARKET_ID --owner-wallet OWNER_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --side buy --total-size-atoms TOTAL_ATOMS --slices 10 --tolerance-bps 100 --interval-slots 100 --limit-price-atoms PRICE_ATOMS --json", "npx -y @stratabook/sdk twap-submit --market-id MARKET_ID --twap-control-id CONTROL_ID --signed-transaction-base64 TRANSACTION --idempotency-key KEY --json", "npx -y @stratabook/sdk account --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk portfolio-history --wallet WALLET_PUBLIC_KEY --range 24h --json", "npx -y @stratabook/sdk maker-status --market-id MARKET_ID --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk maker-reputation --market-id MARKET_ID --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk vault-status --wallet WALLET_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --json", "npx -y @stratabook/sdk session-keygen --json", "npx -y @stratabook/sdk vault-setup --wallet WALLET_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --json", "npx -y @stratabook/sdk vault-deposit --wallet WALLET_PUBLIC_KEY --market-id MARKET_ID --asset-id ASSET_ID --amount-atoms AMOUNT --session-public-key SESSION_PUBLIC_KEY --json", "npx -y @stratabook/sdk vault-withdraw --wallet WALLET_PUBLIC_KEY --market-id MARKET_ID --asset-id ASSET_ID --destination-wallet DESTINATION_WALLET --amount-atoms AMOUNT --json", "npx -y @stratabook/sdk vault-delegate --wallet WALLET_PUBLIC_KEY --session-public-key SESSION_PUBLIC_KEY --action revoke --json", "npx -y @stratabook/sdk vault-policy --wallet WALLET_PUBLIC_KEY --mode restricted --allowed-wallets DESTINATION_WALLET --json", "npx -y @stratabook/sdk vault-pause --wallet WALLET_PUBLIC_KEY --paused true --json", "npx -y @stratabook/sdk rewards --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk referrals --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk referral-link --wallet WALLET_PUBLIC_KEY --code REFERRAL_CODE --json", "npx -y @stratabook/sdk referral-claim --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk bugs --wallet WALLET_PUBLIC_KEY --json", "npx -y @stratabook/sdk bug-payload --message REPORT_TEXT --json", "npx -y @stratabook/sdk markets --json", "npx -y @stratabook/sdk quote --market SOL/USDC --side sell --amount-atoms 10000000 --json", "npx -y @stratabook/sdk swap-quote --input-asset-id INPUT_ASSET_ID --output-asset-id OUTPUT_ASSET_ID --amount-atoms 10000000 --json", "npx -y @stratabook/sdk order-slo --market-id MARKET_ID --owner-wallet OWNER_PUBLIC_KEY --json"];
|
|
22
22
|
};
|
|
23
23
|
readonly workflow: readonly [{
|
|
24
24
|
readonly id: "discover_capabilities";
|
|
25
|
-
readonly instruction: "Read the live capability catalog
|
|
25
|
+
readonly instruction: "Call the requested read-only tool directly. Read the live capability catalog only when a requested tool is unavailable, the objective is advanced or ambiguous, or the user explicitly asks what Strata supports. Never turn capability discovery into a prerequisite for a normal market, book, account, or quote request.";
|
|
26
26
|
}, {
|
|
27
27
|
readonly id: "establish_mode";
|
|
28
|
-
readonly instruction: "
|
|
28
|
+
readonly instruction: "Use the compact default MCP tools for ordinary requests. Read the action graphs and use the advanced tool surface only for an integration that needs explicit challenge, prepare, sign, and submit control. The external agent owner configures permissions and signer authority.";
|
|
29
29
|
}, {
|
|
30
30
|
readonly id: "understand_objective";
|
|
31
31
|
readonly instruction: "Resolve the user's market or input/output assets, side when applicable, amount, and tolerance. Ask before proceeding when any economically meaningful input is ambiguous.";
|
|
32
32
|
}, {
|
|
33
33
|
readonly id: "discover_market";
|
|
34
|
-
readonly instruction: "
|
|
34
|
+
readonly instruction: "Pass familiar labels such as SOL/USDC to simple tools. Call strata_markets only when the market is unknown or a low-level operation needs opaque IDs and decimals. Do not guess identifiers or token decimals.";
|
|
35
35
|
}, {
|
|
36
36
|
readonly id: "read_market_data";
|
|
37
37
|
readonly instruction: "When books.read is live, use the opaque market ID to read the Strata book, status, fees, and recent trades. Subscribe to the market stream for changes and recover from any sequence gap with a fresh snapshot.";
|
|
@@ -73,13 +73,13 @@ export declare const STRATA_AGENT_HARNESS: {
|
|
|
73
73
|
readonly instruction: "When mm.reputation.read is live, read the maker's record by wallet address (public, no signature). Use the tier, reliability counters, tier-progress gates, signed-quote eligibility, and minimum cadence before choosing the maker transport; do not infer hidden counterparties or execution paths.";
|
|
74
74
|
}, {
|
|
75
75
|
readonly id: "preserve_atoms";
|
|
76
|
-
readonly instruction: "
|
|
76
|
+
readonly instruction: "For simple MCP tools, prefer exact human strings such as 0.1 SOL, 20 USDC, or $20; the MCP resolves decimals without floating point. For SDK and advanced tools, represent settlement amounts as unsigned base-10 atomic strings and never use floating-point arithmetic.";
|
|
77
77
|
}, {
|
|
78
78
|
readonly id: "authorize_community_actions";
|
|
79
79
|
readonly instruction: "For referral link or claim actions, generate the exact official SDK authorization payload, have the affected owner wallet sign it externally, and submit only the detached signature with the same referral code or payout wallet binding.";
|
|
80
80
|
}, {
|
|
81
81
|
readonly id: "request_quote";
|
|
82
|
-
readonly instruction: "
|
|
82
|
+
readonly instruction: "Call strata_quote directly with a market label, side, and exact human amount. Use opaque asset IDs or exact atoms only for the advanced swap and protocol interfaces. The tolerance is the user's economic choice; zero is the safe default.";
|
|
83
83
|
}, {
|
|
84
84
|
readonly id: "validate_quote";
|
|
85
85
|
readonly instruction: "Verify the quote binds to the selected market and side or the selected input/output assets, plus the exact input and tolerance. Check labelled fees, minimum output, price impact, server time, and expiry.";
|
|
@@ -551,4 +551,4 @@ export declare const STRATA_ACTION_GRAPH: {
|
|
|
551
551
|
}];
|
|
552
552
|
};
|
|
553
553
|
export declare const STRATA_ACTION_GRAPH_URI = "strata://action-graph/v1";
|
|
554
|
-
export declare const STRATA_AGENT_HARNESS_INSTRUCTIONS = "Strata
|
|
554
|
+
export declare const STRATA_AGENT_HARNESS_INSTRUCTIONS = "Strata read-only tools work immediately with no wallet, approval, session key, or environment setup. Call the requested tool directly: use strata_quote for a quote, strata_markets only when the market is unknown, and strata_portfolio for a public account. Do not begin with capability, graph, status, or market discovery unless the requested tool is unavailable, the objective is advanced or ambiguous, or the user asks what Strata supports. Simple MCP tools accept exact human amounts such as 0.1 SOL, 20 USDC, or $20; advanced and SDK interfaces also accept token atoms. Never request or accept a session secret in chat. A session is needed only when the user asks to sign a trading write; if it is missing, strata_trade returns a read-only quote and the single setup link https://stratabook.app/agents. Revoke, withdraw, pause, and policy changes are owner-wallet actions and never use the session signer. Strata Agent Harness 1.0. The action-graph resources are optional references for advanced integrations. The external agent owner controls permission and signer authority. Strata accepts public keys, detached signatures, and signed transactions, never private keys or seed phrases. Check quote bindings, labelled fees, minimum output, price impact, tolerance, and expiry. When portfolio.read is live, read the owner's live Vault portfolio before sizing a write and treat null USD totals as an incomplete valuation, never as zero. When mm.status.read is live, reconcile the owner's maker products and exposure before and after maker actions. For normal Strand or Current operation, prefer strata_market_making_prepare, sign only its prepared transaction externally, then pass its unchanged preparationToken with the signed transaction to strata_market_making_submit_and_wait. Advanced order, TWAP, and execution integrations may use explicit challenge, prepare, verify, sign, and submit flows with idempotency. Stop on ambiguity, sequence gaps, unavailable capabilities, paused markets, inconsistent bindings, expiry, or missing signer authority.";
|
|
@@ -53,6 +53,7 @@ export const STRATA_AGENT_HARNESS = {
|
|
|
53
53
|
"strata_bugs",
|
|
54
54
|
"strata_bug_submit",
|
|
55
55
|
"strata_quote",
|
|
56
|
+
"strata_trade",
|
|
56
57
|
"strata_exact_output_quote",
|
|
57
58
|
"strata_swap_quote",
|
|
58
59
|
"strata_execution_challenge",
|
|
@@ -104,11 +105,11 @@ export const STRATA_AGENT_HARNESS = {
|
|
|
104
105
|
"workflow": [
|
|
105
106
|
{
|
|
106
107
|
"id": "discover_capabilities",
|
|
107
|
-
"instruction": "Read the live capability catalog
|
|
108
|
+
"instruction": "Call the requested read-only tool directly. Read the live capability catalog only when a requested tool is unavailable, the objective is advanced or ambiguous, or the user explicitly asks what Strata supports. Never turn capability discovery into a prerequisite for a normal market, book, account, or quote request."
|
|
108
109
|
},
|
|
109
110
|
{
|
|
110
111
|
"id": "establish_mode",
|
|
111
|
-
"instruction": "
|
|
112
|
+
"instruction": "Use the compact default MCP tools for ordinary requests. Read the action graphs and use the advanced tool surface only for an integration that needs explicit challenge, prepare, sign, and submit control. The external agent owner configures permissions and signer authority."
|
|
112
113
|
},
|
|
113
114
|
{
|
|
114
115
|
"id": "understand_objective",
|
|
@@ -116,7 +117,7 @@ export const STRATA_AGENT_HARNESS = {
|
|
|
116
117
|
},
|
|
117
118
|
{
|
|
118
119
|
"id": "discover_market",
|
|
119
|
-
"instruction": "
|
|
120
|
+
"instruction": "Pass familiar labels such as SOL/USDC to simple tools. Call strata_markets only when the market is unknown or a low-level operation needs opaque IDs and decimals. Do not guess identifiers or token decimals."
|
|
120
121
|
},
|
|
121
122
|
{
|
|
122
123
|
"id": "read_market_data",
|
|
@@ -172,7 +173,7 @@ export const STRATA_AGENT_HARNESS = {
|
|
|
172
173
|
},
|
|
173
174
|
{
|
|
174
175
|
"id": "preserve_atoms",
|
|
175
|
-
"instruction": "
|
|
176
|
+
"instruction": "For simple MCP tools, prefer exact human strings such as 0.1 SOL, 20 USDC, or $20; the MCP resolves decimals without floating point. For SDK and advanced tools, represent settlement amounts as unsigned base-10 atomic strings and never use floating-point arithmetic."
|
|
176
177
|
},
|
|
177
178
|
{
|
|
178
179
|
"id": "authorize_community_actions",
|
|
@@ -180,7 +181,7 @@ export const STRATA_AGENT_HARNESS = {
|
|
|
180
181
|
},
|
|
181
182
|
{
|
|
182
183
|
"id": "request_quote",
|
|
183
|
-
"instruction": "
|
|
184
|
+
"instruction": "Call strata_quote directly with a market label, side, and exact human amount. Use opaque asset IDs or exact atoms only for the advanced swap and protocol interfaces. The tolerance is the user's economic choice; zero is the safe default."
|
|
184
185
|
},
|
|
185
186
|
{
|
|
186
187
|
"id": "validate_quote",
|
|
@@ -788,4 +789,4 @@ export const STRATA_ACTION_GRAPH = {
|
|
|
788
789
|
]
|
|
789
790
|
};
|
|
790
791
|
export const STRATA_ACTION_GRAPH_URI = "strata://action-graph/v1";
|
|
791
|
-
export const STRATA_AGENT_HARNESS_INSTRUCTIONS = "Strata
|
|
792
|
+
export const STRATA_AGENT_HARNESS_INSTRUCTIONS = "Strata read-only tools work immediately with no wallet, approval, session key, or environment setup. Call the requested tool directly: use strata_quote for a quote, strata_markets only when the market is unknown, and strata_portfolio for a public account. Do not begin with capability, graph, status, or market discovery unless the requested tool is unavailable, the objective is advanced or ambiguous, or the user asks what Strata supports. Simple MCP tools accept exact human amounts such as 0.1 SOL, 20 USDC, or $20; advanced and SDK interfaces also accept token atoms. Never request or accept a session secret in chat. A session is needed only when the user asks to sign a trading write; if it is missing, strata_trade returns a read-only quote and the single setup link https://stratabook.app/agents. Revoke, withdraw, pause, and policy changes are owner-wallet actions and never use the session signer. Strata Agent Harness 1.0. The action-graph resources are optional references for advanced integrations. The external agent owner controls permission and signer authority. Strata accepts public keys, detached signatures, and signed transactions, never private keys or seed phrases. Check quote bindings, labelled fees, minimum output, price impact, tolerance, and expiry. When portfolio.read is live, read the owner's live Vault portfolio before sizing a write and treat null USD totals as an incomplete valuation, never as zero. When mm.status.read is live, reconcile the owner's maker products and exposure before and after maker actions. For normal Strand or Current operation, prefer strata_market_making_prepare, sign only its prepared transaction externally, then pass its unchanged preparationToken with the signed transaction to strata_market_making_submit_and_wait. Advanced order, TWAP, and execution integrations may use explicit challenge, prepare, verify, sign, and submit flows with idempotency. Stop on ambiguity, sequence gaps, unavailable capabilities, paused markets, inconsistent bindings, expiry, or missing signer authority.";
|
package/dist/src/server.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StrataClient, StrataPlatformClient, type CapabilityCatalog } from "@stratabook/sdk";
|
|
3
3
|
import { type SessionAutonomy } from "./autonomy.js";
|
|
4
|
+
import { type StrataMcpToolMode } from "./usability.js";
|
|
4
5
|
export interface StrataMcpOptions {
|
|
5
6
|
apiBase?: string;
|
|
6
7
|
timeoutMs?: number;
|
|
7
8
|
client?: StrataClient;
|
|
8
9
|
platformClient?: StrataPlatformClient;
|
|
10
|
+
/** Compact direct-use tools by default; advanced exposes the full protocol surface. */
|
|
11
|
+
toolMode?: StrataMcpToolMode;
|
|
9
12
|
/**
|
|
10
13
|
* When set, the MCP may finish trades itself with this Vault session key,
|
|
11
14
|
* bounded by the user-owned autonomy slider. Absent = the calm default:
|
package/dist/src/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { decideAutonomy, estimateBaseNotionalUsd, quoteNotionalUsd, MarketMetaRe
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { STRATA_AGENT_HARNESS, STRATA_AGENT_HARNESS_INSTRUCTIONS, STRATA_AGENT_HARNESS_URI, STRATA_ACTION_GRAPH_URI, } from "./generated-harness.js";
|
|
6
6
|
import { SERVER_VERSION } from "./version.js";
|
|
7
|
+
import { SIMPLE_TOOL_NAMES, formatAtoms, friendlyApiError, humanQuoteAmount, } from "./usability.js";
|
|
7
8
|
const REFRESH_INTERVAL_MS = 5_000;
|
|
8
9
|
export const STRATA_PLATFORM_GRAPH_URI = "strata://platform-graph/v2";
|
|
9
10
|
const makerPublicKeySchema = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
|
|
@@ -158,6 +159,8 @@ function decodeMakerPreparationToken(token) {
|
|
|
158
159
|
const LEGACY_TOOL_CAPABILITIES = {
|
|
159
160
|
strata_markets: { ids: ["markets.read"] },
|
|
160
161
|
strata_quote: { ids: ["quotes.read"] },
|
|
162
|
+
// Always available for a read-only preview; it only submits when a session exists.
|
|
163
|
+
strata_trade: { ids: ["quotes.read"] },
|
|
161
164
|
strata_exact_output_quote: { ids: ["quotes.read"] },
|
|
162
165
|
strata_execution_challenge: { ids: ["trade.prepare"] },
|
|
163
166
|
strata_execution_prepare: { ids: ["trade.prepare"] },
|
|
@@ -181,6 +184,7 @@ const PLATFORM_TOOL_CAPABILITIES = {
|
|
|
181
184
|
strata_candles: { ids: ["market_data.candles.read"] },
|
|
182
185
|
strata_marks: { ids: ["market_data.marks.read"] },
|
|
183
186
|
strata_quote: { ids: ["quotes.market.read"] },
|
|
187
|
+
strata_trade: { ids: ["quotes.market.read"] },
|
|
184
188
|
strata_swap_quote: { ids: ["quotes.swap.read"] },
|
|
185
189
|
strata_exact_output_quote: { ids: ["quotes.exact_output.read"] },
|
|
186
190
|
strata_execution_challenge: { ids: ["execution.prepare"] },
|
|
@@ -393,6 +397,7 @@ async function platformMarketIdentities(platformClient) {
|
|
|
393
397
|
}
|
|
394
398
|
export async function createStrataMcpServer(options = {}) {
|
|
395
399
|
const client = strataClient(options);
|
|
400
|
+
const toolMode = options.toolMode ?? "simple";
|
|
396
401
|
const platformClient = options.platformClient ?? new StrataPlatformClient({
|
|
397
402
|
apiBase: options.apiBase,
|
|
398
403
|
timeoutMs: options.timeoutMs,
|
|
@@ -1269,8 +1274,9 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1269
1274
|
}));
|
|
1270
1275
|
const quote = registerTool("strata_quote", {
|
|
1271
1276
|
title: "Sonar quote",
|
|
1272
|
-
description: "Request a short-lived Sonar quote
|
|
1273
|
-
+ "
|
|
1277
|
+
description: "Request a short-lived Sonar quote. Use a market label and a human amount such as "
|
|
1278
|
+
+ "0.1 SOL, 20 USDC, or $20; exact input atoms remain available for advanced clients. "
|
|
1279
|
+
+ "Returns expected output, fees, price impact, and expiry.",
|
|
1274
1280
|
inputSchema: {
|
|
1275
1281
|
market: z
|
|
1276
1282
|
.string()
|
|
@@ -1278,11 +1284,18 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1278
1284
|
.max(128)
|
|
1279
1285
|
.describe("Market label such as SOL/USDC, or its public market ID."),
|
|
1280
1286
|
side: z.enum(["buy", "sell"]).describe("Buy or sell the market's base asset."),
|
|
1287
|
+
amount: z
|
|
1288
|
+
.string()
|
|
1289
|
+
.min(1)
|
|
1290
|
+
.max(64)
|
|
1291
|
+
.optional()
|
|
1292
|
+
.describe("Human input amount, for example 0.1 SOL, 20 USDC, or $20."),
|
|
1281
1293
|
amountInAtoms: z
|
|
1282
1294
|
.string()
|
|
1283
1295
|
.regex(/^[0-9]+$/)
|
|
1284
1296
|
.max(20)
|
|
1285
|
-
.
|
|
1297
|
+
.optional()
|
|
1298
|
+
.describe("Advanced: exact input amount in the input token's smallest atomic unit."),
|
|
1286
1299
|
maximumToleranceBps: z
|
|
1287
1300
|
.number()
|
|
1288
1301
|
.int()
|
|
@@ -1300,15 +1313,36 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1300
1313
|
idempotentHint: false,
|
|
1301
1314
|
openWorldHint: true,
|
|
1302
1315
|
},
|
|
1303
|
-
}, async ({ market, side, amountInAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
1316
|
+
}, async ({ market, side, amount, amountInAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
|
|
1317
|
+
if ((amount === undefined) === (amountInAtoms === undefined)) {
|
|
1318
|
+
return toolError("invalid_amount", "Give exactly one amount: a human value such as 0.1 SOL, or amountInAtoms for advanced use.", false);
|
|
1319
|
+
}
|
|
1320
|
+
let resolvedMarket = market;
|
|
1321
|
+
let resolvedAtoms = amountInAtoms;
|
|
1322
|
+
let display;
|
|
1323
|
+
if (amount !== undefined) {
|
|
1324
|
+
try {
|
|
1325
|
+
const parsed = humanQuoteAmount((await client.markets()).markets, market, side, amount);
|
|
1326
|
+
resolvedMarket = parsed.market.label;
|
|
1327
|
+
resolvedAtoms = parsed.atoms;
|
|
1328
|
+
display = {
|
|
1329
|
+
input: parsed.display,
|
|
1330
|
+
outputSymbol: parsed.outputSymbol,
|
|
1331
|
+
outputDecimals: parsed.outputDecimals,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
return toolError("invalid_amount", safeMessage(error), false);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1304
1338
|
const request = {
|
|
1305
|
-
market,
|
|
1339
|
+
market: resolvedMarket,
|
|
1306
1340
|
side,
|
|
1307
|
-
amountInAtoms,
|
|
1341
|
+
amountInAtoms: resolvedAtoms,
|
|
1308
1342
|
maximumToleranceBps,
|
|
1309
1343
|
};
|
|
1310
|
-
const response = await client.quote(request);
|
|
1311
|
-
return toolResult(response, quoteSummary(response));
|
|
1344
|
+
const response = await retryReadOnce(() => client.quote(request));
|
|
1345
|
+
return toolResult(response, quoteSummary(response, display));
|
|
1312
1346
|
}));
|
|
1313
1347
|
const exactOutputQuote = registerTool("strata_exact_output_quote", {
|
|
1314
1348
|
title: "Sonar exact-output quote",
|
|
@@ -1354,9 +1388,90 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1354
1388
|
amountOutAtoms,
|
|
1355
1389
|
maximumToleranceBps,
|
|
1356
1390
|
};
|
|
1357
|
-
const response = await client.quote(request);
|
|
1391
|
+
const response = await retryReadOnce(() => client.quote(request));
|
|
1358
1392
|
return toolResult(response, quoteSummary(response));
|
|
1359
1393
|
}));
|
|
1394
|
+
registerTool("strata_trade", {
|
|
1395
|
+
title: "Trade on Strata",
|
|
1396
|
+
description: "Quote and trade in one obvious tool. Human amounts such as 0.1 SOL and $20 are supported. "
|
|
1397
|
+
+ "Without a trading connection it returns the live read-only quote plus one setup link; "
|
|
1398
|
+
+ "with a session it follows the user's autonomy limits and may submit.",
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
market: z.string().min(1).max(128).describe("Market label, for example SOL/USDC."),
|
|
1401
|
+
side: z.enum(["buy", "sell"]),
|
|
1402
|
+
amount: z.string().min(1).max(64).optional()
|
|
1403
|
+
.describe("Human input amount, for example 0.1 SOL, 20 USDC, or $20."),
|
|
1404
|
+
amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional()
|
|
1405
|
+
.describe("Advanced alternative: exact input token atoms."),
|
|
1406
|
+
maximumToleranceBps: z.number().int().min(0).max(1_000).optional()
|
|
1407
|
+
.default(DEFAULT_MAXIMUM_TOLERANCE_BPS),
|
|
1408
|
+
idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
1409
|
+
},
|
|
1410
|
+
annotations: {
|
|
1411
|
+
readOnlyHint: false,
|
|
1412
|
+
destructiveHint: true,
|
|
1413
|
+
idempotentHint: false,
|
|
1414
|
+
openWorldHint: true,
|
|
1415
|
+
},
|
|
1416
|
+
}, async ({ market, side, amount, amountInAtoms, maximumToleranceBps, idempotencyKey }) => guardedTool(client, "quotes.read", async () => {
|
|
1417
|
+
if ((amount === undefined) === (amountInAtoms === undefined)) {
|
|
1418
|
+
return toolError("invalid_amount", "Give exactly one amount: a human value such as 0.1 SOL, or amountInAtoms for advanced use.", false);
|
|
1419
|
+
}
|
|
1420
|
+
const sonarMarkets = (await client.markets()).markets;
|
|
1421
|
+
let resolvedMarket = market;
|
|
1422
|
+
let resolvedAtoms = amountInAtoms;
|
|
1423
|
+
let inputDisplay = amountInAtoms ? `${amountInAtoms} input atoms` : amount;
|
|
1424
|
+
if (amount !== undefined) {
|
|
1425
|
+
try {
|
|
1426
|
+
const parsed = humanQuoteAmount(sonarMarkets, market, side, amount);
|
|
1427
|
+
resolvedMarket = parsed.market.label;
|
|
1428
|
+
resolvedAtoms = parsed.atoms;
|
|
1429
|
+
inputDisplay = parsed.display;
|
|
1430
|
+
}
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
return toolError("invalid_amount", safeMessage(error), false);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const freshQuote = await retryReadOnce(() => client.quote({
|
|
1436
|
+
market: resolvedMarket,
|
|
1437
|
+
side,
|
|
1438
|
+
amountInAtoms: resolvedAtoms,
|
|
1439
|
+
maximumToleranceBps,
|
|
1440
|
+
}));
|
|
1441
|
+
if (!options.sessionAutonomy) {
|
|
1442
|
+
return toolResult({
|
|
1443
|
+
executed: false,
|
|
1444
|
+
reason: "trading_not_connected",
|
|
1445
|
+
quote: freshQuote,
|
|
1446
|
+
connect_url: "https://stratabook.app/agents",
|
|
1447
|
+
}, `Live quote ready for ${inputDisplay}; no transaction was sent. Trading is not connected. `
|
|
1448
|
+
+ "Open https://stratabook.app/agents when you want to trade; read-only tools need no setup.");
|
|
1449
|
+
}
|
|
1450
|
+
const liveCatalog = await client.capabilities();
|
|
1451
|
+
if (!capabilityAvailable(liveCatalog, "trade.submit")) {
|
|
1452
|
+
return toolError("trading_temporarily_unavailable", "The quote works, but trading submission is temporarily unavailable. No transaction was sent.", true);
|
|
1453
|
+
}
|
|
1454
|
+
const sonar = sonarMarkets.find((candidate) => candidate.market_pda === freshQuote.market_id)
|
|
1455
|
+
?? sonarMarkets.find((candidate) => candidate.label === resolvedMarket);
|
|
1456
|
+
const notional = sonar
|
|
1457
|
+
? quoteNotionalUsd(freshQuote.side, freshQuote.amount_in_atoms, freshQuote.minimum_output_atoms, sonar.quote_decimals)
|
|
1458
|
+
: null;
|
|
1459
|
+
const resolver = new MarketMetaResolver(platformClient, async () => sonarMarkets, () => Date.now());
|
|
1460
|
+
const marketId = sonar ? await resolver.idForLabel(sonar.label) : null;
|
|
1461
|
+
const decision = decideAutonomy(options.sessionAutonomy, marketId ?? "", notional, Date.now());
|
|
1462
|
+
if (!decision.allow) {
|
|
1463
|
+
return toolResult({ executed: false, reason: decision.reason, quote: freshQuote }, `${decision.reason} The live quote is attached; no transaction was sent.`);
|
|
1464
|
+
}
|
|
1465
|
+
const receipt = await client.executeQuote({
|
|
1466
|
+
quote: freshQuote,
|
|
1467
|
+
ownerWallet: options.sessionAutonomy.ownerWallet,
|
|
1468
|
+
signer: options.sessionAutonomy.signer,
|
|
1469
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
1470
|
+
});
|
|
1471
|
+
if (notional !== null)
|
|
1472
|
+
options.sessionAutonomy.dailyBudget.record(notional, Date.now());
|
|
1473
|
+
return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${side} ${inputDisplay} as ${receipt.signature}.`);
|
|
1474
|
+
}));
|
|
1360
1475
|
registerTool("strata_swap_quote", {
|
|
1361
1476
|
title: "Sonar asset swap quote",
|
|
1362
1477
|
description: "Request short-lived exact-input customer economics between two opaque Strata asset IDs.",
|
|
@@ -1963,7 +2078,7 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1963
2078
|
makerCurrentPrepare,
|
|
1964
2079
|
makerCurrentSubmit,
|
|
1965
2080
|
];
|
|
1966
|
-
applyToolAvailability(registeredTools, initialCatalog, initialPlatformCatalog);
|
|
2081
|
+
applyToolAvailability(registeredTools, initialCatalog, initialPlatformCatalog, toolMode);
|
|
1967
2082
|
let closed = false;
|
|
1968
2083
|
const refresh = async () => {
|
|
1969
2084
|
if (closed)
|
|
@@ -1972,7 +2087,7 @@ export async function createStrataMcpServer(options = {}) {
|
|
|
1972
2087
|
client.capabilities(),
|
|
1973
2088
|
platformClient.discovery.read(),
|
|
1974
2089
|
]);
|
|
1975
|
-
applyToolAvailability(registeredTools, catalog, platformCatalog);
|
|
2090
|
+
applyToolAvailability(registeredTools, catalog, platformCatalog, toolMode);
|
|
1976
2091
|
};
|
|
1977
2092
|
const timer = setInterval(() => {
|
|
1978
2093
|
refresh().catch((error) => {
|
|
@@ -2007,19 +2122,22 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2007
2122
|
},
|
|
2008
2123
|
}, async () => {
|
|
2009
2124
|
const howToChange = {
|
|
2010
|
-
|
|
2011
|
-
per_trade_env: "STRATA_AUTONOMY_MAX_USD_PER_TRADE",
|
|
2012
|
-
per_day_env: "STRATA_AUTONOMY_MAX_USD_PER_DAY",
|
|
2013
|
-
markets_env: "STRATA_AUTONOMY_MARKETS (comma-separated opaque market IDs)",
|
|
2014
|
-
session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
|
|
2125
|
+
setup: "Open the Agents page, connect the owner wallet, register once, then copy the MCP trading config into your client's local settings.",
|
|
2015
2126
|
agents_page: "https://stratabook.app/agents",
|
|
2016
|
-
|
|
2127
|
+
generic_clients: "Claude Desktop, Cursor, Windsurf, Codex, or any local stdio MCP host",
|
|
2128
|
+
note: "Read-only tools need none of this. Only the user changes trading authority; an agent can never raise its own level.",
|
|
2129
|
+
advanced_environment_reference: {
|
|
2130
|
+
level_env: "STRATA_AUTONOMY = ask | limits | instant",
|
|
2131
|
+
per_trade_env: "STRATA_AUTONOMY_MAX_USD_PER_TRADE",
|
|
2132
|
+
per_day_env: "STRATA_AUTONOMY_MAX_USD_PER_DAY",
|
|
2133
|
+
markets_env: "STRATA_AUTONOMY_MARKETS (comma-separated opaque market IDs)",
|
|
2134
|
+
session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
|
|
2135
|
+
},
|
|
2017
2136
|
};
|
|
2018
2137
|
if (!autonomy) {
|
|
2019
|
-
return toolResult({ session_configured: false, level: "ask", how_to_change: howToChange }, "
|
|
2020
|
-
+ "
|
|
2021
|
-
+ "
|
|
2022
|
-
+ "then choose STRATA_AUTONOMY=limits or instant.");
|
|
2138
|
+
return toolResult({ session_configured: false, level: "ask", how_to_change: howToChange }, "Read-only is ready. Trading is not connected, so I cannot send transactions. "
|
|
2139
|
+
+ "If you want trading, open https://stratabook.app/agents and copy its MCP trading config "
|
|
2140
|
+
+ "into your client; never paste the session secret into chat.");
|
|
2023
2141
|
}
|
|
2024
2142
|
const { config } = autonomy;
|
|
2025
2143
|
const spentToday = autonomy.dailyBudget.spentToday(nowMs());
|
|
@@ -2077,12 +2195,12 @@ function registerAutonomyTools(registerTool, client, platformClient, autonomy, n
|
|
|
2077
2195
|
openWorldHint: true,
|
|
2078
2196
|
},
|
|
2079
2197
|
}, async (args) => guardedTool(client, "trade.submit", async () => {
|
|
2080
|
-
const quote = await client.quote({
|
|
2198
|
+
const quote = await retryReadOnce(() => client.quote({
|
|
2081
2199
|
market: args.market,
|
|
2082
2200
|
side: args.side,
|
|
2083
2201
|
amountInAtoms: args.amountInAtoms,
|
|
2084
2202
|
...(args.toleranceBps === undefined ? {} : { toleranceBps: args.toleranceBps }),
|
|
2085
|
-
});
|
|
2203
|
+
}));
|
|
2086
2204
|
const sonar = (await client.markets()).markets.find((market) => market.market_pda === quote.market_id);
|
|
2087
2205
|
const notional = sonar
|
|
2088
2206
|
? quoteNotionalUsd(quote.side, quote.amount_in_atoms, quote.minimum_output_atoms, sonar.quote_decimals)
|
|
@@ -2261,11 +2379,12 @@ function requirementAvailable(requirement, available) {
|
|
|
2261
2379
|
? requirement.ids.some(available)
|
|
2262
2380
|
: requirement.ids.every(available);
|
|
2263
2381
|
}
|
|
2264
|
-
function applyToolAvailability(handles, catalog, platformCatalog) {
|
|
2382
|
+
function applyToolAvailability(handles, catalog, platformCatalog, toolMode) {
|
|
2265
2383
|
for (const [name, tool] of handles) {
|
|
2266
2384
|
const legacyAvailable = requirementAvailable(LEGACY_TOOL_CAPABILITIES[name], (id) => capabilityAvailable(catalog, id));
|
|
2267
2385
|
const platformAvailable = requirementAvailable(PLATFORM_TOOL_CAPABILITIES[name], (id) => platformCapabilityAvailable(platformCatalog, id));
|
|
2268
|
-
|
|
2386
|
+
const modeAvailable = toolMode === "advanced" || SIMPLE_TOOL_NAMES.has(name);
|
|
2387
|
+
setToolEnabled(tool, modeAvailable && legacyAvailable && platformAvailable);
|
|
2269
2388
|
}
|
|
2270
2389
|
}
|
|
2271
2390
|
function setToolEnabled(tool, enabled) {
|
|
@@ -2284,7 +2403,7 @@ async function guardedTool(client, capabilityId, operation) {
|
|
|
2284
2403
|
}
|
|
2285
2404
|
catch (error) {
|
|
2286
2405
|
if (error instanceof StrataApiError) {
|
|
2287
|
-
return toolError(error.code, error.message, error.retryable);
|
|
2406
|
+
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable);
|
|
2288
2407
|
}
|
|
2289
2408
|
return toolError("request_failed", safeMessage(error), true);
|
|
2290
2409
|
}
|
|
@@ -2295,16 +2414,35 @@ async function safeTool(operation) {
|
|
|
2295
2414
|
}
|
|
2296
2415
|
catch (error) {
|
|
2297
2416
|
if (error instanceof StrataApiError) {
|
|
2298
|
-
return toolError(error.code, error.message, error.retryable);
|
|
2417
|
+
return toolError(error.code, friendlyApiError(error.code, error.message), error.retryable);
|
|
2299
2418
|
}
|
|
2300
2419
|
return toolError("request_failed", safeMessage(error), true);
|
|
2301
2420
|
}
|
|
2302
2421
|
}
|
|
2422
|
+
/** Retry an idempotent public read once; trading writes are deliberately never retried here. */
|
|
2423
|
+
async function retryReadOnce(operation) {
|
|
2424
|
+
try {
|
|
2425
|
+
return await operation();
|
|
2426
|
+
}
|
|
2427
|
+
catch (error) {
|
|
2428
|
+
if (!(error instanceof StrataApiError) || !error.retryable)
|
|
2429
|
+
throw error;
|
|
2430
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2431
|
+
return operation();
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2303
2434
|
/**
|
|
2304
2435
|
* One line that keeps the two numbers apart: price impact is measured from the
|
|
2305
2436
|
* book; the tolerance is the caller's own floor.
|
|
2306
2437
|
*/
|
|
2307
|
-
function quoteSummary(response) {
|
|
2438
|
+
function quoteSummary(response, display) {
|
|
2439
|
+
if (display) {
|
|
2440
|
+
const output = `${formatAtoms(response.amount_out_atoms, display.outputDecimals)} ${display.outputSymbol}`;
|
|
2441
|
+
const minimum = `${formatAtoms(response.minimum_output_atoms, display.outputDecimals)} ${display.outputSymbol}`;
|
|
2442
|
+
return (`Sonar ${response.side} quote: ${display.input} → about ${output}; minimum ${minimum}; `
|
|
2443
|
+
+ `price impact ${response.price_impact_pct}%; tolerance ${response.maximum_tolerance_bps} bps. `
|
|
2444
|
+
+ `This is a read-only quote and expires at ${response.expires_at_ms}.`);
|
|
2445
|
+
}
|
|
2308
2446
|
return (`Sonar ${response.side} quote: ${response.amount_in_consumed_atoms} input atoms for `
|
|
2309
2447
|
+ `${response.amount_out_atoms} user-net output atoms; price impact ${response.price_impact_pct}% `
|
|
2310
2448
|
+ `(measured from the book); your tolerance ${response.maximum_tolerance_bps} bps, so the `
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Market } from "@stratabook/sdk";
|
|
2
|
+
export type StrataMcpToolMode = "simple" | "advanced";
|
|
3
|
+
/**
|
|
4
|
+
* The default surface is intentionally small enough for an agent to choose a
|
|
5
|
+
* useful tool directly. `advanced` keeps the complete protocol machinery for
|
|
6
|
+
* integrators that need explicit challenge / prepare / submit control.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SIMPLE_TOOL_NAMES: Set<string>;
|
|
9
|
+
export declare function parseToolMode(raw: string | undefined): StrataMcpToolMode;
|
|
10
|
+
export interface HumanQuoteAmount {
|
|
11
|
+
readonly atoms: string;
|
|
12
|
+
readonly market: Market;
|
|
13
|
+
readonly inputSymbol: string;
|
|
14
|
+
readonly inputDecimals: number;
|
|
15
|
+
readonly outputSymbol: string;
|
|
16
|
+
readonly outputDecimals: number;
|
|
17
|
+
readonly display: string;
|
|
18
|
+
}
|
|
19
|
+
/** Resolve friendly market spellings without making opaque IDs stop working. */
|
|
20
|
+
export declare function resolveMarket(markets: readonly Market[], requested: string): Market;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an exact decimal amount to atoms without ever passing through a
|
|
23
|
+
* floating-point number. For buys the input is quote; for sells it is base.
|
|
24
|
+
*/
|
|
25
|
+
export declare function humanQuoteAmount(markets: readonly Market[], requestedMarket: string, side: "buy" | "sell", amount: string): HumanQuoteAmount;
|
|
26
|
+
export declare function formatAtoms(atoms: string, decimals: number): string;
|
|
27
|
+
export declare function friendlyApiError(code: string, message: string): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default surface is intentionally small enough for an agent to choose a
|
|
3
|
+
* useful tool directly. `advanced` keeps the complete protocol machinery for
|
|
4
|
+
* integrators that need explicit challenge / prepare / submit control.
|
|
5
|
+
*/
|
|
6
|
+
export const SIMPLE_TOOL_NAMES = new Set([
|
|
7
|
+
"strata_markets",
|
|
8
|
+
"strata_quote",
|
|
9
|
+
"strata_book",
|
|
10
|
+
"strata_trades",
|
|
11
|
+
"strata_candles",
|
|
12
|
+
"strata_marks",
|
|
13
|
+
"strata_portfolio",
|
|
14
|
+
"strata_market_making_status",
|
|
15
|
+
"strata_market_making_prepare",
|
|
16
|
+
"strata_market_making_submit_and_wait",
|
|
17
|
+
"strata_autonomy",
|
|
18
|
+
"strata_trade",
|
|
19
|
+
]);
|
|
20
|
+
export function parseToolMode(raw) {
|
|
21
|
+
const value = raw?.trim().toLowerCase() || "simple";
|
|
22
|
+
if (value === "simple" || value === "advanced")
|
|
23
|
+
return value;
|
|
24
|
+
throw new TypeError("mode must be simple or advanced");
|
|
25
|
+
}
|
|
26
|
+
/** Resolve friendly market spellings without making opaque IDs stop working. */
|
|
27
|
+
export function resolveMarket(markets, requested) {
|
|
28
|
+
const trimmed = requested.trim();
|
|
29
|
+
const normalized = normalizedMarketLabel(trimmed);
|
|
30
|
+
const match = markets.find((market) => market.market_pda === trimmed || normalizedMarketLabel(market.label) === normalized);
|
|
31
|
+
if (match)
|
|
32
|
+
return match;
|
|
33
|
+
throw new TypeError(`Unknown market ${trimmed}. Try a label from strata_markets, for example SOL/USDC.`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert an exact decimal amount to atoms without ever passing through a
|
|
37
|
+
* floating-point number. For buys the input is quote; for sells it is base.
|
|
38
|
+
*/
|
|
39
|
+
export function humanQuoteAmount(markets, requestedMarket, side, amount) {
|
|
40
|
+
const market = resolveMarket(markets, requestedMarket);
|
|
41
|
+
// Sonar's base/quote fields may be mint addresses; the public label carries
|
|
42
|
+
// the display symbols agents and users actually type.
|
|
43
|
+
const [labelBase, labelQuote] = market.label.split("/", 2).map((part) => part.trim());
|
|
44
|
+
const inputSymbol = side === "buy"
|
|
45
|
+
? labelQuote || market.quote
|
|
46
|
+
: labelBase || market.base;
|
|
47
|
+
const inputDecimals = side === "buy" ? market.quote_decimals : market.base_decimals;
|
|
48
|
+
const outputSymbol = side === "buy"
|
|
49
|
+
? labelBase || market.base
|
|
50
|
+
: labelQuote || market.quote;
|
|
51
|
+
const outputDecimals = side === "buy" ? market.base_decimals : market.quote_decimals;
|
|
52
|
+
const value = amount.trim();
|
|
53
|
+
const dollar = value.startsWith("$");
|
|
54
|
+
const match = /^(?:\$\s*)?([0-9]+)(?:\.([0-9]+))?(?:\s*([A-Za-z0-9._-]+))?$/.exec(value);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new TypeError(`amount must look like 0.1 ${inputSymbol} or 20 ${inputSymbol}`);
|
|
57
|
+
}
|
|
58
|
+
if (dollar && !isDollarSymbol(inputSymbol)) {
|
|
59
|
+
throw new TypeError(`$ amounts are only valid when the input token is USD-like; this input is ${inputSymbol}`);
|
|
60
|
+
}
|
|
61
|
+
const suppliedSymbol = match[3];
|
|
62
|
+
if (suppliedSymbol && suppliedSymbol.toLowerCase() !== inputSymbol.toLowerCase()) {
|
|
63
|
+
throw new TypeError(`This ${side} uses ${inputSymbol} as input, not ${suppliedSymbol}`);
|
|
64
|
+
}
|
|
65
|
+
const fraction = match[2] ?? "";
|
|
66
|
+
if (fraction.length > inputDecimals) {
|
|
67
|
+
throw new TypeError(`${inputSymbol} supports at most ${inputDecimals} decimal places`);
|
|
68
|
+
}
|
|
69
|
+
const scale = 10n ** BigInt(inputDecimals);
|
|
70
|
+
const atoms = BigInt(match[1]) * scale
|
|
71
|
+
+ BigInt((fraction + "0".repeat(inputDecimals)).slice(0, inputDecimals) || "0");
|
|
72
|
+
if (atoms <= 0n || atoms > 18446744073709551615n) {
|
|
73
|
+
throw new TypeError("amount is outside the supported token range");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
atoms: atoms.toString(),
|
|
77
|
+
market,
|
|
78
|
+
inputSymbol,
|
|
79
|
+
inputDecimals,
|
|
80
|
+
outputSymbol,
|
|
81
|
+
outputDecimals,
|
|
82
|
+
display: `${formatAtoms(atoms.toString(), inputDecimals)} ${inputSymbol}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function formatAtoms(atoms, decimals) {
|
|
86
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(atoms))
|
|
87
|
+
return atoms;
|
|
88
|
+
if (decimals === 0)
|
|
89
|
+
return atoms;
|
|
90
|
+
const padded = atoms.padStart(decimals + 1, "0");
|
|
91
|
+
const whole = padded.slice(0, -decimals);
|
|
92
|
+
const fraction = padded.slice(-decimals).replace(/0+$/, "");
|
|
93
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
94
|
+
}
|
|
95
|
+
export function friendlyApiError(code, message) {
|
|
96
|
+
const normalized = code.toLowerCase();
|
|
97
|
+
if (normalized.includes("session") || normalized.includes("delegate")) {
|
|
98
|
+
return "Trading is not connected. Open https://stratabook.app/agents, connect your wallet, and copy the MCP trading config for your client. Read-only tools still work now.";
|
|
99
|
+
}
|
|
100
|
+
if (normalized.includes("balance") || normalized.includes("fund")) {
|
|
101
|
+
return "There is not enough available balance for this action. Deposit funds or reduce the amount, then try again.";
|
|
102
|
+
}
|
|
103
|
+
if (normalized.includes("warm") || normalized.includes("temporar") || normalized.includes("unavailable")) {
|
|
104
|
+
return "This market is temporarily warming up. Try again shortly; no transaction was sent.";
|
|
105
|
+
}
|
|
106
|
+
if (normalized.includes("expired")) {
|
|
107
|
+
return "The quote or prepared transaction expired. Request a fresh one and try again.";
|
|
108
|
+
}
|
|
109
|
+
return message;
|
|
110
|
+
}
|
|
111
|
+
function normalizedMarketLabel(value) {
|
|
112
|
+
return value.trim().toUpperCase().replace(/[\s:_-]+/g, "/");
|
|
113
|
+
}
|
|
114
|
+
function isDollarSymbol(symbol) {
|
|
115
|
+
return ["USD", "USDC", "USDT", "USDG"].includes(symbol.toUpperCase());
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratabook/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Connect AI agents to Strata markets and Sonar quotes with MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
48
|
-
"@stratabook/sdk": "0.2.
|
|
48
|
+
"@stratabook/sdk": "0.2.12",
|
|
49
49
|
"zod": "^3.25.76"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|