@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 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 capability-gated MCP access to Strata and Sonar. The server delegates
4
- to `@stratabook/sdk`: it follows the live capability catalog and contains no
5
- separate quote or execution logic.
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 gating is a runtime safety check so clients stop if policy changes;
10
- it does **not** mean quotes are inactive.
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
- Example client configuration:
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
- The tools currently available are:
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. The
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. Amounts are token atoms encoded as base-10
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 < argv.length; index++) {
13
- const token = argv[index];
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 = argv[index + 1];
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
- This server exposes capability-gated quote and execution operations. The external
65
- agent owner controls permission and signing. Strata accepts public keys,
66
- signatures, and signed transactions, never private keys or seed phrases.
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 before every objective. Never infer permission from documentation, package support, or an earlier session.";
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: "Read the compact action graph and the complete platform graph, then identify which operation and workflow nodes are live. The external agent owner configures its permissions and signer authority; static documentation never enables a Strata operation.";
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: "List catalog assets and markets, select currently available product identities, and use discovered decimals. Do not guess identifiers or token decimals.";
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: "Represent token amounts as unsigned base-10 atomic strings. Never pass settlement amounts through floating-point arithmetic.";
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: "Request a fresh Sonar quote using either a selected market and explicit side or selected input/output asset IDs, plus exact input atoms and the execution tolerance supplied by the external agent.";
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 Agent Harness 1.0. Start every objective with strata_capabilities, then strata_action_graph, then strata_platform_graph, then strata_status, then strata_markets. Read strata://agent-harness/v1, strata://action-graph/v1, and strata://platform-graph/v2. The external agent owner controls permission and signer authority. Strata accepts public keys, detached signatures, and signed transactions, never private keys or seed phrases. Resolve a market and side or catalog input/output asset IDs, plus exact input atoms and tolerance, before strata_quote or strata_swap_quote. When portfolio.read is live, read the owner's live Vault portfolio before sizing any action and treat null USD totals as an incomplete valuation, never as zero. When mm.status.read is live, reconcile the owner's own maker products, exposure, and dead-man guards through the authorized status read before and after every maker action, and when mm.fills.stream is live keep the official SDK's authenticated maker stream open to apply contiguous maker fills and exposure changes. 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; the token carries no signing authority and survives stateless HTTP requests. When algos.twap.stream is live, follow TWAP progress through the official SDK's sequenced TWAP stream instead of polling, and when execution.stream is live watch prepared execution handles through the sequenced execution stream. Before selecting a maker transport, read the externally authorized owner-scoped maker reputation record and follow its signed-quote eligibility, cadence, and tier-progress fields. Treat amounts as unsigned base-10 token atoms; check quote bindings, labelled fees, minimum output, and expiry. To execute or control a resting order: request a challenge, verify its quote or exact opaque order bindings, sign canonical authorization bytes externally, prepare, verify and sign the returned transaction externally, then submit with idempotency. When websocket order transport is live, prefer the official SDK persistent command stream, choose explicit self-trade prevention, keep its durable dead-man guard armed for resting exposure, and distinguish immediate RPC broadcast from pushed terminal chain status. If submission is ambiguous, recover durable status with the same control ID and idempotency key. Order control supports place, cancel, bounded cancel-all, atomic replace, and atomic heterogeneous batches of up to six operations. Stop on ambiguity, sequence gaps, unavailable capabilities, paused markets, unsupported contracts, inconsistent bindings, expiry, or missing signer authority.";
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 before every objective. Never infer permission from documentation, package support, or an earlier session."
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": "Read the compact action graph and the complete platform graph, then identify which operation and workflow nodes are live. The external agent owner configures its permissions and signer authority; static documentation never enables a Strata operation."
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": "List catalog assets and markets, select currently available product identities, and use discovered decimals. Do not guess identifiers or token decimals."
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": "Represent token amounts as unsigned base-10 atomic strings. Never pass settlement amounts through floating-point arithmetic."
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": "Request a fresh Sonar quote using either a selected market and explicit side or selected input/output asset IDs, plus exact input atoms and the execution tolerance supplied by the external agent."
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 Agent Harness 1.0. Start every objective with strata_capabilities, then strata_action_graph, then strata_platform_graph, then strata_status, then strata_markets. Read strata://agent-harness/v1, strata://action-graph/v1, and strata://platform-graph/v2. The external agent owner controls permission and signer authority. Strata accepts public keys, detached signatures, and signed transactions, never private keys or seed phrases. Resolve a market and side or catalog input/output asset IDs, plus exact input atoms and tolerance, before strata_quote or strata_swap_quote. When portfolio.read is live, read the owner's live Vault portfolio before sizing any action and treat null USD totals as an incomplete valuation, never as zero. When mm.status.read is live, reconcile the owner's own maker products, exposure, and dead-man guards through the authorized status read before and after every maker action, and when mm.fills.stream is live keep the official SDK's authenticated maker stream open to apply contiguous maker fills and exposure changes. 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; the token carries no signing authority and survives stateless HTTP requests. When algos.twap.stream is live, follow TWAP progress through the official SDK's sequenced TWAP stream instead of polling, and when execution.stream is live watch prepared execution handles through the sequenced execution stream. Before selecting a maker transport, read the externally authorized owner-scoped maker reputation record and follow its signed-quote eligibility, cadence, and tier-progress fields. Treat amounts as unsigned base-10 token atoms; check quote bindings, labelled fees, minimum output, and expiry. To execute or control a resting order: request a challenge, verify its quote or exact opaque order bindings, sign canonical authorization bytes externally, prepare, verify and sign the returned transaction externally, then submit with idempotency. When websocket order transport is live, prefer the official SDK persistent command stream, choose explicit self-trade prevention, keep its durable dead-man guard armed for resting exposure, and distinguish immediate RPC broadcast from pushed terminal chain status. If submission is ambiguous, recover durable status with the same control ID and idempotency key. Order control supports place, cancel, bounded cancel-all, atomic replace, and atomic heterogeneous batches of up to six operations. Stop on ambiguity, sequence gaps, unavailable capabilities, paused markets, unsupported contracts, inconsistent bindings, expiry, or missing signer authority.";
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.";
@@ -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:
@@ -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 for a Strata market. Returns expected "
1273
- + "output, minimum output, fees, price impact, and expiry.",
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
- .describe("Exact input amount in the input token's smallest atomic unit."),
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
- level_env: "STRATA_AUTONOMY = ask | limits | instant",
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
- note: "Only the user changes these; an agent can offer but never raise its own level.",
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 }, "Autonomy: ask (no session key configured). I can prepare trades for you to sign, "
2020
- + "but I cannot sign any myself. To let me trade unattended, register a Vault session "
2021
- + "key on the Agents page and set STRATA_SESSION_SECRET_KEY (+ STRATA_OWNER_WALLET), "
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
- setToolEnabled(tool, legacyAvailable && platformAvailable);
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.10",
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.10",
48
+ "@stratabook/sdk": "0.2.12",
49
49
  "zod": "^3.25.76"
50
50
  },
51
51
  "devDependencies": {