@stratabook/mcp 0.2.12 → 0.2.13
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 +13 -0
- package/README.md +20 -2
- package/dist/src/cli.js +42 -10
- package/dist/src/generated-harness.d.ts +1 -1
- package/dist/src/generated-harness.js +1 -1
- package/dist/src/pairing.d.ts +26 -0
- package/dist/src/pairing.js +293 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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.13
|
|
7
|
+
|
|
8
|
+
- Add `strata-mcp connect`: generate the session secret locally, open the
|
|
9
|
+
owner-wallet registration page with only its public key, verify activation,
|
|
10
|
+
and save a mode-0600 local credential. No secret copying, chat, config-file,
|
|
11
|
+
or environment-variable setup is required.
|
|
12
|
+
- Add `strata-mcp disconnect` for exact-session on-chain revocation followed
|
|
13
|
+
by local credential removal.
|
|
14
|
+
- Automatically load the private local connection for MCP and `doctor`, while
|
|
15
|
+
preserving explicit environment variables for managed deployments.
|
|
16
|
+
- Keep read-only tools immediate and client-neutral, and document hosted HTTP,
|
|
17
|
+
Cursor, Claude Code, Codex, Windsurf, and generic stdio installation.
|
|
18
|
+
|
|
6
19
|
## 0.2.12
|
|
7
20
|
|
|
8
21
|
- Make MCP client-neutral: the primary setup works with any stdio or
|
package/README.md
CHANGED
|
@@ -22,8 +22,22 @@ Read-only use needs no wallet, key, autonomy setting, or environment variable:
|
|
|
22
22
|
npx -y @stratabook/mcp
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Trading setup is optional. When needed, the local MCP generates its own key,
|
|
26
|
+
opens the owner-wallet page, and saves the secret in a mode-0600 local file.
|
|
27
|
+
The browser and Strata receive only the public key:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y @stratabook/mcp connect
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
There is no secret to paste into chat, no environment-variable screen, and no
|
|
34
|
+
client config to edit after the read-only server is installed. Restart or
|
|
35
|
+
refresh the MCP client after the browser confirms connection. Revoke the exact
|
|
36
|
+
session and delete its local credential with:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx -y @stratabook/mcp disconnect
|
|
40
|
+
```
|
|
27
41
|
|
|
28
42
|
Generic configuration for Claude Desktop, Cursor, Windsurf, and other
|
|
29
43
|
JSON-config MCP clients:
|
|
@@ -51,6 +65,10 @@ Check the whole read-only connection without placing a trade:
|
|
|
51
65
|
npx -y @stratabook/mcp doctor
|
|
52
66
|
```
|
|
53
67
|
|
|
68
|
+
The private credential defaults to `~/.config/strata/mcp.json` on macOS/Linux
|
|
69
|
+
and `%APPDATA%\Strata\mcp.json` on Windows. Override it with
|
|
70
|
+
`STRATA_MCP_CREDENTIALS_FILE` when a managed secret volume is required.
|
|
71
|
+
|
|
54
72
|
The compact default exposes the tools ordinary users need:
|
|
55
73
|
|
|
56
74
|
- `strata_markets`, `strata_marks`, `strata_book`, `strata_candles`, `strata_trades`
|
package/dist/src/cli.js
CHANGED
|
@@ -8,16 +8,26 @@ import { createStrataMcpServer, probeStrataMcpReadiness } from "./server.js";
|
|
|
8
8
|
import { sessionAutonomyFromEnv } from "./autonomy.js";
|
|
9
9
|
import { SERVER_VERSION } from "./version.js";
|
|
10
10
|
import { friendlyApiError, humanQuoteAmount, parseToolMode } from "./usability.js";
|
|
11
|
+
import { DEFAULT_PAIRING_WEB_BASE, loadTradingEnvironment, runLocalPairing, tradingCredentialsPath, } from "./pairing.js";
|
|
11
12
|
function parse(argv) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const knownCommands = new Set(["doctor", "connect", "disconnect"]);
|
|
14
|
+
const first = argv[0];
|
|
15
|
+
const command = first && knownCommands.has(first)
|
|
16
|
+
? first
|
|
17
|
+
: "serve";
|
|
18
|
+
const args = command === "serve" ? argv : argv.slice(1);
|
|
14
19
|
const values = new Map();
|
|
20
|
+
let openBrowser = true;
|
|
15
21
|
for (let index = 0; index < args.length; index++) {
|
|
16
22
|
const token = args[index];
|
|
17
23
|
if (token === "--help" || token === "-h") {
|
|
18
24
|
help();
|
|
19
25
|
process.exit(0);
|
|
20
26
|
}
|
|
27
|
+
if (token === "--no-open") {
|
|
28
|
+
openBrowser = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
21
31
|
if (!token?.startsWith("--"))
|
|
22
32
|
throw new Error(`unexpected argument: ${token}`);
|
|
23
33
|
const next = args[index + 1];
|
|
@@ -43,6 +53,11 @@ function parse(argv) {
|
|
|
43
53
|
timeoutMs,
|
|
44
54
|
host,
|
|
45
55
|
port,
|
|
56
|
+
openBrowser,
|
|
57
|
+
webBase: values.get("web-base") ?? process.env.STRATA_MCP_WEB_BASE ?? DEFAULT_PAIRING_WEB_BASE,
|
|
58
|
+
...(values.get("credentials-file") === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { credentialsFile: values.get("credentials-file") }),
|
|
46
61
|
};
|
|
47
62
|
}
|
|
48
63
|
function boundedInteger(raw, name, min, max) {
|
|
@@ -58,6 +73,8 @@ function help() {
|
|
|
58
73
|
Usage:
|
|
59
74
|
strata-mcp
|
|
60
75
|
strata-mcp doctor
|
|
76
|
+
strata-mcp connect
|
|
77
|
+
strata-mcp disconnect
|
|
61
78
|
strata-mcp --transport http [--host localhost] [--port 8787]
|
|
62
79
|
|
|
63
80
|
Options:
|
|
@@ -67,13 +84,16 @@ Options:
|
|
|
67
84
|
--timeout-ms N Upstream timeout, 250..60000 (default: 10000)
|
|
68
85
|
--host HOST HTTP bind host (default: localhost)
|
|
69
86
|
--port N HTTP port (default: 8787)
|
|
87
|
+
--credentials-file PATH Override the private local trading credential file
|
|
88
|
+
--no-open Print the wallet pairing URL without opening a browser
|
|
70
89
|
|
|
71
90
|
Read-only markets, books, and Sonar quotes work immediately. Run strata-mcp
|
|
72
|
-
doctor to check them.
|
|
73
|
-
|
|
91
|
+
doctor to check them. Run strata-mcp connect once to register a local session
|
|
92
|
+
with the owner wallet; there are no secrets to copy or environment variables
|
|
93
|
+
to edit. Run strata-mcp disconnect to revoke it. Strata never asks for seed phrases.
|
|
74
94
|
`);
|
|
75
95
|
}
|
|
76
|
-
async function runDoctor(options) {
|
|
96
|
+
async function runDoctor(options, sessionEnv) {
|
|
77
97
|
const client = new StrataClient({ apiBase: options.apiBase, timeoutMs: options.timeoutMs });
|
|
78
98
|
process.stdout.write("Strata MCP doctor\n\n");
|
|
79
99
|
try {
|
|
@@ -95,10 +115,10 @@ async function runDoctor(options) {
|
|
|
95
115
|
else {
|
|
96
116
|
process.stdout.write("○ SOL/USDC is not currently listed; quote check skipped\n");
|
|
97
117
|
}
|
|
98
|
-
const sessionConfigured = Boolean(
|
|
118
|
+
const sessionConfigured = Boolean(sessionEnv.STRATA_SESSION_SECRET_KEY && sessionEnv.STRATA_OWNER_WALLET);
|
|
99
119
|
process.stdout.write(sessionConfigured
|
|
100
120
|
? "✓ Trading connection found (no transaction sent)\n"
|
|
101
|
-
: "○ Trading is not connected; read-only use is ready.
|
|
121
|
+
: "○ Trading is not connected; read-only use is ready. Run strata-mcp connect when needed.\n");
|
|
102
122
|
}
|
|
103
123
|
catch (error) {
|
|
104
124
|
if (error instanceof StrataApiError) {
|
|
@@ -202,15 +222,27 @@ function safeError(error) {
|
|
|
202
222
|
}
|
|
203
223
|
async function main() {
|
|
204
224
|
const options = parse(process.argv.slice(2));
|
|
225
|
+
if (options.command === "connect" || options.command === "disconnect") {
|
|
226
|
+
await runLocalPairing({
|
|
227
|
+
action: options.command,
|
|
228
|
+
webBase: options.webBase,
|
|
229
|
+
apiBase: options.apiBase,
|
|
230
|
+
openBrowser: options.openBrowser,
|
|
231
|
+
...(options.credentialsFile === undefined ? {} : { credentialsFile: options.credentialsFile }),
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const sessionEnv = await loadTradingEnvironment(process.env);
|
|
205
236
|
if (options.command === "doctor") {
|
|
206
|
-
await runDoctor(options);
|
|
237
|
+
await runDoctor(options, sessionEnv);
|
|
207
238
|
return;
|
|
208
239
|
}
|
|
209
|
-
const sessionAutonomy = await sessionAutonomyFromEnv(
|
|
240
|
+
const sessionAutonomy = await sessionAutonomyFromEnv(sessionEnv);
|
|
210
241
|
const withSession = sessionAutonomy ? { ...options, sessionAutonomy } : options;
|
|
211
242
|
if (sessionAutonomy) {
|
|
212
243
|
process.stderr.write(`[strata-mcp] session autonomy: ${sessionAutonomy.config.level} `
|
|
213
|
-
+ `(wallet ${sessionAutonomy.ownerWallet.slice(0, 6)}…, session ${sessionAutonomy.signer.publicKey.slice(0, 6)}
|
|
244
|
+
+ `(wallet ${sessionAutonomy.ownerWallet.slice(0, 6)}…, session ${sessionAutonomy.signer.publicKey.slice(0, 6)}…, `
|
|
245
|
+
+ `credentials ${process.env.STRATA_SESSION_SECRET_KEY ? "environment" : tradingCredentialsPath(process.env)})\n`);
|
|
214
246
|
}
|
|
215
247
|
if (withSession.transport === "stdio")
|
|
216
248
|
await runStdio(withSession);
|
|
@@ -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 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.";
|
|
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. The user can run npx -y @stratabook/mcp connect to generate and store the secret locally while the Agents page receives only its public key. 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.";
|
|
@@ -789,4 +789,4 @@ export const STRATA_ACTION_GRAPH = {
|
|
|
789
789
|
]
|
|
790
790
|
};
|
|
791
791
|
export const STRATA_ACTION_GRAPH_URI = "strata://action-graph/v1";
|
|
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.";
|
|
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. The user can run npx -y @stratabook/mcp connect to generate and store the secret locally while the Agents page receives only its public key. 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.";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const DEFAULT_PAIRING_WEB_BASE = "https://stratabook.app";
|
|
2
|
+
export interface StoredTradingConnection {
|
|
3
|
+
readonly schema_version: 1;
|
|
4
|
+
readonly owner_wallet: string;
|
|
5
|
+
readonly session_public_key: string;
|
|
6
|
+
readonly session_secret_key: string;
|
|
7
|
+
readonly autonomy: "ask" | "limits" | "instant";
|
|
8
|
+
readonly connected_at_ms: number;
|
|
9
|
+
}
|
|
10
|
+
export type PairingAction = "connect" | "disconnect";
|
|
11
|
+
export interface PairingOptions {
|
|
12
|
+
readonly action: PairingAction;
|
|
13
|
+
readonly webBase?: string;
|
|
14
|
+
readonly openBrowser?: boolean;
|
|
15
|
+
readonly credentialsFile?: string;
|
|
16
|
+
readonly apiBase?: string;
|
|
17
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
readonly nowMs?: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare function tradingCredentialsPath(env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, home?: string): string;
|
|
21
|
+
export declare function readTradingConnection(path?: string): Promise<StoredTradingConnection | null>;
|
|
22
|
+
export declare function writeTradingConnection(connection: StoredTradingConnection, path?: string): Promise<void>;
|
|
23
|
+
export declare function removeTradingConnection(path?: string): Promise<void>;
|
|
24
|
+
/** Explicit environment variables win; otherwise load the private local file. */
|
|
25
|
+
export declare function loadTradingEnvironment(env?: Readonly<Record<string, string | undefined>>): Promise<Record<string, string | undefined>>;
|
|
26
|
+
export declare function runLocalPairing(options: PairingOptions): Promise<void>;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { DEFAULT_API_BASE, generateSessionKeypair, } from "@stratabook/sdk";
|
|
8
|
+
const PUBLIC_KEY_PATTERN = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
9
|
+
const SECRET_KEY_PATTERN = /^[1-9A-HJ-NP-Za-km-z]{80,90}$/;
|
|
10
|
+
const PAIRING_TIMEOUT_MS = 10 * 60_000;
|
|
11
|
+
export const DEFAULT_PAIRING_WEB_BASE = "https://stratabook.app";
|
|
12
|
+
function validPublicKey(value) {
|
|
13
|
+
return typeof value === "string" && PUBLIC_KEY_PATTERN.test(value);
|
|
14
|
+
}
|
|
15
|
+
function validSecretKey(value) {
|
|
16
|
+
return typeof value === "string" && SECRET_KEY_PATTERN.test(value);
|
|
17
|
+
}
|
|
18
|
+
export function tradingCredentialsPath(env = process.env, platform = process.platform, home = homedir()) {
|
|
19
|
+
const override = env.STRATA_MCP_CREDENTIALS_FILE?.trim();
|
|
20
|
+
if (override)
|
|
21
|
+
return resolve(override);
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
const appData = env.APPDATA?.trim()
|
|
24
|
+
|| join(env.USERPROFILE?.trim() || home, "AppData", "Roaming");
|
|
25
|
+
return join(appData, "Strata", "mcp.json");
|
|
26
|
+
}
|
|
27
|
+
const configRoot = env.XDG_CONFIG_HOME?.trim() || join(home, ".config");
|
|
28
|
+
return join(configRoot, "strata", "mcp.json");
|
|
29
|
+
}
|
|
30
|
+
export async function readTradingConnection(path = tradingCredentialsPath()) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readFile(path, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ENOENT")
|
|
37
|
+
return null;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(`Strata trading credentials are not valid JSON: ${path}`);
|
|
46
|
+
}
|
|
47
|
+
if (!value || typeof value !== "object") {
|
|
48
|
+
throw new Error(`Strata trading credentials are invalid: ${path}`);
|
|
49
|
+
}
|
|
50
|
+
const candidate = value;
|
|
51
|
+
if (candidate.schema_version !== 1
|
|
52
|
+
|| !validPublicKey(candidate.owner_wallet)
|
|
53
|
+
|| !validPublicKey(candidate.session_public_key)
|
|
54
|
+
|| !validSecretKey(candidate.session_secret_key)
|
|
55
|
+
|| !["ask", "limits", "instant"].includes(candidate.autonomy ?? "")
|
|
56
|
+
|| !Number.isSafeInteger(candidate.connected_at_ms)) {
|
|
57
|
+
throw new Error(`Strata trading credentials are invalid: ${path}`);
|
|
58
|
+
}
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
export async function writeTradingConnection(connection, path = tradingCredentialsPath()) {
|
|
62
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
63
|
+
await writeFile(path, `${JSON.stringify(connection, null, 2)}\n`, {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
67
|
+
// `mode` is honored on creation. chmod also tightens a pre-existing file.
|
|
68
|
+
if (process.platform !== "win32")
|
|
69
|
+
await chmod(path, 0o600);
|
|
70
|
+
}
|
|
71
|
+
export async function removeTradingConnection(path = tradingCredentialsPath()) {
|
|
72
|
+
try {
|
|
73
|
+
await unlink(path);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT")
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Explicit environment variables win; otherwise load the private local file. */
|
|
81
|
+
export async function loadTradingEnvironment(env = process.env) {
|
|
82
|
+
if (env.STRATA_SESSION_SECRET_KEY || env.STRATA_OWNER_WALLET)
|
|
83
|
+
return { ...env };
|
|
84
|
+
const path = tradingCredentialsPath(env);
|
|
85
|
+
const connection = await readTradingConnection(path);
|
|
86
|
+
if (!connection)
|
|
87
|
+
return { ...env };
|
|
88
|
+
return {
|
|
89
|
+
...env,
|
|
90
|
+
STRATA_OWNER_WALLET: connection.owner_wallet,
|
|
91
|
+
STRATA_SESSION_PUBLIC_KEY: connection.session_public_key,
|
|
92
|
+
STRATA_SESSION_SECRET_KEY: connection.session_secret_key,
|
|
93
|
+
STRATA_AUTONOMY: env.STRATA_AUTONOMY ?? connection.autonomy,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function pairingWebBase(raw) {
|
|
97
|
+
const url = new URL(raw);
|
|
98
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
99
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
|
|
100
|
+
throw new Error("pairing web base must use HTTPS (or HTTP on localhost)");
|
|
101
|
+
}
|
|
102
|
+
url.pathname = "";
|
|
103
|
+
url.search = "";
|
|
104
|
+
url.hash = "";
|
|
105
|
+
return url.toString().replace(/\/$/, "");
|
|
106
|
+
}
|
|
107
|
+
function launchBrowser(url) {
|
|
108
|
+
const command = process.platform === "darwin"
|
|
109
|
+
? { file: "open", args: [url] }
|
|
110
|
+
: process.platform === "win32"
|
|
111
|
+
? { file: "cmd", args: ["/c", "start", "", url] }
|
|
112
|
+
: { file: "xdg-open", args: [url] };
|
|
113
|
+
const child = spawn(command.file, command.args, { detached: true, stdio: "ignore" });
|
|
114
|
+
child.on("error", () => undefined);
|
|
115
|
+
child.unref();
|
|
116
|
+
}
|
|
117
|
+
function successHtml(action, wallet) {
|
|
118
|
+
const connected = action === "connect";
|
|
119
|
+
return `<!doctype html>
|
|
120
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
|
121
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
|
|
122
|
+
<title>Strata agent ${connected ? "connected" : "disconnected"}</title>
|
|
123
|
+
<style>html{color-scheme:dark}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#080b11;color:#e2e8f0;font:15px Inter,system-ui,sans-serif}.card{width:min(520px,calc(100% - 48px));padding:28px;border:1px solid transparent;border-radius:16px;background:linear-gradient(#10141c,#090c12) padding-box,linear-gradient(120deg,#22d3ee,#8b5cf6) border-box;box-shadow:0 22px 80px #0008}h1{margin:0 0 10px;color:white;font-size:24px}p{line-height:1.55;color:#94a3b8}.ok{color:#34d399;font-weight:700}.mono{font-family:ui-monospace,monospace;color:#cbd5e1}</style>
|
|
124
|
+
</head><body><main class="card"><div class="ok">${connected ? "CONNECTED" : "REVOKED"}</div>
|
|
125
|
+
<h1>${connected ? "Your agent can trade" : "Agent access removed"}</h1>
|
|
126
|
+
<p>${connected ? "The session secret was saved only on this computer. Restart or refresh your MCP client, then trade normally." : "The local trading credential has been removed. Read-only Strata tools still work."}</p>
|
|
127
|
+
<p class="mono">Wallet ${wallet.slice(0, 6)}…${wallet.slice(-6)}</p></main></body></html>`;
|
|
128
|
+
}
|
|
129
|
+
async function waitForOnChainSession(apiBase, action, ownerWallet, sessionPublicKey) {
|
|
130
|
+
const deadline = Date.now() + 60_000;
|
|
131
|
+
let lastState = "unavailable";
|
|
132
|
+
while (Date.now() < deadline) {
|
|
133
|
+
try {
|
|
134
|
+
const query = new URLSearchParams({
|
|
135
|
+
wallet_address: ownerWallet,
|
|
136
|
+
session_public_key: sessionPublicKey,
|
|
137
|
+
});
|
|
138
|
+
const response = await fetch(`${apiBase.replace(/\/$/, "")}/v2/vault/status?${query.toString()}`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
|
|
139
|
+
if (response.ok) {
|
|
140
|
+
const body = await response.json();
|
|
141
|
+
if (body.wallet_address === ownerWallet) {
|
|
142
|
+
lastState = typeof body.session?.state === "string" ? body.session.state : "absent";
|
|
143
|
+
const exactSession = body.session?.session_public_key === sessionPublicKey;
|
|
144
|
+
if (action === "connect" && exactSession && body.session?.state === "active")
|
|
145
|
+
return;
|
|
146
|
+
if (action === "disconnect" && (!body.session || (exactSession && body.session.state === "absent")))
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
lastState = `http_${response.status}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
lastState = error instanceof Error ? error.name : "unavailable";
|
|
156
|
+
}
|
|
157
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 1_000));
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Strata could not confirm the ${action === "connect" ? "active" : "revoked"} session on-chain `
|
|
160
|
+
+ `(last state: ${lastState}). The local credential was not changed.`);
|
|
161
|
+
}
|
|
162
|
+
async function waitForPairingCallback(action, sessionPublicKey, state, onComplete) {
|
|
163
|
+
let settle;
|
|
164
|
+
let reject;
|
|
165
|
+
const completion = new Promise((resolvePromise, rejectPromise) => {
|
|
166
|
+
settle = resolvePromise;
|
|
167
|
+
reject = rejectPromise;
|
|
168
|
+
});
|
|
169
|
+
let finished = false;
|
|
170
|
+
const server = createServer(async (request, response) => {
|
|
171
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
172
|
+
if (request.method !== "GET" || requestUrl.pathname !== `/complete/${state}`) {
|
|
173
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
174
|
+
response.end("Not found");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (finished) {
|
|
178
|
+
response.writeHead(409, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
179
|
+
response.end("Pairing already completed");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const ownerWallet = requestUrl.searchParams.get("owner_wallet") ?? "";
|
|
183
|
+
const returnedSession = requestUrl.searchParams.get("session_public_key") ?? "";
|
|
184
|
+
if (!validPublicKey(ownerWallet) || returnedSession !== sessionPublicKey) {
|
|
185
|
+
response.writeHead(400, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
186
|
+
response.end("Invalid Strata pairing callback");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
finished = true;
|
|
190
|
+
try {
|
|
191
|
+
await onComplete(ownerWallet);
|
|
192
|
+
response.writeHead(200, {
|
|
193
|
+
"content-type": "text/html; charset=utf-8",
|
|
194
|
+
"cache-control": "no-store",
|
|
195
|
+
"x-content-type-options": "nosniff",
|
|
196
|
+
"x-frame-options": "DENY",
|
|
197
|
+
});
|
|
198
|
+
response.end(successHtml(action, ownerWallet));
|
|
199
|
+
settle(ownerWallet);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
response.writeHead(500, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
203
|
+
response.end("Could not save the local Strata connection");
|
|
204
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
server.close();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
server.on("error", (error) => reject(error));
|
|
211
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
212
|
+
server.once("error", rejectListen);
|
|
213
|
+
server.listen(0, "127.0.0.1", () => {
|
|
214
|
+
server.off("error", rejectListen);
|
|
215
|
+
resolveListen();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
const address = server.address();
|
|
219
|
+
if (!address || typeof address === "string") {
|
|
220
|
+
server.close();
|
|
221
|
+
throw new Error("could not start the local Strata pairing callback");
|
|
222
|
+
}
|
|
223
|
+
const timeout = setTimeout(() => {
|
|
224
|
+
if (finished)
|
|
225
|
+
return;
|
|
226
|
+
finished = true;
|
|
227
|
+
server.close();
|
|
228
|
+
reject(new Error("Strata pairing timed out after 10 minutes"));
|
|
229
|
+
}, PAIRING_TIMEOUT_MS);
|
|
230
|
+
timeout.unref();
|
|
231
|
+
completion.finally(() => clearTimeout(timeout)).catch(() => undefined);
|
|
232
|
+
return {
|
|
233
|
+
callbackUrl: `http://127.0.0.1:${address.port}/complete/${state}`,
|
|
234
|
+
completion,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export async function runLocalPairing(options) {
|
|
238
|
+
const env = options.env ?? process.env;
|
|
239
|
+
const path = options.credentialsFile ?? tradingCredentialsPath(env);
|
|
240
|
+
const existing = await readTradingConnection(path);
|
|
241
|
+
if (options.action === "connect" && existing) {
|
|
242
|
+
throw new Error(`Trading is already connected for wallet ${existing.owner_wallet}. `
|
|
243
|
+
+ "Run strata-mcp disconnect before replacing this session.");
|
|
244
|
+
}
|
|
245
|
+
if (options.action === "disconnect" && !existing) {
|
|
246
|
+
process.stdout.write("Strata trading is not connected. Read-only tools remain ready.\n");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
let generated;
|
|
250
|
+
if (existing) {
|
|
251
|
+
generated = existing;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const keypair = await generateSessionKeypair();
|
|
255
|
+
generated = {
|
|
256
|
+
schema_version: 1,
|
|
257
|
+
owner_wallet: "",
|
|
258
|
+
session_public_key: keypair.publicKey,
|
|
259
|
+
session_secret_key: keypair.secretKey,
|
|
260
|
+
autonomy: "instant",
|
|
261
|
+
connected_at_ms: 0,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const state = randomBytes(24).toString("hex");
|
|
265
|
+
const callback = await waitForPairingCallback(options.action, generated.session_public_key, state, async (ownerWallet) => {
|
|
266
|
+
await waitForOnChainSession(options.apiBase ?? DEFAULT_API_BASE, options.action, ownerWallet, generated.session_public_key);
|
|
267
|
+
if (options.action === "disconnect") {
|
|
268
|
+
if (existing?.owner_wallet !== ownerWallet) {
|
|
269
|
+
throw new Error("the revoking wallet does not own this local Strata connection");
|
|
270
|
+
}
|
|
271
|
+
await removeTradingConnection(path);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await writeTradingConnection({
|
|
275
|
+
...generated,
|
|
276
|
+
owner_wallet: ownerWallet,
|
|
277
|
+
connected_at_ms: (options.nowMs ?? Date.now)(),
|
|
278
|
+
}, path);
|
|
279
|
+
});
|
|
280
|
+
const webBase = pairingWebBase(options.webBase ?? DEFAULT_PAIRING_WEB_BASE);
|
|
281
|
+
const agentUrl = new URL("/agents", webBase);
|
|
282
|
+
agentUrl.searchParams.set("pair", options.action);
|
|
283
|
+
agentUrl.searchParams.set("session_public_key", generated.session_public_key);
|
|
284
|
+
agentUrl.searchParams.set("callback", callback.callbackUrl);
|
|
285
|
+
process.stdout.write(`${options.action === "connect" ? "Connect" : "Revoke"} Strata agent access in your browser:\n${agentUrl.toString()}\n\n`);
|
|
286
|
+
if (options.openBrowser !== false)
|
|
287
|
+
launchBrowser(agentUrl.toString());
|
|
288
|
+
process.stdout.write("Waiting for the owner-wallet signature…\n");
|
|
289
|
+
const ownerWallet = await callback.completion;
|
|
290
|
+
process.stdout.write(options.action === "connect"
|
|
291
|
+
? `✓ Trading connected for ${ownerWallet}. Credentials saved privately at ${path}.\nRestart or refresh your MCP client.\n`
|
|
292
|
+
: `✓ Session revoked for ${ownerWallet}. Local trading credentials removed.\n`);
|
|
293
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratabook/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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.13",
|
|
49
49
|
"zod": "^3.25.76"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|