@okx_ai/okx-trade-mcp 1.2.8 → 1.2.9-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,20 +5,25 @@ import { parseArgs } from "util";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
7
  // ../core/dist/index.js
8
- import { ProxyAgent } from "undici";
8
+ import { Agent, ProxyAgent } from "undici";
9
+ import { unlink } from "fs/promises";
10
+ import { execFile } from "child_process";
11
+ import { access, constants } from "fs/promises";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
9
14
  import { createHmac } from "crypto";
10
15
  import fs from "fs";
11
16
  import path from "path";
12
17
  import os from "os";
13
18
  import { writeFileSync, renameSync, unlinkSync, mkdirSync } from "fs";
14
- import { join, resolve, basename, sep } from "path";
19
+ import { join as join2, resolve, basename, sep } from "path";
15
20
  import { randomUUID } from "crypto";
16
21
  import yauzl from "yauzl";
17
- import { join as join3, dirname as dirname2 } from "path";
18
- import { homedir } from "os";
19
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3 } from "fs";
20
- import { join as join4, dirname as dirname3 } from "path";
22
+ import { join as join4, dirname as dirname2 } from "path";
21
23
  import { homedir as homedir2 } from "os";
24
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3 } from "fs";
25
+ import { join as join5, dirname as dirname3 } from "path";
26
+ import { homedir as homedir3 } from "os";
22
27
 
23
28
  // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
24
29
  function getLineColFromPtr(string, ptr) {
@@ -707,8 +712,8 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
707
712
 
708
713
  // ../core/dist/index.js
709
714
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
710
- import { join as join5 } from "path";
711
- import { homedir as homedir3 } from "os";
715
+ import { join as join6 } from "path";
716
+ import { homedir as homedir4 } from "os";
712
717
  import fs2 from "fs";
713
718
  import path2 from "path";
714
719
  import os2 from "os";
@@ -716,6 +721,60 @@ import * as fs3 from "fs";
716
721
  import * as path3 from "path";
717
722
  import * as os3 from "os";
718
723
  import { execFileSync } from "child_process";
724
+ var EXEC_TIMEOUT_MS = 15e3;
725
+ var DOH_BIN_DIR = join(homedir(), ".okx", "bin");
726
+ var DOH_CACHE_FILE = join(homedir(), ".okx", ".doh-cache.json");
727
+ function getDohBinaryPath() {
728
+ if (process.env.OKX_DOH_BINARY_PATH) {
729
+ return process.env.OKX_DOH_BINARY_PATH;
730
+ }
731
+ const ext = process.platform === "win32" ? ".exe" : "";
732
+ return join(DOH_BIN_DIR, `okx-doh-resolver${ext}`);
733
+ }
734
+ function getDohCachePath() {
735
+ return DOH_CACHE_FILE;
736
+ }
737
+ async function dohBinaryExists() {
738
+ try {
739
+ const flag = process.platform === "win32" ? constants.F_OK : constants.X_OK;
740
+ await access(getDohBinaryPath(), flag);
741
+ return true;
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ function execDohBinary(domain) {
747
+ const binPath = getDohBinaryPath();
748
+ return new Promise((resolve3) => {
749
+ execFile(
750
+ binPath,
751
+ ["--domain", domain],
752
+ { timeout: EXEC_TIMEOUT_MS, encoding: "utf-8" },
753
+ (error, stdout) => {
754
+ if (error) {
755
+ resolve3(null);
756
+ return;
757
+ }
758
+ try {
759
+ const result = JSON.parse(stdout);
760
+ if (result.code === 0 && result.data) {
761
+ resolve3(result.data);
762
+ } else {
763
+ resolve3(null);
764
+ }
765
+ } catch {
766
+ resolve3(null);
767
+ }
768
+ }
769
+ );
770
+ });
771
+ }
772
+ async function resolveDoh(domain) {
773
+ if (!await dohBinaryExists()) {
774
+ return null;
775
+ }
776
+ return execDohBinary(domain);
777
+ }
719
778
  function getNow() {
720
779
  return (/* @__PURE__ */ new Date()).toISOString();
721
780
  }
@@ -934,12 +993,80 @@ var OkxRestClient = class _OkxRestClient {
934
993
  config;
935
994
  rateLimiter;
936
995
  dispatcher;
937
- constructor(config) {
996
+ // DoH proxy state (lazy-resolved on first request)
997
+ dohResolverFn;
998
+ dohResolved = false;
999
+ dohNode = null;
1000
+ dohAgent = null;
1001
+ dohBaseUrl = null;
1002
+ /**
1003
+ * @param config - OKX API client configuration
1004
+ * @param options - Optional overrides (e.g. custom DoH resolver for testing).
1005
+ * Pass `{ resolveDoh: null }` to disable DoH entirely.
1006
+ */
1007
+ constructor(config, options) {
938
1008
  this.config = config;
939
1009
  this.rateLimiter = new RateLimiter(3e4, config.verbose);
940
1010
  if (config.proxyUrl) {
941
1011
  this.dispatcher = new ProxyAgent(config.proxyUrl);
942
1012
  }
1013
+ this.dohResolverFn = options?.resolveDoh !== void 0 ? options.resolveDoh : resolveDoh;
1014
+ }
1015
+ /**
1016
+ * Lazily resolve the DoH proxy node on the first request.
1017
+ * Skipped entirely when the user has configured proxy_url or DoH is disabled.
1018
+ * On failure, silently falls back to direct connection.
1019
+ */
1020
+ async ensureDoh() {
1021
+ if (this.dohResolved || this.dispatcher || !this.dohResolverFn) return;
1022
+ this.dohResolved = true;
1023
+ try {
1024
+ const { hostname, protocol } = new URL(this.config.baseUrl);
1025
+ const node = await this.dohResolverFn(hostname);
1026
+ if (!node) return;
1027
+ if (node.ip === hostname) {
1028
+ if (this.config.verbose) {
1029
+ vlog(`DoH: resolved ip matches hostname (${hostname}), using direct connection`);
1030
+ }
1031
+ return;
1032
+ }
1033
+ this.dohNode = node;
1034
+ this.dohBaseUrl = `${protocol}//${node.host}`;
1035
+ this.dohAgent = new Agent({
1036
+ connect: {
1037
+ lookup: (_hostname, options, callback) => {
1038
+ if (options?.all) {
1039
+ callback(null, [{ address: node.ip, family: 4 }]);
1040
+ } else {
1041
+ callback(null, node.ip, 4);
1042
+ }
1043
+ }
1044
+ }
1045
+ });
1046
+ if (this.config.verbose) {
1047
+ vlog(`DoH proxy active: ${hostname} \u2192 ${node.host} (${node.ip}), ttl=${node.ttl}s`);
1048
+ }
1049
+ } catch (err) {
1050
+ if (this.config.verbose) {
1051
+ const cause = err instanceof Error ? err.message : String(err);
1052
+ vlog(`DoH resolution failed, falling back to direct: ${cause}`);
1053
+ }
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Invalidate DoH state and delete the binary's cache file so the next
1058
+ * resolution performs a fresh lookup. Called on network-level failures
1059
+ * when a DoH proxy node was in use.
1060
+ */
1061
+ async invalidateDoh() {
1062
+ this.dohNode = null;
1063
+ this.dohAgent = null;
1064
+ this.dohBaseUrl = null;
1065
+ this.dohResolved = false;
1066
+ try {
1067
+ await unlink(getDohCachePath());
1068
+ } catch {
1069
+ }
943
1070
  }
944
1071
  logRequest(method, url, auth) {
945
1072
  if (!this.config.verbose) return;
@@ -1110,13 +1237,18 @@ var OkxRestClient = class _OkxRestClient {
1110
1237
  * Security: validates Content-Type and enforces maxBytes limit.
1111
1238
  */
1112
1239
  async privatePostBinary(path4, body, opts) {
1240
+ await this.ensureDoh();
1113
1241
  const maxBytes = opts?.maxBytes ?? _OkxRestClient.DEFAULT_MAX_BYTES;
1114
1242
  const expectedCT = opts?.expectedContentType ?? "application/octet-stream";
1115
1243
  const bodyJson = body ? JSON.stringify(body) : "";
1116
1244
  const endpoint = `POST ${path4}`;
1117
- this.logRequest("POST", `${this.config.baseUrl}${path4}`, "private");
1245
+ const baseUrl = this.dohNode ? this.dohBaseUrl : this.config.baseUrl;
1246
+ this.logRequest("POST", `${baseUrl}${path4}`, "private");
1118
1247
  const reqConfig = { method: "POST", path: path4, auth: "private" };
1119
1248
  const headers = this.buildHeaders(reqConfig, path4, bodyJson, getNow());
1249
+ if (this.dohNode) {
1250
+ headers.set("User-Agent", "OKX/2.7.2");
1251
+ }
1120
1252
  const t0 = Date.now();
1121
1253
  const response = await this.fetchBinary(path4, endpoint, headers, bodyJson, t0);
1122
1254
  const elapsed = Date.now() - t0;
@@ -1146,14 +1278,19 @@ var OkxRestClient = class _OkxRestClient {
1146
1278
  /** Execute fetch for binary endpoint, wrapping network errors. */
1147
1279
  async fetchBinary(path4, endpoint, headers, bodyJson, t0) {
1148
1280
  try {
1281
+ const baseUrl = this.dohNode ? this.dohBaseUrl : this.config.baseUrl;
1149
1282
  const fetchOptions = {
1150
1283
  method: "POST",
1151
1284
  headers,
1152
1285
  body: bodyJson || void 0,
1153
1286
  signal: AbortSignal.timeout(this.config.timeoutMs)
1154
1287
  };
1155
- if (this.dispatcher) fetchOptions.dispatcher = this.dispatcher;
1156
- return await fetch(`${this.config.baseUrl}${path4}`, fetchOptions);
1288
+ if (this.dispatcher) {
1289
+ fetchOptions.dispatcher = this.dispatcher;
1290
+ } else if (this.dohAgent) {
1291
+ fetchOptions.dispatcher = this.dohAgent;
1292
+ }
1293
+ return await fetch(`${baseUrl}${path4}`, fetchOptions);
1157
1294
  } catch (error) {
1158
1295
  if (this.config.verbose) {
1159
1296
  vlog(`\u2717 NetworkError after ${Date.now() - t0}ms: ${error instanceof Error ? error.message : String(error)}`);
@@ -1184,9 +1321,11 @@ var OkxRestClient = class _OkxRestClient {
1184
1321
  // JSON request
1185
1322
  // ---------------------------------------------------------------------------
1186
1323
  async request(reqConfig) {
1324
+ await this.ensureDoh();
1187
1325
  const queryString = buildQueryString(reqConfig.query);
1188
1326
  const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1189
- const url = `${this.config.baseUrl}${requestPath}`;
1327
+ const baseUrl = this.dohNode ? this.dohBaseUrl : this.config.baseUrl;
1328
+ const url = `${baseUrl}${requestPath}`;
1190
1329
  const bodyJson = reqConfig.body ? JSON.stringify(reqConfig.body) : "";
1191
1330
  const timestamp = getNow();
1192
1331
  this.logRequest(reqConfig.method, url, reqConfig.auth);
@@ -1194,6 +1333,9 @@ var OkxRestClient = class _OkxRestClient {
1194
1333
  await this.rateLimiter.consume(reqConfig.rateLimit);
1195
1334
  }
1196
1335
  const headers = this.buildHeaders(reqConfig, requestPath, bodyJson, timestamp);
1336
+ if (this.dohNode) {
1337
+ headers.set("User-Agent", "OKX/2.7.2");
1338
+ }
1197
1339
  const t0 = Date.now();
1198
1340
  let response;
1199
1341
  try {
@@ -1205,9 +1347,18 @@ var OkxRestClient = class _OkxRestClient {
1205
1347
  };
1206
1348
  if (this.dispatcher) {
1207
1349
  fetchOptions.dispatcher = this.dispatcher;
1350
+ } else if (this.dohAgent) {
1351
+ fetchOptions.dispatcher = this.dohAgent;
1208
1352
  }
1209
1353
  response = await fetch(url, fetchOptions);
1210
1354
  } catch (error) {
1355
+ if (this.dohNode) {
1356
+ if (this.config.verbose) {
1357
+ vlog(`DoH request failed, invalidating cache and retrying: ${error instanceof Error ? error.message : String(error)}`);
1358
+ }
1359
+ await this.invalidateDoh();
1360
+ return this.request(reqConfig);
1361
+ }
1211
1362
  if (this.config.verbose) {
1212
1363
  const elapsed2 = Date.now() - t0;
1213
1364
  const cause = error instanceof Error ? error.message : String(error);
@@ -2833,7 +2984,7 @@ function safeWriteFile(targetDir, fileName, data) {
2833
2984
  throw new Error(`Invalid file name: "${fileName}"`);
2834
2985
  }
2835
2986
  const resolvedDir = resolve(targetDir);
2836
- const filePath = join(resolvedDir, safeName);
2987
+ const filePath = join2(resolvedDir, safeName);
2837
2988
  const resolvedPath = resolve(filePath);
2838
2989
  if (!resolvedPath.startsWith(resolvedDir + sep)) {
2839
2990
  throw new Error(`Path traversal detected: "${fileName}" resolves outside target directory`);
@@ -2864,7 +3015,7 @@ async function downloadSkillZip(client, name, targetDir) {
2864
3015
  return filePath;
2865
3016
  }
2866
3017
  var DEFAULT_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
2867
- var DEFAULT_REGISTRY_PATH = join3(homedir(), ".okx", "skills", "registry.json");
3018
+ var DEFAULT_REGISTRY_PATH = join4(homedir2(), ".okx", "skills", "registry.json");
2868
3019
  function registerSkillsTools() {
2869
3020
  return [
2870
3021
  {
@@ -6966,7 +7117,7 @@ function toMcpTool(tool) {
6966
7117
  };
6967
7118
  }
6968
7119
  function configFilePath() {
6969
- return join4(homedir2(), ".okx", "config.toml");
7120
+ return join5(homedir3(), ".okx", "config.toml");
6970
7121
  }
6971
7122
  function readFullConfig() {
6972
7123
  const path4 = configFilePath();
@@ -7102,7 +7253,7 @@ function loadConfig(cli) {
7102
7253
  verbose: cli.verbose ?? false
7103
7254
  };
7104
7255
  }
7105
- var CACHE_FILE = join5(homedir3(), ".okx", "update-check.json");
7256
+ var CACHE_FILE = join6(homedir4(), ".okx", "update-check.json");
7106
7257
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7107
7258
  function readCache() {
7108
7259
  try {
@@ -7115,7 +7266,7 @@ function readCache() {
7115
7266
  }
7116
7267
  function writeCache(cache) {
7117
7268
  try {
7118
- mkdirSync5(join5(homedir3(), ".okx"), { recursive: true });
7269
+ mkdirSync5(join6(homedir4(), ".okx"), { recursive: true });
7119
7270
  writeFileSync4(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7120
7271
  } catch {
7121
7272
  }
@@ -7384,7 +7535,7 @@ var _require = createRequire(import.meta.url);
7384
7535
  var pkg = _require("../package.json");
7385
7536
  var SERVER_NAME = "okx-trade-mcp";
7386
7537
  var SERVER_VERSION = pkg.version;
7387
- var GIT_HASH = true ? "0b9b3b7" : "dev";
7538
+ var GIT_HASH = true ? "53b8eb6" : "dev";
7388
7539
 
7389
7540
  // src/server.ts
7390
7541
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";